Theme Management via Admin UI
parent
2d5f0fc2a1
commit
b3e28eade1
|
|
@ -77,6 +77,11 @@ public class FileSystemThemeProvider implements ThemeProvider {
|
||||||
@Inject
|
@Inject
|
||||||
private ThemeFileInfoUtil themeFileInfoUtil;
|
private ThemeFileInfoUtil themeFileInfoUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "FileSystemThemeProvider";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ThemeInfo> getThemes() {
|
public List<ThemeInfo> getThemes() {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ public class StaticThemeProvider implements ThemeProvider {
|
||||||
@Inject
|
@Inject
|
||||||
private ThemeFileInfoUtil themeFileInfoUtil;
|
private ThemeFileInfoUtil themeFileInfoUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "StaticThemeProvider";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ThemeInfo> getThemes() {
|
public List<ThemeInfo> getThemes() {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ import javax.enterprise.context.RequestScoped;
|
||||||
*/
|
*/
|
||||||
public interface ThemeProvider extends Serializable {
|
public interface ThemeProvider extends Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human readable name for the {@code ThemeProvider} implementation.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a list of all themes provided by this theme provider. The list
|
* Provides a list of all themes provided by this theme provider. The list
|
||||||
* should be ordered by the name of the theme.
|
* should be ordered by the name of the theme.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.enterprise.context.RequestScoped;
|
import javax.enterprise.context.RequestScoped;
|
||||||
import javax.enterprise.inject.Any;
|
import javax.enterprise.inject.Any;
|
||||||
|
|
@ -53,6 +55,7 @@ public class Themes implements Serializable {
|
||||||
@Any
|
@Any
|
||||||
private Instance<ThemeProvider> providers;
|
private Instance<ThemeProvider> providers;
|
||||||
//
|
//
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ThemeProcessors themeProcessors;
|
private ThemeProcessors themeProcessors;
|
||||||
|
|
||||||
|
|
@ -108,6 +111,100 @@ public class Themes implements Serializable {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ThemeProvider> getThemeProviders() {
|
||||||
|
return providers
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ThemeProvider> findThemeProviderInstance(
|
||||||
|
final Class<? extends ThemeProvider> ofClazz
|
||||||
|
) {
|
||||||
|
final Instance<? extends ThemeProvider> instance = providers
|
||||||
|
.select(ofClazz);
|
||||||
|
if (instance.isResolvable()) {
|
||||||
|
return Optional.of(instance.get());
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeInfo createTheme(
|
||||||
|
final String themeName, final String providerName
|
||||||
|
) {
|
||||||
|
final Class<ThemeProvider> providerClass = getThemeProviderClass(
|
||||||
|
providerName
|
||||||
|
);
|
||||||
|
|
||||||
|
return createTheme(
|
||||||
|
themeName,
|
||||||
|
findThemeProviderInstance(providerClass).orElseThrow(
|
||||||
|
() -> new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"No instance of ThemeProvider implementation %s available.",
|
||||||
|
providerName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeInfo createTheme(
|
||||||
|
final String themeName, final ThemeProvider themeProvider
|
||||||
|
) {
|
||||||
|
return themeProvider.createTheme(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteTheme(final String themeName) {
|
||||||
|
final Optional<ThemeProvider> provider = findProviderOfTheme(
|
||||||
|
Objects.requireNonNull(
|
||||||
|
themeName,
|
||||||
|
"Can't delete theme null."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (provider.isPresent()) {
|
||||||
|
provider.get().deleteTheme(themeName);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"No provider providing a theme named %s found.",
|
||||||
|
themeName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishTheme(final String themeName) {
|
||||||
|
final Optional<ThemeProvider> provider = findProviderOfTheme(themeName);
|
||||||
|
|
||||||
|
if (provider.isPresent()) {
|
||||||
|
provider.get().publishTheme(themeName);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"No provider providing a theme named %s found.",
|
||||||
|
themeName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpublishTheme(final String themeName) {
|
||||||
|
final Optional<ThemeProvider> provider = findProviderOfTheme(themeName);
|
||||||
|
|
||||||
|
if (provider.isPresent()) {
|
||||||
|
provider.get().unpublishTheme(themeName);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"No provider providing a theme named %s found.",
|
||||||
|
themeName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates HTML from the result of rendering a {@link PageModel}.
|
* Creates HTML from the result of rendering a {@link PageModel}.
|
||||||
*
|
*
|
||||||
|
|
@ -240,7 +337,37 @@ public class Themes implements Serializable {
|
||||||
|
|
||||||
final ThemeProvider provider = forTheme.get();
|
final ThemeProvider provider = forTheme.get();
|
||||||
provider.deleteThemeFile(theme.getName(), path);
|
provider.deleteThemeFile(theme.getName(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Class<ThemeProvider> getThemeProviderClass(
|
||||||
|
final String providerName
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return (Class<ThemeProvider>) Class.forName(providerName);
|
||||||
|
} catch (ClassNotFoundException ex) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"No ThemeProvider implementation %s available.",
|
||||||
|
providerName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ThemeProvider> findProviderOfTheme(
|
||||||
|
final String themeName
|
||||||
|
) {
|
||||||
|
final List<ThemeProvider> providersList = new ArrayList<>();
|
||||||
|
providers.forEach(provider -> providersList.add(provider));
|
||||||
|
|
||||||
|
return providersList
|
||||||
|
.stream()
|
||||||
|
.filter(
|
||||||
|
current -> current.providesTheme(
|
||||||
|
themeName, ThemeVersion.DRAFT
|
||||||
|
)
|
||||||
|
).findAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ public class DatabaseThemeProvider implements ThemeProvider {
|
||||||
@Inject
|
@Inject
|
||||||
private ThemeRepository themeRepository;
|
private ThemeRepository themeRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "DatabaseThemeProvider";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(Transactional.TxType.REQUIRED)
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
public List<ThemeInfo> getThemes() {
|
public List<ThemeInfo> getThemes() {
|
||||||
|
|
@ -419,6 +424,7 @@ public class DatabaseThemeProvider implements ThemeProvider {
|
||||||
private class DataFileOutputStream extends OutputStream {
|
private class DataFileOutputStream extends OutputStream {
|
||||||
|
|
||||||
private final DataFile dataFile;
|
private final DataFile dataFile;
|
||||||
|
|
||||||
private final ByteArrayOutputStream outputStream;
|
private final ByteArrayOutputStream outputStream;
|
||||||
|
|
||||||
private DataFileOutputStream(final DataFile dataFile) {
|
private DataFileOutputStream(final DataFile dataFile) {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import javax.enterprise.context.RequestScoped;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.mvc.Controller;
|
import javax.mvc.Controller;
|
||||||
import javax.mvc.Models;
|
import javax.mvc.Models;
|
||||||
import javax.mvc.MvcContext;
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,6 @@ import javax.inject.Inject;
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ApplicationsPage implements AdminPage {
|
public class ApplicationsPage implements AdminPage {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(
|
|
||||||
ApplicationsPage.class
|
|
||||||
);
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ApplicationManager applicationManager;
|
private ApplicationManager applicationManager;
|
||||||
|
|
||||||
|
|
@ -65,8 +61,8 @@ public class ApplicationsPage implements AdminPage {
|
||||||
@Override
|
@Override
|
||||||
public String getUriIdentifier() {
|
public String getUriIdentifier() {
|
||||||
return String.format(
|
return String.format(
|
||||||
"%s#getApplicationTypes", ApplicationsController.class
|
"%s#getApplicationTypes",
|
||||||
.getSimpleName()
|
ApplicationsController.class.getSimpleName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import java.util.Set;
|
||||||
|
|
||||||
import org.libreccm.ui.admin.AdminConstants;
|
import org.libreccm.ui.admin.AdminConstants;
|
||||||
import org.libreccm.ui.admin.AdminPage;
|
import org.libreccm.ui.admin.AdminPage;
|
||||||
import org.libreccm.ui.admin.imexport.ImExportController;
|
|
||||||
|
|
||||||
import javax.enterprise.context.ApplicationScoped;
|
import javax.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
|
@ -76,7 +75,7 @@ public class SystemInformationPage implements AdminPage {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPosition() {
|
public int getPosition() {
|
||||||
return 70;
|
return 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 LibreCCM Foundation.
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package org.libreccm.ui.admin.themes;
|
||||||
|
|
||||||
|
import org.libreccm.core.CoreConstants;
|
||||||
|
import org.libreccm.security.AuthorizationRequired;
|
||||||
|
import org.libreccm.security.RequiresPrivilege;
|
||||||
|
import org.libreccm.theming.ThemeInfo;
|
||||||
|
import org.libreccm.theming.Themes;
|
||||||
|
import org.libreccm.theming.manager.ThemeManager;
|
||||||
|
;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.enterprise.context.RequestScoped;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.mvc.Controller;
|
||||||
|
import javax.mvc.Models;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import javax.ws.rs.FormParam;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@RequestScoped
|
||||||
|
@Controller
|
||||||
|
@Path("/themes")
|
||||||
|
public class ThemesController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Themes themes;
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Models models;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/")
|
||||||
|
@AuthorizationRequired
|
||||||
|
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
|
||||||
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
|
public String getThemes() {
|
||||||
|
return "org/libreccm/ui/admin/themes/themes.xhtml";
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/new")
|
||||||
|
@AuthorizationRequired
|
||||||
|
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
|
||||||
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
|
public String createTheme(
|
||||||
|
@FormParam("themeName") final String themeName,
|
||||||
|
@FormParam("providerName") final String providerName
|
||||||
|
) {
|
||||||
|
themes.createTheme(themeName, providerName);
|
||||||
|
|
||||||
|
return "redirect:themes/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{themeName}/publish")
|
||||||
|
@AuthorizationRequired
|
||||||
|
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
|
||||||
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
|
public String publishTheme(final String themeName) {
|
||||||
|
themes.publishTheme(themeName);
|
||||||
|
|
||||||
|
return "redirect:themes/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{themeName}/unpublish")
|
||||||
|
@AuthorizationRequired
|
||||||
|
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
|
||||||
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
|
public String unpublishTheme(final String themeName) {
|
||||||
|
themes.unpublishTheme(themeName);
|
||||||
|
|
||||||
|
return "redirect:themes/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{themeName}/delete")
|
||||||
|
@AuthorizationRequired
|
||||||
|
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
|
||||||
|
@Transactional(Transactional.TxType.REQUIRED)
|
||||||
|
public String deleteTheme(@PathParam("themeName") final String themeName) {
|
||||||
|
themes.deleteTheme(themeName);
|
||||||
|
|
||||||
|
return "redirect:themes/";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 LibreCCM Foundation.
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package org.libreccm.ui.admin.themes;
|
||||||
|
|
||||||
|
import org.libreccm.l10n.GlobalizationHelper;
|
||||||
|
import org.libreccm.l10n.LocalizedTextsUtil;
|
||||||
|
import org.libreccm.theming.ThemeInfo;
|
||||||
|
import org.libreccm.theming.ThemeProvider;
|
||||||
|
import org.libreccm.theming.ThemeVersion;
|
||||||
|
import org.libreccm.theming.Themes;
|
||||||
|
import org.libreccm.ui.admin.AdminConstants;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.enterprise.context.RequestScoped;
|
||||||
|
import javax.enterprise.inject.Any;
|
||||||
|
import javax.enterprise.inject.Instance;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@RequestScoped
|
||||||
|
@Named("Themes")
|
||||||
|
public class ThemesModel {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private GlobalizationHelper globalizationHelper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Themes themes;
|
||||||
|
|
||||||
|
public List<ThemesTableRow> getThemes() {
|
||||||
|
return themes
|
||||||
|
.getAvailableThemes()
|
||||||
|
.stream()
|
||||||
|
.map(this::mapThemeInfo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getProviderOptions() {
|
||||||
|
return themes
|
||||||
|
.getThemeProviders()
|
||||||
|
.stream()
|
||||||
|
.filter(ThemeProvider::supportsChanges)
|
||||||
|
.filter(ThemeProvider::supportsDraftThemes)
|
||||||
|
.collect(
|
||||||
|
Collectors.toMap(
|
||||||
|
provider -> provider.getClass().getName(),
|
||||||
|
provider -> provider.getName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThemesTableRow mapThemeInfo(final ThemeInfo themeInfo) {
|
||||||
|
|
||||||
|
final LocalizedTextsUtil textsUtil = globalizationHelper
|
||||||
|
.getLocalizedTextsUtil(AdminConstants.ADMIN_BUNDLE);
|
||||||
|
|
||||||
|
final ThemesTableRow row = new ThemesTableRow();
|
||||||
|
row.setDescription(
|
||||||
|
Optional
|
||||||
|
.ofNullable(themeInfo.getManifest().getDescription())
|
||||||
|
.map(ls -> globalizationHelper.getValueFromLocalizedString(ls))
|
||||||
|
.orElse("")
|
||||||
|
);
|
||||||
|
row.setName(themeInfo.getName());
|
||||||
|
row.setProvider(themeInfo.getProvider().getName());
|
||||||
|
row.setTitle(
|
||||||
|
Optional
|
||||||
|
.ofNullable(themeInfo.getManifest().getTitle())
|
||||||
|
.map(ls -> globalizationHelper.getValueFromLocalizedString(ls))
|
||||||
|
.orElse("")
|
||||||
|
);
|
||||||
|
row.setType(themeInfo.getType());
|
||||||
|
row.setVersion(
|
||||||
|
textsUtil.getText(
|
||||||
|
String.format(
|
||||||
|
"themes.versions.%s",
|
||||||
|
Objects.toString(
|
||||||
|
themeInfo.getVersion()).toLowerCase(Locale.ROOT)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
row.setPublished(themeInfo.getVersion() == ThemeVersion.LIVE);
|
||||||
|
|
||||||
|
final Optional<ThemeProvider> themeProviderResult = themes
|
||||||
|
.findThemeProviderInstance(themeInfo.getProvider());
|
||||||
|
if (themeProviderResult.isPresent()) {
|
||||||
|
final ThemeProvider themeProvider = themeProviderResult.get();
|
||||||
|
|
||||||
|
row.setEditable(themeProvider.supportsChanges());
|
||||||
|
row.setPublishable(themeProvider.supportsDraftThemes());
|
||||||
|
} else {
|
||||||
|
row.setEditable(false);
|
||||||
|
row.setPublishable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 LibreCCM Foundation.
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package org.libreccm.ui.admin.themes;
|
||||||
|
|
||||||
|
import org.libreccm.ui.admin.AdminConstants;
|
||||||
|
import org.libreccm.ui.admin.AdminPage;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ThemesPage implements AdminPage {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Class<?>> getControllerClasses() {
|
||||||
|
final Set<Class<?>> classes = new HashSet<>();
|
||||||
|
classes.add(ThemesController.class);
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUriIdentifier() {
|
||||||
|
return String.format(
|
||||||
|
"%s#getThemes", ThemesController.class.getSimpleName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelBundle() {
|
||||||
|
return AdminConstants.ADMIN_BUNDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelKey() {
|
||||||
|
return "themes.label";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescriptionBundle() {
|
||||||
|
return AdminConstants.ADMIN_BUNDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescriptionKey() {
|
||||||
|
return "themes.description";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIcon() {
|
||||||
|
return "brush-fill";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPosition() {
|
||||||
|
return 70;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 LibreCCM Foundation.
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package org.libreccm.ui.admin.themes;
|
||||||
|
|
||||||
|
import org.libreccm.theming.ThemeVersion;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
public class ThemesTableRow implements Comparable<ThemesTableRow>, Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
private ThemeVersion themeVersion;
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private boolean editable;
|
||||||
|
|
||||||
|
private boolean publishable;
|
||||||
|
|
||||||
|
private boolean published;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setName(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeVersion getThemeVersion() {
|
||||||
|
return themeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setThemeVersion(final ThemeVersion themeVersion) {
|
||||||
|
this.themeVersion = themeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setType(final String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setVersion(final String version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProvider() {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setProvider(final String provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setTitle(final String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setDescription(final String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEditable() {
|
||||||
|
return editable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setEditable(final boolean editable) {
|
||||||
|
this.editable = editable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPublishable() {
|
||||||
|
return publishable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setPublishable(final boolean publishable) {
|
||||||
|
this.publishable = publishable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPublished() {
|
||||||
|
return published;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setPublished(final boolean published) {
|
||||||
|
this.published = published;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final ThemesTableRow other) {
|
||||||
|
return Comparator.nullsFirst(Comparator
|
||||||
|
.comparing(ThemesTableRow::getTitle)
|
||||||
|
.thenComparing(ThemesTableRow::getName)
|
||||||
|
.thenComparing(ThemesTableRow::getVersion)
|
||||||
|
.thenComparing(ThemesTableRow::getType)
|
||||||
|
.thenComparing(ThemesTableRow::getProvider)
|
||||||
|
.thenComparing(ThemesTableRow::isPublishable)
|
||||||
|
.thenComparing(ThemesTableRow::isPublished)
|
||||||
|
.thenComparing(ThemesTableRow::isEditable)
|
||||||
|
).compare(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
<!DOCTYPE html [<!ENTITY times '×'>]>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:bootstrap="http://xmlns.jcp.org/jsf/composite/components/bootstrap"
|
||||||
|
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
|
||||||
|
xmlns:f="http://xmlns.jcp.org/jsf/core"
|
||||||
|
xmlns:libreccm="http://xmlns.jcp.org/jsf/composite/components/libreccm"
|
||||||
|
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
|
||||||
|
<ui:composition template="/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml">
|
||||||
|
|
||||||
|
<ui:param name="activePage" value="applications" />
|
||||||
|
<ui:param name="title" value="#{AdminMessages['applications.label']}" />
|
||||||
|
|
||||||
|
<ui:define name="breadcrumb">
|
||||||
|
<li class="breadcrumb-item active">
|
||||||
|
#{AdminMessages['themes.label']}
|
||||||
|
</li>
|
||||||
|
</ui:define>
|
||||||
|
|
||||||
|
<ui:define name="main">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<h1>#{AdminMessages['themes.label']}</h1>
|
||||||
|
|
||||||
|
<bootstrap:modalForm actionTarget="#{mvc.uri('ThemesController#createTheme')}"
|
||||||
|
buttonIcon="plus-circle"
|
||||||
|
buttonText="#{AdminMessages['themes.create_new_theme']}"
|
||||||
|
buttonTextClass="text-right"
|
||||||
|
dialogId="create-theme-dialog">
|
||||||
|
<f:facet name="title">
|
||||||
|
<h2>#{AdminMessages['themes.dialog.new_theme.title']}</h2>
|
||||||
|
</f:facet>
|
||||||
|
<f:facet name="body">
|
||||||
|
<bootstrap:formGroupText help="#{AdminMessages['themes.dialog.new_theme.name.help']}"
|
||||||
|
inputId="create-theme-dialog-name"
|
||||||
|
label="#{AdminMessages['themes.dialog.new_theme.name.label']}"
|
||||||
|
name="themeName" />
|
||||||
|
<bootstrap:formGroupSelect help="#{AdminMessages['themes.dialog.new_theme.provider.help']}"
|
||||||
|
inputId="create-theme-dialog-provider"
|
||||||
|
label="#{AdminMessages['themes.dialog.new_theme.provider.label']}"
|
||||||
|
name="providerName"
|
||||||
|
options="#{Themes.providerOptions}">
|
||||||
|
</bootstrap:formGroupSelect>
|
||||||
|
</f:facet>
|
||||||
|
<f:facet name="footer">
|
||||||
|
<button class="btn btn-secondary"
|
||||||
|
data-dismiss="modal"
|
||||||
|
type="button" >
|
||||||
|
#{AdminMessages['themes.dialog.new_theme.close']}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
#{AdminMessages['themes.dialog.new_theme.create']}
|
||||||
|
</button>
|
||||||
|
</f:facet>
|
||||||
|
</bootstrap:modalForm>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#{AdminMessages['themes.table.headers.name']}</th>
|
||||||
|
<th>#{AdminMessages['themes.table.headers.title']}</th>
|
||||||
|
<th>#{AdminMessages['themes.table.headers.version']}</th>
|
||||||
|
<th>#{AdminMessages['themes.table.headers.type']}</th>
|
||||||
|
<th>#{AdminMessages['themes.table.headers.provider']}</th>
|
||||||
|
<th class="text-center" colspan="4">#{AdminMessages['themes.table.headers.actions']}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<c:forEach items="#{Themes.themes}" var="theme">
|
||||||
|
<tr>
|
||||||
|
<td>#{theme.name}</td>
|
||||||
|
<td>#{theme.title}</td>
|
||||||
|
<td>#{theme.version}</td>
|
||||||
|
<td>#{theme.type}</td>
|
||||||
|
<td>#{theme.provider}</td>
|
||||||
|
<td>
|
||||||
|
<c:if test="#{theme.description != null and !theme.description.isEmpty()}">
|
||||||
|
<button class="btn btn-info"
|
||||||
|
data-target="#theme-#{theme.name}-description"
|
||||||
|
data-toggle="modal"
|
||||||
|
type="button">
|
||||||
|
<bootstrap:svgIcon icon="info-circle" />
|
||||||
|
<span class="sr-only">#{AdminMessages.getMessage('themes.table.description.show', [theme.name])}</span>
|
||||||
|
</button>
|
||||||
|
<div aria-labelledby="theme-#{theme.name}-description-title"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="modal fade"
|
||||||
|
id="theme-#{theme.name}-description"
|
||||||
|
tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"
|
||||||
|
id="theme-#{theme.name}-description-title">
|
||||||
|
<h2>#{AdminMessages.getMessage('themes.dialog.description.title', [theme.name])}</h2>
|
||||||
|
<button aria-label="#{AdminMessages['themes.dialog.description.close']}"
|
||||||
|
class="close"
|
||||||
|
data-dismiss="modal"
|
||||||
|
type="button">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
#{theme.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
data-dismiss="modal"
|
||||||
|
type="button">
|
||||||
|
#{AdminMessages['themes.dialog.description.close']}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="#{theme.publishable and theme.published }">
|
||||||
|
<form action="#{mvc.uri('ThemesController#publishTheme', {'themeName': theme.name})}">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
type="button">
|
||||||
|
#{AdminMessages['themes.table.actions.republish']}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</c:when>
|
||||||
|
<c:when test="#{theme.publishable}">
|
||||||
|
<form action="#{mvc.uri('ThemesController#publishTheme', {'themeName': theme.name})}">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
type="button">
|
||||||
|
#{AdminMessages['themes.table.actions.publish']}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</c:when>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:if test="#{theme.published and theme.publishable}">
|
||||||
|
<libreccm:deleteDialog actionTarget="#{mvc.uri('ThemesController#unpublishTheme', {'themeName': theme.name})}"
|
||||||
|
buttonText="#{AdminMessages['themes.table.actions.unpublish']}"
|
||||||
|
cancelLabel="#{AdminMessages['themes.table.actions.unpublish.cancel']}"
|
||||||
|
confirmLabel="#{AdminMessages['themes.table.actions.unpublish.confirm']}"
|
||||||
|
dialogId="theme-#{theme.name}-unpublish"
|
||||||
|
dialogTitle="#{AdminMessages['themes.table.actions.unpublish.title']}"
|
||||||
|
message="#{AdminMessages.getMessage('themes.table.actions.unpublish.message', [theme.name])}"
|
||||||
|
/>
|
||||||
|
</c:if>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:if test="#{theme.editable and !theme.published}">
|
||||||
|
<libreccm:deleteDialog actionTarget="#{mvc.uri('ThemesController#deleteTheme', {'themeName': theme.name})}"
|
||||||
|
buttonText="#{AdminMessages['themes.table.actions.delete']}"
|
||||||
|
cancelLabel="#{AdminMessages['themes.table.actions.delete.cancel']}"
|
||||||
|
confirmLabel="#{AdminMessages['themes.table.actions.delete.confirm']}"
|
||||||
|
dialogId="theme-#{theme.name}-unpublish"
|
||||||
|
dialogTitle="#{AdminMessages['themes.table.actions.delete.title']}"
|
||||||
|
message="#{AdminMessages.getMessage('themes.table.actions.delete.message', [theme.name])}"
|
||||||
|
/>
|
||||||
|
</c:if>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c:forEach>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ui:define>
|
||||||
|
|
||||||
|
</ui:composition>
|
||||||
|
</html>
|
||||||
|
|
@ -493,3 +493,36 @@ sites.form.buttons.save=Save
|
||||||
applications.types.singleton=Singleton
|
applications.types.singleton=Singleton
|
||||||
applications.number_of_instances={0} instances
|
applications.number_of_instances={0} instances
|
||||||
applications.number_of_instances_one={0} instance
|
applications.number_of_instances_one={0} instance
|
||||||
|
themes.label=Themes
|
||||||
|
themes.description=Manage themes
|
||||||
|
themes.table.headers.name=Name
|
||||||
|
themes.table.headers.title=Title
|
||||||
|
themes.table.headers.version=Version
|
||||||
|
themes.table.headers.type=Type
|
||||||
|
themes.table.headers.provider=Provided by
|
||||||
|
themes.table.actions.republish=Republish
|
||||||
|
themes.table.actions.publish=Publish
|
||||||
|
themes.table.actions.unpublish=Unpublish
|
||||||
|
themes.table.actions.delete=Delete
|
||||||
|
themes.versions.live=Live
|
||||||
|
themes.versions.draft=Draft
|
||||||
|
themes.table.headers.actions=Actions
|
||||||
|
themes.dialog.description.title=Description of theme {0}
|
||||||
|
themes.dialog.description.close=Close
|
||||||
|
themes.table.description.show=Show description of theme {0}
|
||||||
|
themes.create_new_theme=Create new theme
|
||||||
|
themes.dialog.new_theme.title=Create new theme
|
||||||
|
themes.dialog.new_theme.close=Cancel
|
||||||
|
themes.dialog.new_theme.create=Create new theme
|
||||||
|
themes.dialog.new_theme.name.help=Unique name of the new theme
|
||||||
|
themes.dialog.new_theme.name.label=Name
|
||||||
|
themes.dialog.new_theme.provider.help=The provider which manages the theme.
|
||||||
|
themes.dialog.new_theme.provider.label=Provider
|
||||||
|
themes.table.actions.unpublish.cancel=Cancel
|
||||||
|
themes.table.actions.unpublish.confirm=Unpublish theme
|
||||||
|
themes.table.actions.unpublish.title=Confirm to unpublish theme
|
||||||
|
themes.table.actions.unpublish.message=Are your sure to unpublish theme {0}?
|
||||||
|
themes.table.actions.delete.cancel=Cancel
|
||||||
|
themes.table.actions.delete.confirm=Delete theme
|
||||||
|
themes.table.actions.delete.title=Confirm theme deletion
|
||||||
|
themes.table.actions.delete.message=Are you sure to delete the theme {0}?
|
||||||
|
|
|
||||||
|
|
@ -493,3 +493,36 @@ sites.form.buttons.save=Speichern
|
||||||
applications.types.singleton=Singleton
|
applications.types.singleton=Singleton
|
||||||
applications.number_of_instances={0} Instanzen
|
applications.number_of_instances={0} Instanzen
|
||||||
applications.number_of_instances_one={0} instance
|
applications.number_of_instances_one={0} instance
|
||||||
|
themes.label=Themes
|
||||||
|
themes.description=Themes verwalten
|
||||||
|
themes.table.headers.name=Name
|
||||||
|
themes.table.headers.title=Titel
|
||||||
|
themes.table.headers.version=Version
|
||||||
|
themes.table.headers.type=Typ
|
||||||
|
themes.table.headers.provider=Bereitgestellt durch
|
||||||
|
themes.table.actions.republish=Republizieren
|
||||||
|
themes.table.actions.publish=Publizieren
|
||||||
|
themes.table.actions.unpublish=Depublizieren
|
||||||
|
themes.table.actions.delete=L\u00f6schen
|
||||||
|
themes.versions.live=Live
|
||||||
|
themes.versions.draft=Draft
|
||||||
|
themes.table.headers.actions=Aktionen
|
||||||
|
themes.dialog.description.title=Beschreibung Theme {0}
|
||||||
|
themes.dialog.description.close=Schlie\u00dfen
|
||||||
|
themes.table.description.show=Beschreibung des Themes {0} anzeigen
|
||||||
|
themes.create_new_theme=Neues Theme anlegen
|
||||||
|
themes.dialog.new_theme.title=Neues Theme anlegen
|
||||||
|
themes.dialog.new_theme.close=Abbrechen
|
||||||
|
themes.dialog.new_theme.create=Neues Theme anlegen
|
||||||
|
themes.dialog.new_theme.name.help=Eindeutiger Name des neuen Themes
|
||||||
|
themes.dialog.new_theme.name.label=Name
|
||||||
|
themes.dialog.new_theme.provider.help=Der Provider, \u00fcber den das Theme verwaltet wird.
|
||||||
|
themes.dialog.new_theme.provider.label=Provider
|
||||||
|
themes.table.actions.unpublish.cancel=Abbrechen
|
||||||
|
themes.table.actions.unpublish.confirm=Theme depublizieren
|
||||||
|
themes.table.actions.unpublish.title=Depublizieren des Themes best\u00e4tigen
|
||||||
|
themes.table.actions.unpublish.message=Sind Sie sicher, dass Sie das Theme {0} depublizieren wollen?
|
||||||
|
themes.table.actions.delete.cancel=Abbrechen
|
||||||
|
themes.table.actions.delete.confirm=Theme l\u00f6schen
|
||||||
|
themes.table.actions.delete.title=L\u00f6schen des Themes best\u00e4tigen
|
||||||
|
themes.table.actions.delete.message=Sind Sie sicher, dass Sie das Theme {0} l\u00f6schen wollen?
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue