]> source.dussan.org Git - nextcloud-server.git/commitdiff
Migrating themes to Theming app
authorJohn Molakvoæ <skjnldsv@protonmail.com>
Fri, 15 Apr 2022 11:55:19 +0000 (13:55 +0200)
committerJohn Molakvoæ <skjnldsv@protonmail.com>
Thu, 21 Apr 2022 07:31:07 +0000 (09:31 +0200)
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
25 files changed:
apps/accessibility/css/style.scss [deleted file]
apps/theming/appinfo/info.xml
apps/theming/appinfo/routes.php
apps/theming/img/dark.jpg [new file with mode: 0644]
apps/theming/img/default.jpg [new file with mode: 0644]
apps/theming/img/highcontrast.jpg [new file with mode: 0644]
apps/theming/img/opendyslexic.jpg [new file with mode: 0644]
apps/theming/lib/Controller/UserThemeController.php [new file with mode: 0644]
apps/theming/lib/ITheme.php
apps/theming/lib/Service/ThemesService.php
apps/theming/lib/Settings/Admin.php
apps/theming/lib/Settings/AdminSection.php [new file with mode: 0644]
apps/theming/lib/Settings/Personal.php [new file with mode: 0644]
apps/theming/lib/Settings/PersonalSection.php [new file with mode: 0644]
apps/theming/lib/Settings/Section.php [deleted file]
apps/theming/lib/Themes/DarkHighContrastTheme.php
apps/theming/lib/Themes/DarkTheme.php
apps/theming/lib/Themes/DefaultTheme.php
apps/theming/lib/Themes/DyslexiaFont.php [new file with mode: 0644]
apps/theming/lib/Themes/HighContrastTheme.php
apps/theming/src/UserThemes.vue [new file with mode: 0644]
apps/theming/src/components/ItemPreview.vue [new file with mode: 0644]
apps/theming/src/settings.js [new file with mode: 0644]
apps/theming/templates/settings-personal.php [new file with mode: 0644]
webpack.modules.js

diff --git a/apps/accessibility/css/style.scss b/apps/accessibility/css/style.scss
deleted file mode 100644 (file)
index 9dd2e42..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-// Rules we could port to the rest of Nextcloud too
-
-// Proper highlight for links and focus feedback
-#accessibility a {
-       font-weight: bold;
-
-       &:hover,
-       &:focus {
-               text-decoration: underline;
-       }
-}
-
-// Highlight checkbox label in bold for focus feedback
-// Drawback: Text width increases a bit
-#accessibility .checkbox:focus + label {
-       font-weight: bold;
-}
-
-// Limit width of settings sections for readability
-#accessibility.section p {
-       max-width: 800px;
-}
-
-// End of rules we could port to rest of Nextcloud
-
-
-
-.preview-list {
-       display: flex;
-       flex-direction: column;
-       max-width: 800px;
-}
-
-.preview {
-       display: flex;
-       justify-content: flex-start;
-       margin-top: 3em;
-       position: relative;
-
-       &,
-       * {
-               user-select: none;
-       }
-
-       .preview-image {
-               flex-basis: 200px;
-               flex-shrink: 0;
-               margin-right: 1em;
-               background-position: top left;
-               background-size: cover;
-               background-repeat: no-repeat;
-               border-radius: var(--border-radius);
-       }
-
-       .preview-description {
-               display: flex;
-               flex-direction: column;
-
-               label {
-                       padding: 12px 0;
-               }
-       }
-}
-
-@media (max-width: ($breakpoint-mobile / 2)) {
-       .app-settings #accessibility .preview-list .preview {
-               display: unset;
-
-               .preview-image {
-                       height: 150px;
-               }
-       }
-}
index 3d7cabe721340aa3edbd209b0471039b790facd9..7b6e20a781f38c865b830fd6922e1c2acbe539e1 100644 (file)
 
        <settings>
                <admin>OCA\Theming\Settings\Admin</admin>
-               <admin-section>OCA\Theming\Settings\Section</admin-section>
+               <admin-section>OCA\Theming\Settings\AdminSection</admin-section>
+               <personal>OCA\Theming\Settings\Personal</personal>
+               <personal-section>OCA\Theming\Settings\PersonalSection</personal-section>
        </settings>
+
        <commands>
                <command>OCA\Theming\Command\UpdateConfig</command>
        </commands>
index 358f6a39ad4a517c85d228f5d34915ad44ce881f..c9a99a409ef09a55e7a4d297fc84b050fdbdee35 100644 (file)
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-return ['routes' => [
-       [
-               'name' => 'Theming#updateStylesheet',
-               'url' => '/ajax/updateStylesheet',
-               'verb' => 'POST'
+return [
+       'routes' => [
+               [
+                       'name' => 'Theming#updateStylesheet',
+                       'url' => '/ajax/updateStylesheet',
+                       'verb' => 'POST'
+               ],
+               [
+                       'name' => 'Theming#undo',
+                       'url' => '/ajax/undoChanges',
+                       'verb' => 'POST'
+               ],
+               [
+                       'name' => 'Theming#uploadImage',
+                       'url' => '/ajax/uploadImage',
+                       'verb' => 'POST'
+               ],
+               [
+                       'name' => 'Theming#getThemeVariables',
+                       'url' => '/theme/{themeId}.css',
+                       'verb' => 'GET',
+               ],
+               [
+                       'name' => 'Theming#getImage',
+                       'url' => '/image/{key}',
+                       'verb' => 'GET',
+               ],
+               [
+                       'name' => 'Theming#getManifest',
+                       'url' => '/manifest/{app}',
+                       'verb' => 'GET',
+                       'defaults' => ['app' => 'core']
+               ],
+               [
+                       'name' => 'Icon#getFavicon',
+                       'url' => '/favicon/{app}',
+                       'verb' => 'GET',
+                       'defaults' => ['app' => 'core'],
+               ],
+               [
+                       'name' => 'Icon#getTouchIcon',
+                       'url' => '/icon/{app}',
+                       'verb' => 'GET',
+                       'defaults' => ['app' => 'core'],
+               ],
+               [
+                       'name' => 'Icon#getThemedIcon',
+                       'url' => '/img/{app}/{image}',
+                       'verb' => 'GET',
+                       'requirements' => ['image' => '.+']
+               ],
        ],
-       [
-               'name' => 'Theming#undo',
-               'url' => '/ajax/undoChanges',
-               'verb' => 'POST'
-       ],
-       [
-               'name' => 'Theming#uploadImage',
-               'url' => '/ajax/uploadImage',
-               'verb' => 'POST'
-       ],
-       [
-               'name' => 'Theming#getThemeVariables',
-               'url' => '/theme/{themeId}.css',
-               'verb' => 'GET',
-       ],
-       [
-               'name' => 'Theming#getImage',
-               'url' => '/image/{key}',
-               'verb' => 'GET',
-       ],
-       [
-               'name' => 'Theming#getManifest',
-               'url' => '/manifest/{app}',
-               'verb' => 'GET',
-               'defaults' => ['app' => 'core']
-       ],
-       [
-               'name' => 'Icon#getFavicon',
-               'url' => '/favicon/{app}',
-               'verb' => 'GET',
-               'defaults' => ['app' => 'core'],
-       ],
-       [
-               'name' => 'Icon#getTouchIcon',
-               'url' => '/icon/{app}',
-               'verb' => 'GET',
-               'defaults' => ['app' => 'core'],
-       ],
-       [
-               'name' => 'Icon#getThemedIcon',
-               'url' => '/img/{app}/{image}',
-               'verb' => 'GET',
-               'requirements' => ['image' => '.+']
-       ],
-]];
+       'ocs' => [
+               [
+                       'name' => 'userTheme#enableTheme',
+                       'url' => '/api/v1/theme/{themeId}/enable',
+                       'verb' => 'PUT',
+               ],
+               [
+                       'name' => 'userTheme#disableTheme',
+                       'url' => '/api/v1/theme/{themeId}',
+                       'verb' => 'DELETE',
+               ],
+       ]
+];
diff --git a/apps/theming/img/dark.jpg b/apps/theming/img/dark.jpg
new file mode 100644 (file)
index 0000000..b207c39
Binary files /dev/null and b/apps/theming/img/dark.jpg differ
diff --git a/apps/theming/img/default.jpg b/apps/theming/img/default.jpg
new file mode 100644 (file)
index 0000000..ad3fafd
Binary files /dev/null and b/apps/theming/img/default.jpg differ
diff --git a/apps/theming/img/highcontrast.jpg b/apps/theming/img/highcontrast.jpg
new file mode 100644 (file)
index 0000000..8c55a73
Binary files /dev/null and b/apps/theming/img/highcontrast.jpg differ
diff --git a/apps/theming/img/opendyslexic.jpg b/apps/theming/img/opendyslexic.jpg
new file mode 100644 (file)
index 0000000..db8e60f
Binary files /dev/null and b/apps/theming/img/opendyslexic.jpg differ
diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php
new file mode 100644 (file)
index 0000000..ec379d2
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2018 John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
+ * @copyright Copyright (c) 2019 Janis Köhr <janiskoehr@icloud.com>
+ *
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Daniel Kesselberg <mail@danielkesselberg.de>
+ * @author Janis Köhr <janis.koehr@novatec-gmbh.de>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Theming\Controller;
+
+use OCA\Theming\Service\ThemesService;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCSController;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IUserSession;
+use OCP\PreConditionNotMetException;
+
+class UserThemeController extends OCSController {
+
+       protected string $userId;
+       private IConfig $config;
+       private IUserSession $userSession;
+       private ThemesService $themesService;
+
+       /**
+        * Config constructor.
+        */
+       public function __construct(string $appName,
+                                                               IRequest $request,
+                                                               IConfig $config,
+                                                               IUserSession $userSession,
+                                                               ThemesService $themesService) {
+               parent::__construct($appName, $request);
+               $this->config = $config;
+               $this->userSession = $userSession;
+               $this->themesService = $themesService;
+               $this->userId = $userSession->getUser()->getUID();
+       }
+
+       /**
+        * @NoAdminRequired
+        *
+        * Enable theme
+        *
+        * @param string $themeId the theme ID
+        * @return DataResponse
+        * @throws OCSBadRequestException|PreConditionNotMetException
+        */
+       public function enableTheme(string $themeId): DataResponse {
+               if ($themeId === '' || !$themeId) {
+                       throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+               }
+
+               $themes = $this->themesService->getThemes();
+               if (!isset($themes[$themeId])) {
+                       throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+               }
+               
+               // Enable selected theme
+               $this->themesService->enableTheme($themes[$themeId]);
+               return new DataResponse();
+       }
+
+       /**
+        * @NoAdminRequired
+        *
+        * Disable theme
+        *
+        * @param string $themeId the theme ID
+        * @return DataResponse
+        * @throws OCSBadRequestException|PreConditionNotMetException
+        */
+       public function disableTheme(string $themeId): DataResponse {
+               if ($themeId === '' || !$themeId) {
+                       throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+               }
+
+               $themes = $this->themesService->getThemes();
+               if (!isset($themes[$themeId])) {
+                       throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+               }
+               
+               // Enable selected theme
+               $this->themesService->disableTheme($themes[$themeId]);
+               return new DataResponse();
+       }
+}
index 7f3e49075ca8efb68e5dafe8d12e977f8b977b9a..20508fac4e8a37d11f142ff3d525b9ceae39e6ff 100644 (file)
@@ -30,12 +30,46 @@ namespace OCA\Theming;
  */
 interface ITheme {
 
+       const TYPE_THEME = 1;
+       const TYPE_FONT = 2;
+
        /**
         * Unique theme id
+        * Will be used to search for ID.png in the img folder
+        *
         * @since 25.0.0
         */
        public function getId(): string;
 
+       /**
+        * Theme type
+        * TYPE_THEME or TYPE_FONT
+        *
+        * @since 25.0.0
+        */
+       public function getType(): int;
+
+       /**
+        * The theme translated title
+        *
+        * @since 25.0.0
+        */
+       public function getTitle(): string;
+
+       /**
+        * The theme enable checkbox translated label
+        *
+        * @since 25.0.0
+        */
+       public function getEnableLabel(): string;
+
+       /**
+        * The theme translated description
+        *
+        * @since 25.0.0
+        */
+       public function getDescription(): string;
+
        /**
         * Get the media query triggering this theme
         * Optional, ignored if falsy
index 832c443a2e12ced2e01b602536e03925751d5e89..8b39da6bb5d3a42695e7681c084c2a7295a3cfca 100644 (file)
 namespace OCA\Theming\Service;
 
 use OCA\Theming\AppInfo\Application;
-use OCA\Theming\Themes\DefaultTheme;
-use OCA\Theming\Themes\DarkTheme;
+use OCA\Theming\ITheme;
 use OCA\Theming\Themes\DarkHighContrastTheme;
+use OCA\Theming\Themes\DarkTheme;
+use OCA\Theming\Themes\DefaultTheme;
+use OCA\Theming\Themes\DyslexiaFont;
 use OCA\Theming\Themes\HighContrastTheme;
-use OCA\Theming\ITheme;
-use OCP\IAppConfig;
 use OCP\IConfig;
 use OCP\IUser;
 use OCP\IUserSession;
 
 class ThemesService {
-       private IUserSession $session;
+       private IUserSession $userSession;
        private IConfig $config;
 
        /** @var ITheme[] */
@@ -45,7 +45,8 @@ class ThemesService {
                                                                DefaultTheme $defaultTheme,
                                                                DarkTheme $darkTheme,
                                                                DarkHighContrastTheme $darkHighContrastTheme,
-                                                               HighContrastTheme $highContrastTheme) {
+                                                               HighContrastTheme $highContrastTheme,
+                                                               DyslexiaFont $dyslexiaFont) {
                $this->userSession = $userSession;
                $this->config = $config;
 
@@ -53,28 +54,57 @@ class ThemesService {
                $this->themesProviders = [
                        $defaultTheme->getId()                  => $defaultTheme,
                        $darkTheme->getId()                             => $darkTheme,
-                       $darkHighContrastTheme->getId() => $darkHighContrastTheme,
                        $highContrastTheme->getId()             => $highContrastTheme,
+                       $darkHighContrastTheme->getId() => $darkHighContrastTheme,
+                       $dyslexiaFont->getId()                  => $dyslexiaFont,
                ];
        }
 
+       /**
+        * Get the list of all registered themes
+        * 
+        * @return ITheme[]
+        */
        public function getThemes(): array {
                return $this->themesProviders;
        }
 
-       public function getThemeVariables(string $id): array {
-               return $this->themesProviders[$id]->getCSSVariables();
-       }
-
+       /**
+        * Enable a theme for the logged-in user
+        * 
+        * @param ITheme $theme the theme to enable
+        */
        public function enableTheme(ITheme $theme): void {
-               $themes = $this->getEnabledThemes();
-               array_push($themes, $theme->getId());
-               $this->setEnabledThemes($themes);
+               $themesIds = $this->getEnabledThemes();
+
+               /** @var ITheme[] */
+               $themes = array_map(function($themeId) {
+                       return $this->getThemes()[$themeId];
+               }, $themesIds);
+
+               // Filtering all themes with the same type
+               $filteredThemes = array_filter($themes, function($t) use ($theme) {
+                       return $theme->getType() === $t->getType();
+               });
+
+               // Disable all the other themes of the same type
+               // as there can only be one enabled at the same time
+               foreach ($filteredThemes as $t) {
+                       $this->disableTheme($t);
+               }
+
+               $this->setEnabledThemes([...$this->getEnabledThemes(), $theme->getId()]);
        }
 
+       /**
+        * Disable a theme for the logged-in user
+        * 
+        * @param ITheme $theme the theme to disable
+        */
        public function disableTheme(ITheme $theme): void {
                // Using keys as it's faster
                $themes = $this->getEnabledThemes();
+
                // If enabled, removing it
                if (in_array($theme->getId(), $themes)) {
                        $this->setEnabledThemes(array_filter($themes, function($themeId) use ($theme) {
@@ -83,6 +113,12 @@ class ThemesService {
                }
        }
 
+       /**
+        * Check whether a theme is enabled or not
+        * for the logged-in user
+        * 
+        * @return bool
+        */
        public function isEnabled(ITheme $theme): bool {
                $user = $this->userSession->getUser();
                if ($user instanceof IUser) {
@@ -92,12 +128,27 @@ class ThemesService {
                }
        }
 
+       /**
+        * Get the list of all enabled themes IDs
+        * for the logged-in user
+        * 
+        * @return string[]
+        */
        public function getEnabledThemes(): array {
                $user = $this->userSession->getUser();
-               $enabledThemes = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]');
-               return json_decode($enabledThemes);
+               try {
+                       return json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]'));
+               } catch (\Exception $e) {
+                       return [];
+               }
        }
 
+       /**
+        * Set the list of enabled themes 
+        * for the logged-in user
+        * 
+        * @param string[] $themes the list of enabled themes IDs
+        */
        private function setEnabledThemes(array $themes): void {
                $user = $this->userSession->getUser();
                $this->config->setUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', json_encode(array_unique($themes)));
index 045f0b3fe77dc1c342c90735886f1e7892cae18c..6caa174d99bf85130562ee0b0838b7e70488b2da 100644 (file)
@@ -36,22 +36,20 @@ use OCP\IURLGenerator;
 use OCP\Settings\IDelegatedSettings;
 
 class Admin implements IDelegatedSettings {
-       /** @var IConfig */
-       private $config;
-       /** @var IL10N */
-       private $l;
-       /** @var ThemingDefaults */
-       private $themingDefaults;
-       /** @var IURLGenerator */
-       private $urlGenerator;
-       /** @var ImageManager */
-       private $imageManager;
+       private string $appName;
+       private IConfig $config;
+       private IL10N $l;
+       private ThemingDefaults $themingDefaults;
+       private IURLGenerator $urlGenerator;
+       private ImageManager $imageManager;
 
-       public function __construct(IConfig $config,
+       public function __construct(string $appName,
+                                                               IConfig $config,
                                                                IL10N $l,
                                                                ThemingDefaults $themingDefaults,
                                                                IURLGenerator $urlGenerator,
                                                                ImageManager $imageManager) {
+               $this->appName = $appName;
                $this->config = $config;
                $this->l = $l;
                $this->themingDefaults = $themingDefaults;
@@ -86,14 +84,14 @@ class Admin implements IDelegatedSettings {
                        'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
                ];
 
-               return new TemplateResponse('theming', 'settings-admin', $parameters, '');
+               return new TemplateResponse($this->appName, 'settings-admin', $parameters, '');
        }
 
        /**
         * @return string the section ID, e.g. 'sharing'
         */
        public function getSection(): string {
-               return 'theming';
+               return $this->appName;
        }
 
        /**
@@ -113,7 +111,7 @@ class Admin implements IDelegatedSettings {
 
        public function getAuthorizedAppConfig(): array {
                return [
-                       'theming' => '/.*/',
+                       $this->appName => '/.*/',
                ];
        }
 }
diff --git a/apps/theming/lib/Settings/AdminSection.php b/apps/theming/lib/Settings/AdminSection.php
new file mode 100644 (file)
index 0000000..2fcc81a
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Theming\Settings;
+
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\Settings\IIconSection;
+
+class AdminSection implements IIconSection {
+       private string $appName;
+       private IL10N $l;
+       private IURLGenerator $url;
+
+       public function __construct(string $appName, IURLGenerator $url, IL10N $l) {
+               $this->appName = $appName;
+               $this->url = $url;
+               $this->l = $l;
+       }
+
+       /**
+        * returns the ID of the section. It is supposed to be a lower case string,
+        * e.g. 'ldap'
+        *
+        * @returns string
+        */
+       public function getID() {
+               return $this->appName;
+       }
+
+       /**
+        * returns the translated name as it should be displayed, e.g. 'LDAP / AD
+        * integration'. Use the L10N service to translate it.
+        *
+        * @return string
+        */
+       public function getName() {
+               return $this->l->t('Theming');
+       }
+
+       /**
+        * @return int whether the form should be rather on the top or bottom of
+        * the settings navigation. The sections are arranged in ascending order of
+        * the priority values. It is required to return a value between 0 and 99.
+        *
+        * E.g.: 70
+        */
+       public function getPriority() {
+               return 30;
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getIcon() {
+               return $this->url->imagePath($this->appName, 'app-dark.svg');
+       }
+}
diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php
new file mode 100644 (file)
index 0000000..6dd865b
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ * @copyright Copyright (c) 2019 Janis Köhr <janiskoehr@icloud.com>
+ *
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Theming\Settings;
+
+use OCA\Theming\Service\ThemesService;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\Settings\ISettings;
+use OCP\Util;
+
+class Personal implements ISettings {
+
+       protected string $appName;
+       private IConfig $config;
+       private IUserSession $userSession;
+       private ThemesService $themesService;
+       private IInitialState $initialStateService;
+
+       public function __construct(string $appName,
+                                                               IConfig $config,
+                                                               IUserSession $userSession,
+                                                               ThemesService $themesService,
+                                                               IInitialState $initialStateService) {
+               $this->appName = $appName;
+               $this->config = $config;
+               $this->userSession = $userSession;
+               $this->themesService = $themesService;
+               $this->initialStateService = $initialStateService;
+       }
+
+       public function getForm(): TemplateResponse {
+               $themes = array_map(function($theme) {
+                       return [
+                               'id' => $theme->getId(),
+                               'type' => $theme->getType(),
+                               'title' => $theme->getTitle(),
+                               'enableLabel' => $theme->getEnableLabel(),
+                               'description' => $theme->getDescription(),
+                               'enabled' => $this->themesService->isEnabled($theme),
+                       ];
+               }, $this->themesService->getThemes());
+
+               $this->initialStateService->provideInitialState('themes', array_values($themes));
+               Util::addScript($this->appName, 'theming-settings');
+
+               return new TemplateResponse($this->appName, 'settings-personal');
+       }
+
+       /**
+        * @return string the section ID, e.g. 'sharing'
+        * @since 9.1
+        */
+       public function getSection(): string {
+               return $this->appName;
+       }
+
+       /**
+        * @return int whether the form should be rather on the top or bottom of
+        * the admin section. The forms are arranged in ascending order of the
+        * priority values. It is required to return a value between 0 and 100.
+        *
+        * E.g.: 70
+        * @since 9.1
+        */
+       public function getPriority(): int {
+               return 40;
+       }
+}
diff --git a/apps/theming/lib/Settings/PersonalSection.php b/apps/theming/lib/Settings/PersonalSection.php
new file mode 100644 (file)
index 0000000..821708e
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+/**
+ * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Theming\Settings;
+
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\Settings\IIconSection;
+
+class PersonalSection implements IIconSection {
+
+       /** @var string */
+       protected $appName;
+
+       /** @var IURLGenerator */
+       private $urlGenerator;
+
+       /** @var IL10N */
+       private $l;
+
+       /**
+        * Personal Section constructor.
+        *
+        * @param string $appName
+        * @param IURLGenerator $urlGenerator
+        * @param IL10N $l
+        */
+       public function __construct(string $appName,
+                                                               IURLGenerator $urlGenerator,
+                                                               IL10N $l) {
+               $this->appName = $appName;
+               $this->urlGenerator = $urlGenerator;
+               $this->l = $l;
+       }
+
+       /**
+        * returns the relative path to an 16*16 icon describing the section.
+        * e.g. '/core/img/places/files.svg'
+        *
+        * @returns string
+        * @since 13.0.0
+        */
+       public function getIcon() {
+               return $this->urlGenerator->imagePath($this->appName, 'app-dark.svg');
+       }
+
+       /**
+        * returns the ID of the section. It is supposed to be a lower case string,
+        * e.g. 'ldap'
+        *
+        * @returns string
+        * @since 9.1
+        */
+       public function getID() {
+               return $this->appName;
+       }
+
+       /**
+        * returns the translated name as it should be displayed, e.g. 'LDAP / AD
+        * integration'. Use the L10N service to translate it.
+        *
+        * @return string
+        * @since 9.1
+        */
+       public function getName() {
+               return $this->l->t('Appearance and accessibility');
+       }
+
+       /**
+        * @return int whether the form should be rather on the top or bottom of
+        * the settings navigation. The sections are arranged in ascending order of
+        * the priority values. It is required to return a value between 0 and 99.
+        *
+        * E.g.: 70
+        * @since 9.1
+        */
+       public function getPriority() {
+               return 15;
+       }
+}
diff --git a/apps/theming/lib/Settings/Section.php b/apps/theming/lib/Settings/Section.php
deleted file mode 100644 (file)
index fe2cc92..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\Theming\Settings;
-
-use OCP\IL10N;
-use OCP\IURLGenerator;
-use OCP\Settings\IIconSection;
-
-class Section implements IIconSection {
-       /** @var IL10N */
-       private $l;
-       /** @var IURLGenerator */
-       private $url;
-
-       /**
-        * @param IURLGenerator $url
-        * @param IL10N $l
-        */
-       public function __construct(IURLGenerator $url, IL10N $l) {
-               $this->url = $url;
-               $this->l = $l;
-       }
-
-       /**
-        * returns the ID of the section. It is supposed to be a lower case string,
-        * e.g. 'ldap'
-        *
-        * @returns string
-        */
-       public function getID() {
-               return 'theming';
-       }
-
-       /**
-        * returns the translated name as it should be displayed, e.g. 'LDAP / AD
-        * integration'. Use the L10N service to translate it.
-        *
-        * @return string
-        */
-       public function getName() {
-               return $this->l->t('Theming');
-       }
-
-       /**
-        * @return int whether the form should be rather on the top or bottom of
-        * the settings navigation. The sections are arranged in ascending order of
-        * the priority values. It is required to return a value between 0 and 99.
-        *
-        * E.g.: 70
-        */
-       public function getPriority() {
-               return 30;
-       }
-
-       /**
-        * {@inheritdoc}
-        */
-       public function getIcon() {
-               return $this->url->imagePath('theming', 'app-dark.svg');
-       }
-}
index 1f00990c7de0cc28c20e65bd6c2b1f18c67498a4..8d0b134c75f22d9fcee1a4daeceba417a7e35fda 100644 (file)
@@ -36,6 +36,18 @@ class DarkHighContrastTheme extends HighContrastTheme implements ITheme {
                return '(prefers-color-scheme: dark) and (prefers-contrast: more)';
        }
 
+       public function getTitle(): string {
+               return $this->l->t('Dark theme with high contrast mode');
+       }
+
+       public function getEnableLabel(): string {
+               return $this->l->t('Enable dark high contrast mode');
+       }
+
+       public function getDescription(): string {
+               return $this->l->t('Similar to the high contrast mode, but with dark colours.');
+       }
+
        public function getCSSVariables(): array {
                $variables = parent::getCSSVariables();
 
index b7ec16aa56b4d52d9d8297b605b4bfa837cd0ddb..c00f8a7ea4d51da3de9255e553c9fd200fadfa40 100644 (file)
@@ -36,6 +36,18 @@ class DarkTheme extends DefaultTheme implements ITheme {
                return '(prefers-color-scheme: dark)';
        }
 
+       public function getTitle(): string {
+               return $this->l->t('Dark theme');
+       }
+
+       public function getEnableLabel(): string {
+               return $this->l->t('Enable dark theme');
+       }
+
+       public function getDescription(): string {
+               return $this->l->t('A dark theme to ease your eyes by reducing the overall luminosity and brightness. It is still under development, so please report any issues you may find.');
+       }
+
        public function getCSSVariables(): array {
                $defaultVariables = parent::getCSSVariables();
 
index 990b011bae94ec5045f73eefc7d359ebd5140a03..3b194a36546c86b71061bd41e56d4aa9caa516b1 100644 (file)
@@ -29,6 +29,7 @@ use OCA\Theming\ThemingDefaults;
 use OCA\Theming\Util;
 use OCA\Theming\ITheme;
 use OCP\IConfig;
+use OCP\IL10N;
 use OCP\IURLGenerator;
 
 class DefaultTheme implements ITheme {
@@ -37,6 +38,7 @@ class DefaultTheme implements ITheme {
        public IURLGenerator $urlGenerator;
        public ImageManager $imageManager;
        public IConfig $config;
+       public IL10N $l;
 
        public string $primaryColor;
 
@@ -44,12 +46,14 @@ class DefaultTheme implements ITheme {
                                                                ThemingDefaults $themingDefaults,
                                                                IURLGenerator $urlGenerator,
                                                                ImageManager $imageManager,
-                                                               IConfig $config) {
+                                                               IConfig $config,
+                                                               IL10N $l) {
                $this->util = $util;
                $this->themingDefaults = $themingDefaults;
                $this->urlGenerator = $urlGenerator;
                $this->imageManager = $imageManager;
                $this->config = $config;
+               $this->l = $l;
 
                $this->primaryColor = $this->themingDefaults->getColorPrimary();
        }
@@ -58,6 +62,22 @@ class DefaultTheme implements ITheme {
                return 'default';
        }
 
+       public function getType(): int {
+               return ITheme::TYPE_THEME;
+       }
+
+       public function getTitle(): string {
+               return $this->l->t('Light theme');
+       }
+
+       public function getEnableLabel(): string {
+               return $this->l->t('Enable the default light theme');
+       }
+
+       public function getDescription(): string {
+               return $this->l->t('The default light appearance.');
+       }
+
        public function getMediaQuery(): string {
                return '';
        }
diff --git a/apps/theming/lib/Themes/DyslexiaFont.php b/apps/theming/lib/Themes/DyslexiaFont.php
new file mode 100644 (file)
index 0000000..460147b
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Theming\Themes;
+
+use OCA\Theming\ITheme;
+
+class DyslexiaFont extends DefaultTheme implements ITheme {
+
+       public function getId(): string {
+               return 'opendyslexic';
+       }
+
+       public function getType(): int {
+               return ITheme::TYPE_FONT;
+       }
+
+       public function getTitle(): string {
+               return $this->l->t('Dyslexia font');
+       }
+
+       public function getEnableLabel(): string {
+               return $this->l->t('Enable dyslexia font');
+       }
+
+       public function getDescription(): string {
+               return $this->l->t('OpenDyslexic is a free typeface/font designed to mitigate some of the common reading errors caused by dyslexia.');
+       }
+
+       public function getCSSVariables(): array {
+               $variables = parent::getCSSVariables();
+               $originalFontFace = $variables['--font-face'];
+
+               $variables = [
+                       '--font-face' => 'OpenDyslexic, ' . $originalFontFace
+               ];
+
+               return $variables;
+       }
+}
+
+// @font-face {
+//     font-family: 'OpenDyslexic';
+//     font-style: normal;
+//     font-weight: 400;
+//     src: url('../fonts/OpenDyslexic-Regular.woff') format('woff');
+// }
+
+// @font-face {
+//     font-family: 'OpenDyslexic';
+//     font-style: normal;
+//     font-weight: 700;
+//     src: url('../fonts/OpenDyslexic-Bold.woff') format('woff');
+// }
index cae7cc5be98c5eee62c9b41e1958d08c12e7eeab..67276e4ef00c200424b0e6900d51af356673c07f 100644 (file)
@@ -36,6 +36,18 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
                return '(prefers-contrast: more)';
        }
 
+       public function getTitle(): string {
+               return $this->l->t('High contrast mode');
+       }
+
+       public function getEnableLabel(): string {
+               return $this->l->t('Enable high contrast mode');
+       }
+
+       public function getDescription(): string {
+               return $this->l->t('A high contrast mode to ease your navigation. Visual quality will be reduced but clarity will be increased.');
+       }
+
        public function getCSSVariables(): array {
                $variables = parent::getCSSVariables();
 
diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserThemes.vue
new file mode 100644 (file)
index 0000000..7811502
--- /dev/null
@@ -0,0 +1,175 @@
+<template>
+       <SettingsSection class="theming" :title="t('themes', 'Appaerance and accessibility')">
+               <p v-html="description" />
+               <p v-html="descriptionDetail" />
+
+               <div class="theming__preview-list">
+                       <ItemPreview v-for="theme in themes"
+                               :key="theme.id"
+                               :theme="theme"
+                               :selected="selectedTheme.id === theme.id"
+                               :themes="themes"
+                               type="theme"
+                               @change="changeTheme" />
+                       <ItemPreview v-for="theme in fonts"
+                               :key="theme.id"
+                               :theme="theme"
+                               :selected="theme.enabled"
+                               :themes="fonts"
+                               type="font"
+                               @change="changeFont" />
+               </div>
+       </SettingsSection>
+</template>
+
+<script>
+import { generateOcsUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import axios from '@nextcloud/axios'
+import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
+
+import ItemPreview from './components/ItemPreview'
+
+const availableThemes = loadState('theming', 'themes', [])
+
+console.debug('Available themes', availableThemes)
+
+export default {
+       name: 'UserThemes',
+       components: {
+               ItemPreview,
+               SettingsSection,
+       },
+
+       data() {
+               return {
+                       availableThemes,
+               }
+       },
+
+       computed: {
+               themes() {
+                       return this.availableThemes.filter(theme => theme.type === 1)
+               },
+               fonts() {
+                       return this.availableThemes.filter(theme => theme.type === 2)
+               },
+
+               // Selected theme, fallback on first (default) if none
+               selectedTheme() {
+                       return this.themes.find(theme => theme.enabled === true) || this.themes[0]
+               },
+
+               description() {
+                       // using the `t` replace method escape html, we have to do it manually :/
+                       return t(
+                               'theming',
+                               'Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level.'
+                       )
+                               .replace('{guidelines}', this.guidelinesLink)
+                               .replace('{linkend}', '</a>')
+               },
+               guidelinesLink() {
+                       return '<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">'
+               },
+               descriptionDetail() {
+                       return t(
+                               'theming',
+                               'If you find any issues, don’t hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!'
+                       )
+                               .replace('{issuetracker}', this.issuetrackerLink)
+                               .replace('{designteam}', this.designteamLink)
+                               .replace(/\{linkend\}/g, '</a>')
+               },
+               issuetrackerLink() {
+                       return '<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">'
+               },
+               designteamLink() {
+                       return '<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">'
+               },
+       },
+       methods: {
+               changeTheme({ enabled, id }) {
+                       // Reset selected and select new one
+                       this.themes.forEach(theme => {
+                               if (theme.id === id && enabled) {
+                                       theme.enabled = true
+                                       document.body.setAttribute(`data-theme-${theme.id}`, true)
+                                       return
+                               }
+                               theme.enabled = false
+                               document.body.removeAttribute(`data-theme-${theme.id}`)
+                       })
+
+                       this.selectItem(enabled, id)
+               },
+               changeFont({ enabled, id }) {
+                       // Reset selected and select new one
+                       this.fonts.forEach(font => {
+                               if (font.id === id && enabled) {
+                                       font.enabled = true
+                                       document.body.setAttribute(`data-theme-${font.id}`, true)
+                                       return
+                               }
+                               font.enabled = false
+                               document.body.removeAttribute(`data-theme-${font.id}`)
+                       })
+
+                       this.selectItem(enabled, id)
+               },
+
+               /**
+                * Commit a change and force reload css
+                * Fetching the file again will trigger the server update
+                *
+                * @param {boolean} enabled the theme state
+                * @param {string} themeId the theme ID to change
+                */
+               async selectItem(enabled, themeId) {
+                       try {
+                               if (enabled) {
+                                       await axios({
+                                               url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}/enable', { themeId }),
+                                               method: 'PUT',
+                                       })
+                               } else {
+                                       await axios({
+                                               url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}', { themeId }),
+                                               method: 'DELETE',
+                                       })
+                               }
+
+                       } catch (err) {
+                               console.error(err, err.response)
+                               OC.Notification.showTemporary(t('theming', err.response.data.ocs.meta.message + '. Unable to apply the setting.'))
+                       }
+               },
+       },
+}
+</script>
+<style lang="scss" scoped>
+
+.theming {
+       // Limit width of settings sections for readability
+       p {
+               max-width: 800px;
+       }
+
+       // Proper highlight for links and focus feedback
+       &::v-deep a {
+               font-weight: bold;
+
+               &:hover,
+               &:focus {
+                       text-decoration: underline;
+               }
+       }
+
+       &__preview-list {
+               display: flex;
+               flex-direction: column;
+               max-width: 800px;
+       }
+}
+
+</style>
diff --git a/apps/theming/src/components/ItemPreview.vue b/apps/theming/src/components/ItemPreview.vue
new file mode 100644 (file)
index 0000000..997d66a
--- /dev/null
@@ -0,0 +1,121 @@
+<template>
+       <div class="theming__preview">
+               <div class="theming__preview-image" :style="{ backgroundImage: 'url(' + img + ')' }" />
+               <div class="theming__preview-description">
+                       <h3>{{ theme.title }}</h3>
+                       <p>{{ theme.description }}</p>
+                       <CheckboxRadioSwitch class="theming__preview-toggle"
+                               :checked.sync="checked"
+                               :name="name"
+                               :type="switchType">
+                               {{ theme.enableLabel }}
+                       </CheckboxRadioSwitch>
+               </div>
+       </div>
+</template>
+
+<script>
+import { generateFilePath} from '@nextcloud/router'
+import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
+
+export default {
+       name: 'ItemPreview',
+       components: {
+               CheckboxRadioSwitch,
+       },
+       props: {
+               theme: {
+                       type: Object,
+                       required: true,
+               },
+               selected: {
+                       type: Boolean,
+                       default: false,
+               },
+               type: {
+                       type: String,
+                       default: '',
+               },
+               themes: {
+                       type: Array,
+                       default: () => [],
+               },
+       },
+       computed: {
+               switchType() {
+                       return this.themes.length === 1 ? 'switch' : 'radio'
+               },
+
+               name() {
+                       return this.switchType === 'radio' ? this.type : null
+               },
+
+               img() {
+                       return generateFilePath('theming', 'img', this.theme.id + '.jpg')
+               },
+
+               checked: {
+                       get() {
+                               return this.selected
+                       },
+                       set(checked) {
+                               console.debug('Selecting theme', this.theme, checked)
+
+                               // If this is a radio, we can only enable
+                               if (this.switchType === 'radio') {
+                                       this.$emit('change', { enabled: true, id: this.theme.id })
+                                       return
+                               }
+
+                               // If this is a switch, we can disable the theme
+                               this.$emit('change', { enabled: checked === true, id: this.theme.id })
+                       },
+               },
+       },
+}
+</script>
+<style lang="scss" scoped>
+
+.theming__preview {
+       position: relative;
+       display: flex;
+       justify-content: flex-start;
+       height: 140px;
+       margin-top: 3em;
+
+       &,
+       * {
+               user-select: none;
+       }
+
+       &-image {
+               flex-basis: 200px;
+               flex-shrink: 0;
+               margin-right: 30px;
+               border-radius: var(--border-radius);
+               background-repeat: no-repeat;
+               background-position: top left;
+               background-size: cover;
+       }
+
+       &-description {
+               display: flex;
+               flex-direction: column;
+
+               label {
+                       padding: 12px 0;
+               }
+       }
+}
+
+@media (max-width: (1024 / 2)) {
+       .theming__preview {
+               display: unset;
+
+               &-image {
+                       height: 150px;
+               }
+       }
+}
+
+</style>
diff --git a/apps/theming/src/settings.js b/apps/theming/src/settings.js
new file mode 100644 (file)
index 0000000..94ae6fd
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import App from './UserThemes.vue'
+
+// bind to window
+Vue.prototype.OC = OC
+Vue.prototype.t = t
+
+const View = Vue.extend(App)
+const accessibility = new View()
+accessibility.$mount('#theming')
diff --git a/apps/theming/templates/settings-personal.php b/apps/theming/templates/settings-personal.php
new file mode 100644 (file)
index 0000000..4ba1aa4
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+?>
+
+<span id="theming"></span>
\ No newline at end of file
index 593c201a08dffbddec2f6b51a4ea25af7021a80d..90c4a2b550cca51fb8ebb05a559c13d207a748d0 100644 (file)
@@ -87,6 +87,9 @@ module.exports = {
        systemtags: {
                systemtags: path.join(__dirname, 'apps/systemtags/src', 'systemtags.js'),
        },
+       theming: {
+               'theming-settings': path.join(__dirname, 'apps/theming/src', 'settings.js'),
+       },
        twofactor_backupcodes: {
                settings: path.join(__dirname, 'apps/twofactor_backupcodes/src', 'settings.js'),
        },