diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..d1b9430 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,41 @@ +pipeline: + dependencies: + image: deblan/devenv + commands: + - npm install + when: + event: [tag, push, pull_request] + + build: + image: deblan/devenv + commands: + - make npm-build + when: + event: [push, pull_request] + + package: + image: deblan/devenv + volumes: + - /var/www/html/artifacts:/var/www/html/artifacts + secrets: [app_certificate] + commands: + - mkdir -p "$HOME/.nextcloud/certificates" + - echo "$APP_CERTIFICATE" > "$HOME/.nextcloud/certificates/side_menu.key" + - export VERSION=$(grep "" appinfo/info.xml | grep -o "[0-9]*\.[0-9]*\.[0-9]*" --color=never) + - export RELEASE_DIRECTORY="/var/www/html/artifacts/deblan/side_menu" + - make release + when: + event: [tag] + + release: + image: plugins/gitea-release + volumes: + - /var/www/html/artifacts:/var/www/html/artifacts + settings: + api_key: + from_secret: gitnet_api_key + base_url: https://gitnet.fr + note: ${CI_COMMIT_MESSAGE} + files: /var/www/html/artifacts/deblan/side_menu/${CI_COMMIT_TAG/v//}/* + when: + event: [tag] diff --git a/CHANGELOG.md b/CHANGELOG.md index b0fc110..c473f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,108 @@ ## [Unreleased] +## 2.4.2 +### Fixed +* fix typo +### Changed +* change ci/cd + +## 2.4.1 +### Fixed +* fix user setting save + +## 2.4.0 +### Added +* remove focus on opener after click +* add button to set default colors +* add menu hover effect +* add translations +### Fixed +* fix deprecated app.php file +* fix menu with categories header +* fix minor issues +### Changed +* change saving progression +### Removed +* Nextcloud 19 is not supported anymore +* PHP 7.3 is not supported anymore + +## 2.3.5 +### Fixed +* fix white square (#99) + +## 2.3.4 +### Fixed +* fix blank line when settings are open (#96) + +## 2.3.3 +### Added +* hide the scrollbar when mouse is out (menu always displayed) +### Fixed +* fix SQL Exception InvalidFieldNameException (#93) + +## 2.3.2 +### Fixed +- fix hidden menu + +## 2.3.1 +### Fixed +- fix #88: does not work with default menu + +## 2.3.0 +### Added +- fix #82: add an option to keep visible an app in both menus +- fix #83: add custom categories +- add auto-reload when settings are saved + +## 2.2.0 +### Added +- fix #84: update icons +- fix #85: use Nextcloud colors by default + +### Fixed +- fix categories order in large menu + +## 2.1.0 +### Added +- add compatibility with Nextcloud 23 + +## 2.0.1 +### Fixed +- fix #78: Top menu is broken - invisible apps are shown +- fix #77: Update personal settings - HTTP error 412 (Precondition Failed) +- fix js error on the personal settings page (undefined sortable) + +## 2.0.0 +### Fixed +- fix #66: removing usage of setInterval +- fix #73: icon background +### Changed +- fix #67: replace jQuery with Vanilla JS +### Removed +- Nextcloud 18 is not supported anymore + +## 1.28.0 +### Added +- fix #63: add a new side menu with categories + +## 1.27.2 +### Fixed +- fix #62: hide app notification icon + +## 1.27.1 +### Fixed +- fix German translation render + +## 1.27.0 +### Added +- hide personal settings access when settings are forced by the administrator +### Fixed +- improve German translations + +## 1.26.0 +### Added +- add Czech translation + ## 1.25.2 ### Fixed - fix CHANGELOG diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 7de95c4..a734e49 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,1027 +1,5 @@ +# Contributor Code of Conduct +This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NCoC/CODE_OF_CONDUCT.md at master · domgetter/NCoC · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Skip to content - - - - - - - - -
- -
- - - - - -
- - - -
- - - - - - - - - -
-
-
- - - - - - - - - - - - -
- -
- -
-

- - - / - - NCoC - - -

- - -
- - - -
- - -
- - -
-
- - - - - - - Permalink - - - - - -
- -
-
- - - master - - - - -
- - - -
-
-
- -
- - - - Go to file - - -
- - -
- -
- - - -
- -
-
-
 
-
- -
-
 
- Cannot retrieve contributors at this time -
-
- - - - - - -
- -
-
- - 5 lines (3 sloc) - - 258 Bytes -
- -
- -
- Raw - Blame -
- - -
-
- - -
-

Contributor Code of Conduct

-

This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters.

-

For more information please visit the No Code of Conduct homepage.

-
-
- -
- - - - -
- - -
- - -
-
- - - - -
-
- -
-
- -
- - - - - - -
- - - You can’t perform that action at this time. -
- - - - - - - - - - - - - +For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage. diff --git a/Makefile b/Makefile index 09d3f22..4c8cdd4 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,14 @@ release: npm-build translations exit 1 fi - test -d releases/$$VERSION && rm -fr releases/$$VERSION - mkdir -p releases/$$VERSION/side_menu - cp -r README.md CHANGELOG.md appinfo css lib img l10n js src templates screenshots releases/$$VERSION/side_menu - cd releases/$$VERSION + if [ -z "$$RELEASE_DIRECTORY" ]; then + RELEASE_DIRECTORY=releases + fi + + test -d $$RELEASE_DIRECTORY/$$VERSION && rm -fr $$RELEASE_DIRECTORY/$$VERSION + mkdir -p $$RELEASE_DIRECTORY/$$VERSION/side_menu + cp -r README.md CHANGELOG.md appinfo css lib img l10n js src templates screenshots vendor $$RELEASE_DIRECTORY/$$VERSION/side_menu + cd $$RELEASE_DIRECTORY/$$VERSION zip -r side_menu_v$$VERSION.zip side_menu tar cvzf side_menu_v$$VERSION.tar.gz side_menu rm -fr side_menu diff --git a/README.md b/README.md index a1af4d9..92f05f7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ =============================== Allows you to modify the position of the main menu by creating a panel on the left of the interface or with a big menu on the top. -You can also define apps that must be displayed in the top menu. Fully customisable. +You can also add and sort custom categories, define apps that must be displayed in the top menu, etc. Fully customisable. This application is rather suitable for instances that activate a lot of applications. @@ -10,14 +10,17 @@ You can customize colors depending of the theme (Dark theme and Breeze Dark). Co * [Installation and upgrade](#installation-and-upgrade) * [How to contribute?](#how-to-contribute) +* [Support](#support) * [Screenshots](https://gitnet.fr/deblan/side_menu/src/branch/master/screenshots/) You like this app and you want to support me? ☕ [Buy me a coffee](https://www.buymeacoffee.com/deblan) or [Donate with liberapay](https://liberapay.com/deblan) +[![Build Status](https://ci.gitnet.fr/api/badges/deblan/side_menu/status.svg)](https://ci.gitnet.fr/deblan/side_menu) + Requirements ------------ -* PHP >= 7.3 +* PHP >= 7.4 * App `theming` enabled Installation and upgrade @@ -58,3 +61,8 @@ If you are a developer: Build javascripts using `make npm-build` (or `make npm-watch` to build them in real time). Then commit and create a pull request. + +Support +------- + +You can join the official room on Matrix: [#custommenu:neutralnetwork.org](https://matrix.to/#/#custommenu:neutralnetwork.org). diff --git a/appinfo/app.php b/appinfo/app.php deleted file mode 100644 index 7c48ca1..0000000 --- a/appinfo/app.php +++ /dev/null @@ -1,10 +0,0 @@ -isEnabled()) { - $app->registerAssets(); - $app->registerServices(); -} diff --git a/appinfo/info.xml b/appinfo/info.xml index a9c31f2..1517df1 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -17,7 +17,7 @@ You can report a bug or request a feature by opening an issue. Requirements: -* PHP >= 7.3 +* PHP >= 7.4 * App `theming` enabled If you like this application and if you want to support the development: @@ -26,7 +26,7 @@ If you like this application and if you want to support the development: * [Donate with liberapay](https://liberapay.com/deblan) * [Leave a comment](https://apps.nextcloud.com/apps/side_menu#comments) ]]> - 1.25.2 + 2.4.2 agpl Simon Vieille SideMenu @@ -46,8 +46,8 @@ If you like this application and if you want to support the development: https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc20_big_menu_responsive.png https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/personal_settings.png - - + + OCA\SideMenu\Settings\Admin diff --git a/css/admin.css b/css/admin.css index 41d9ec1..b35455a 100644 --- a/css/admin.css +++ b/css/admin.css @@ -20,7 +20,7 @@ margin: 10px 0 10px 0; } -#side-menu-section input[type="checkbox"] { +#-dropside-menu-section input[type="checkbox"] { vertical-align: middle; } @@ -81,6 +81,12 @@ cursor: pointer; } +.side-menu-setting-list-drop { + background: yellow; + border-color: yellow; + height: 34px; +} + .side-menu-setting.arrow { color: #ccc; padding-right: 5px; @@ -91,6 +97,10 @@ margin-top: -1px; } +#apps-categories-custom-list select { + width: 100%; +} + .side-menu-setting-table { display: table; @@ -109,7 +119,7 @@ .side-menu-setting-form { display: table-cell; - width: 300px; + min-width: 300px; } .side-menu-setting-label-short { @@ -119,3 +129,18 @@ .side-menu-setting-form-long { width: 400px; } + +#side-menu-save-progress { + display: inline-block; + width: 0; + height: 15px; + background: #fff; +} + +.btn-reset { + display: inline-block; + cursor: pointer; + position: absolute; + margin-top: 17px; + margin-left: 5px; +} diff --git a/css/sideMenu.css b/css/sideMenu.css index 466c678..56bf97f 100644 --- a/css/sideMenu.css +++ b/css/sideMenu.css @@ -29,12 +29,16 @@ display: none; } +#side-menu a { + transition: 0.2s; +} + #side-menu.open { display: block; } #header .side-menu-opener { - margin-left: 5px; + margin-left: 0px; } .side-menu-settings { @@ -59,6 +63,8 @@ .side-menu-settings img { vertical-align: bottom; margin-left: 3px; + width: 32px; + height: 32px; } #side-menu.open .side-menu-settings { @@ -67,10 +73,19 @@ .side-menu-opener { background: var(--side-menu-opener, url('../img/side-menu-opener.svg')); - height: 40px; - width: 40px; - border-radius: 0; - border: 0; + background-color: transparent !important; + height: 40px !important; + width: 40px !important; + border-radius: 0 !important; + border: 0 !important; + padding-right: 12px !important; + padding-left: 12px !important; + margin-left: 5px !important; + margin-left: 3px !important; +} + +.side-menu-opener:active, .side-menu-opener:focus { + background-color: var(--side-menu-current-app-background-color, #444) !important; } .side-menu-closer { @@ -105,6 +120,10 @@ margin-top: -3px; } +.side-menu-app-icon .app-icon-notification { + display: none; +} + .side-menu-app a { line-height: 30px; color: var(--side-menu-text-color, #fff); @@ -135,10 +154,13 @@ max-width: 250px; position: fixed; padding-top: 2px; - padding-left: 5px; top: 0; } +#side-menu.side-menu-with-categories .side-menu-header { + max-width: 295px; +} + #side-menu.hide-opener .side-menu-logo { margin-top: 20px; } @@ -158,23 +180,23 @@ transition-property: width; } -#side-menu.side-menu-big { +#side-menu.side-menu-big, #side-menu.side-menu-with-categories { max-width: 100%; height: auto; } -.side-menu-big .side-menu-header { +.side-menu-big .side-menu-header, .side-menu-with-categories .side-menu-header { height: auto; } -.side-menu-big .side-menu-apps-list { +.side-menu-big .side-menu-apps-list, .side-menu-with-categories .side-menu-apps-list { height: auto; position: static; max-width: 100vw; overflow: auto; } -.side-menu-big .side-menu-app a { +.side-menu-big .side-menu-app a, .side-menu-with-categories .side-menu-app a { padding: 7px 0 7px 7px; } @@ -213,7 +235,7 @@ stroke: var(--side-menu-text-color, #fff); } -.side-menu-big .side-menu-app-icon { +.side-menu-with-categories .side-menu-app-icon, .side-menu-big .side-menu-app-icon { vertical-align: middle; margin-top: -2px; } @@ -235,6 +257,11 @@ .side-menu-always-displayed .side-menu-apps-list { height: calc(100vh - 49px); top: 49px; + overflow: hidden; +} + +.side-menu-always-displayed .side-menu-apps-list:hover { + overflow: auto; } .side-menu-always-displayed #side-menu, @@ -267,6 +294,24 @@ transform: translateX(calc(-100% + 50px)) !important; } +#side-menu.side-menu-with-categories { + max-width: 290px; + height: 100vh; +} + +.side-menu-with-categories .side-menu-categories { + display: block; + padding: 0; +} + +.side-menu-with-categories .side-menu-category { + padding: 10px 0; +} + +.side-menu-always-displayed #body-settings, #body-settings.body-settings-side-menu { + overflow-x: visible; +} + @media screen and (max-width: 1024px) { #side-menu.side-menu-big { max-width: 290px; diff --git a/img/admin/layout-side-menu-with-categories.svg b/img/admin/layout-side-menu-with-categories.svg new file mode 100644 index 0000000..4c8c76d --- /dev/null +++ b/img/admin/layout-side-menu-with-categories.svg @@ -0,0 +1,224 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/admin/layout-side-with-categories.svg b/img/admin/layout-side-with-categories.svg new file mode 100644 index 0000000..739d5b2 --- /dev/null +++ b/img/admin/layout-side-with-categories.svg @@ -0,0 +1,223 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/side-menu-opener-closer.svg b/img/side-menu-opener-closer.svg index 3774c6d..0aa965f 100644 --- a/img/side-menu-opener-closer.svg +++ b/img/side-menu-opener-closer.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener-dark.svg b/img/side-menu-opener-dark.svg index 7b56e7a..c6a026c 100644 --- a/img/side-menu-opener-dark.svg +++ b/img/side-menu-opener-dark.svg @@ -1,173 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener-hamburger-2-dark.svg b/img/side-menu-opener-hamburger-2-dark.svg index 505ddee..504e037 100644 --- a/img/side-menu-opener-hamburger-2-dark.svg +++ b/img/side-menu-opener-hamburger-2-dark.svg @@ -1,102 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener-hamburger-2.svg b/img/side-menu-opener-hamburger-2.svg index b5eb64c..e7034ae 100644 --- a/img/side-menu-opener-hamburger-2.svg +++ b/img/side-menu-opener-hamburger-2.svg @@ -1,101 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener-hamburger-dark.svg b/img/side-menu-opener-hamburger-dark.svg index 5d60f54..3585049 100644 --- a/img/side-menu-opener-hamburger-dark.svg +++ b/img/side-menu-opener-hamburger-dark.svg @@ -1,92 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener-hamburger.svg b/img/side-menu-opener-hamburger.svg index c1b1325..09ae9db 100644 --- a/img/side-menu-opener-hamburger.svg +++ b/img/side-menu-opener-hamburger.svg @@ -1,91 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - - + \ No newline at end of file diff --git a/img/side-menu-opener.svg b/img/side-menu-opener.svg index 32abf78..6294ecb 100644 --- a/img/side-menu-opener.svg +++ b/img/side-menu-opener.svg @@ -1,172 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 0558e9a..f7f5e7e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,6 +9,9 @@ use OCA\SideMenu\Service\AppRepository; use OCA\SideMenu\Service\CategoryRepository; use OCA\SideMenu\Service\ConfigProxy; use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\IUserSession; use OCP\Util; use Psr\Container\ContainerInterface; @@ -18,7 +21,7 @@ use Psr\Container\ContainerInterface; * * @author Simon Vieille */ -class Application extends App +class Application extends App implements IBootstrap { public const APP_ID = 'side_menu'; @@ -44,16 +47,9 @@ class Application extends App public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); - - $this->config = OC::$server->getConfig(); - $this->cspnm = OC::$server->getContentSecurityPolicyNonceManager(); - $this->user = OC::$server[IUserSession::class]->getUser(); } - /** - * Checks if this app is enabled. - */ - public function isEnabled(): bool + protected function isEnabled(): bool { $enabled = true; $isForced = (bool) $this->config->getAppValue(self::APP_ID, 'force', '0'); @@ -74,64 +70,65 @@ class Application extends App return $enabled; } - /** - * Registes services. - */ - public function registerServices() - { - $container = $this->getContainer(); - - $container->registerService('AppRepository', function (ContainerInterface $c) { - return new AppRepository(); - }); - - $container->registerService('CategoryRepository', function (ContainerInterface $c) { - return new CategoryRepository(); - }); - - $container->registerService('ConfigProxy', function (ContainerInterface $c) { - return new ConfigProxy(); - }); - } - - /** - * Registers assets. - */ - public function registerAssets() + protected function addAssets() { Util::addScript(self::APP_ID, 'sideMenu'); Util::addStyle(self::APP_ID, 'sideMenu'); - $stylesheet = OC::$server->getURLGenerator()->linkToRoute( - 'side_menu.Css.stylesheet', - [ - 'v' => $this->config->getAppValue(self::APP_ID, 'cache', '0'), - ] - ); - - $script = OC::$server->getURLGenerator()->linkToRoute( - 'side_menu.Js.script', - [ - 'v' => $this->config->getAppValue(self::APP_ID, 'cache', '0'), - ] - ); - - Util::addHeader( - 'link', - [ - 'href' => $stylesheet, - 'rel' => 'stylesheet', + $assets = [ + 'stylesheet' => [ + 'route' => 'side_menu.Css.stylesheet', + 'type' => 'link', + 'route_attr' => 'href', + 'attr' => [ + 'rel' => 'stylesheet', + ], ], - '' - ); - - Util::addHeader( - 'script', - [ - 'src' => $script, - 'nonce' => $this->cspnm->getNonce(), + 'script' => [ + 'route' => 'side_menu.Js.script', + 'type' => 'script', + 'route_attr' => 'src', + 'attr' => [ + 'nonce' => $this->cspnm->getNonce(), + ], ], - '' - ); + ]; + + $cache = $this->config->getAppValue(self::APP_ID, 'cache', '0'); + + foreach ($assets as $key => $value) { + $route = OC::$server->getURLGenerator()->linkToRoute($value['route'], ['v' => $cache]); + $value['attr'][$value['route_attr']] = $route; + + Util::addHeader($value['type'], $value['attr'], ''); + } + } + + public function register(IRegistrationContext $context): void + { + $context->registerService('AppRepository', function (ContainerInterface $c) { + return new AppRepository(); + }); + + $context->registerService('CategoryRepository', function (ContainerInterface $c) { + return new CategoryRepository(); + }); + + $context->registerService('ConfigProxy', function (ContainerInterface $c) { + return new ConfigProxy(); + }); + } + + public function boot(IBootContext $context): void + { + $this->config = OC::$server->getConfig(); + $this->cspnm = OC::$server->getContentSecurityPolicyNonceManager(); + $this->user = OC::$server[IUserSession::class]->getUser(); + + if (!$this->isEnabled()) { + return; + } + + $this->addAssets(); } } diff --git a/lib/Controller/CssController.php b/lib/Controller/CssController.php index cdbf11a..028ac11 100644 --- a/lib/Controller/CssController.php +++ b/lib/Controller/CssController.php @@ -27,6 +27,8 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; use OCP\IUserSession; +use OCA\Theming\ThemingDefaults; +use OCA\SideMenu\Service\Color; class CssController extends Controller { @@ -40,12 +42,30 @@ class CssController extends Controller */ protected $user; - public function __construct(string $appName, IRequest $request, ConfigProxy $config) + /** + * @var ThemingDefaults + */ + protected $theming; + + /** + * @var Color + */ + protected $color; + + public function __construct( + string $appName, + IRequest $request, + ConfigProxy $config, + ThemingDefaults $theming, + Color $color + ) { parent::__construct($appName, $request); $this->user = OC::$server[IUserSession::class]->getUser(); $this->config = $config; + $this->theming = $theming; + $this->color = $color; } /** @@ -67,6 +87,7 @@ class CssController extends Controller { $isForced = $this->config->getAppValueBool('force', '0'); $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); + $topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]'); $isAccessibilityAppEnabled = $this->config->getAppValueBool('enabled', '0', 'accessibility'); $isBreezeDarkAppEnabled = $this->config->getAppValueBool('enabled', '0', 'breezedark'); @@ -74,11 +95,16 @@ class CssController extends Controller if ($this->user) { $userTopMenuApps = $this->config->getUserValueArray($this->user, 'top-menu-apps', '[]'); + $userTopSideMenuApps = $this->config->getUserValueArray($this->user, 'top-side-menu-apps', '[]'); if (!empty($userTopMenuApps) && !$isForced) { $topMenuApps = $userTopMenuApps; } + if (!empty($userTopSideMenuApps) && !$isForced) { + $topSideMenuApps = $userTopSideMenuApps; + } + $isDarkThemeUserEnabled = $this->config->getUserValue($this->user, 'theme', '', 'accessibility') === 'dark'; $isBreezeDarkUserEnabled = $this->config->getUserValue($this->user, 'theme_enabled', '', 'breezedark'); @@ -90,23 +116,29 @@ class CssController extends Controller $isDarkMode = ($isAccessibilityAppEnabled && $isDarkThemeUserEnabled) || ($isBreezeDarkAppEnabled && $isBreezeDarkUserEnabled); + $primaryColor = $this->theming->getColorPrimary(); + $lightenPrimaryColor = $this->color->adjustBrightness($primaryColor, 0.2); + $darkenPrimaryColor = $this->color->adjustBrightness($primaryColor, -0.2); + $darkenPrimaryColor2 = $this->color->adjustBrightness($primaryColor, -0.3); + $textColor = $this->theming->getTextColorPrimary(); + if ($isDarkMode) { - $backgroundColor = $this->config->getAppValue('dark-mode-background-color', '#333333'); - $backgroundColorTo = $this->config->getAppValue('dark-mode-background-color-to', $backgroundColor); - $currentAppBackgroundColor = $this->config->getAppValue('dark-mode-current-app-background-color', '#444444'); - $loaderColor = $this->config->getAppValue('dark-mode-loader-color', '#cccccc'); - $textColor = $this->config->getAppValue('dark-mode-text-color', '#FFFFFF'); + $backgroundColor = $this->config->getAppValue('dark-mode-background-color', $darkenPrimaryColor); + $backgroundColorTo = $this->config->getAppValue('dark-mode-background-color-to', $darkenPrimaryColor); + $currentAppBackgroundColor = $this->config->getAppValue('dark-mode-current-app-background-color', $darkenPrimaryColor2); + $loaderColor = $this->config->getAppValue('dark-mode-loader-color', $lightenPrimaryColor); + $textColor = $this->config->getAppValue('dark-mode-text-color', $textColor); $iconInvertFilter = abs($this->config->getAppValueInt('dark-mode-icon-invert-filter', '0')).'%'; $iconOpacity = abs($this->config->getAppValueInt('dark-mode-icon-opacity', '100') / 100); $opener = $this->config->getAppValue('dark-mode-opener', 'side-menu-opener'); $backgroundOpacity = dechex($this->config->getAppValueInt('dark-mode-background-color-opacity', '100') * 255 / 100); } else { - $backgroundColor = $this->config->getAppValue('background-color', '#333333'); - $backgroundColorTo = $this->config->getAppValue('background-color-to', $backgroundColor); - $currentAppBackgroundColor = $this->config->getAppValue('current-app-background-color', '#444444'); - $loaderColor = $this->config->getAppValue('loader-color', '#0e75ac'); - $textColor = $this->config->getAppValue('text-color', '#FFFFFF'); + $backgroundColor = $this->config->getAppValue('background-color', $darkenPrimaryColor); + $backgroundColorTo = $this->config->getAppValue('background-color-to', $darkenPrimaryColor); + $currentAppBackgroundColor = $this->config->getAppValue('current-app-background-color', $darkenPrimaryColor2); + $loaderColor = $this->config->getAppValue('loader-color', $lightenPrimaryColor); + $textColor = $this->config->getAppValue('text-color', $textColor); $iconInvertFilter = abs($this->config->getAppValueInt('icon-invert-filter', '0')).'%'; $iconOpacity = abs($this->config->getAppValueInt('icon-opacity', '100') / 100); $opener = $this->config->getAppValue('opener', 'side-menu-opener'); @@ -136,6 +168,7 @@ class CssController extends Controller 'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'), 'big-menu' => $this->config->getAppValueBool('big-menu', '0'), 'top-menu-apps' => $topMenuApps, + 'top-side-menu-apps' => $topSideMenuApps, ]; } } diff --git a/lib/Controller/JsController.php b/lib/Controller/JsController.php index b5ed845..c873826 100644 --- a/lib/Controller/JsController.php +++ b/lib/Controller/JsController.php @@ -94,6 +94,7 @@ class JsController extends Controller protected function getConfig(): array { $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); + $topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]'); $targetBlankApps = $this->config->getAppValueArray('target-blank-apps', '[]'); $useAvatar = $this->config->getAppValueBool('use-avatar', '0'); $isForced = $this->config->getAppValueBool('force', '0'); @@ -103,11 +104,16 @@ class JsController extends Controller if ($this->user) { $userTopMenuApps = $this->config->getUserValueArray($this->user, 'top-menu-apps', '[]'); + $userTopSideMenuApps = $this->config->getUserValueArray($this->user, 'top-side-menu-apps', '[]'); if (!empty($userTopMenuApps) && !$isForced) { $topMenuApps = $userTopMenuApps; } + if (!empty($userTopSideMenuApps) && !$isForced) { + $topSideMenuApps = $userTopSideMenuApps; + } + $userTargetBlankMode = $this->config->getUserValueInt($this->user, 'target-blank-mode', '1'); $userTargetBlankApps = $this->config->getUserValueArray($this->user, 'target-blank-apps', '[]'); @@ -152,10 +158,12 @@ class JsController extends Controller 'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'), 'loader-enabled' => $this->config->getAppValueBool('loader-enabled', '1'), 'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'), + 'side-with-categories' => $this->config->getAppValueBool('side-with-categories', '0'), 'big-menu' => $this->config->getAppValueBool('big-menu', '0'), 'big-menu-hidden-apps' => $this->config->getAppValueArray('big-menu-hidden-apps', '[]'), 'avatar' => $avatar, 'top-menu-apps' => $topMenuApps, + 'top-side-menu-apps' => $topSideMenuApps, 'target-blank-apps' => $targetBlankApps, 'settings' => $settings, 'logo' => $this->themingDefaults->getLogo(), diff --git a/lib/Controller/NavController.php b/lib/Controller/NavController.php index 4f6625e..41aff0c 100644 --- a/lib/Controller/NavController.php +++ b/lib/Controller/NavController.php @@ -187,9 +187,18 @@ class NavController extends Controller usort($items, function ($a, $b) use ($categoriesLabels) { foreach ($categoriesLabels as $key => $value) { + if ($a['categoryId'] === 'other') { + return -1; + } + + if ($b['categoryId'] === 'other') { + return 1; + } + if ($a['categoryId'] === $key) { return -1; } + if ($b['categoryId'] === $key) { return 1; } diff --git a/lib/Controller/PersonalSettingController.php b/lib/Controller/PersonalSettingController.php index 99bc65a..bdc9fca 100644 --- a/lib/Controller/PersonalSettingController.php +++ b/lib/Controller/PersonalSettingController.php @@ -54,6 +54,7 @@ class PersonalSettingController extends Controller /** * @NoAdminRequired + * @NoCSRFRequired * * @param mixed $name * @param mixed $value @@ -96,7 +97,7 @@ class PersonalSettingController extends Controller } } - if ('top-menu-apps' === $name) { + if (in_array($name, ['top-menu-apps', 'top-side-menu-apps'])) { $doSave = true; $data = json_decode($value, true); diff --git a/lib/Service/AppRepository.php b/lib/Service/AppRepository.php index 93c8f9c..d91ec75 100644 --- a/lib/Service/AppRepository.php +++ b/lib/Service/AppRepository.php @@ -21,10 +21,27 @@ class AppRepository */ protected $l10nFactory; - public function __construct(\OC_App $ocApp, IFactory $l10nFactory) + /** + * @var ConfigProxy + */ + protected $config; + + /** + * @var CategoryRepository + */ + protected $categoryRepository; + + public function __construct( + \OC_App $ocApp, + IFactory $l10nFactory, + ConfigProxy $config, + CategoryRepository $categoryRepository + ) { $this->ocApp = $ocApp; $this->l10nFactory = $l10nFactory; + $this->config = $config; + $this->categoryRepository = $categoryRepository; } /** @@ -35,6 +52,9 @@ class AppRepository public function getVisibleApps() { $navigation = $this->ocApp->getNavigation(); + $appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]'); + $categoriesCustom = $this->config->getAppValueArray('categories-custom', '[]'); + $categories = $this->categoryRepository->getOrderedCategories(); $apps = $this->ocApp->listAllApps(); $visibleApps = []; @@ -74,6 +94,12 @@ class AppRepository } } + foreach ($visibleApps as $id => $app) { + if (isset($appCategoriesCustom[$id], $categories[$appCategoriesCustom[$id]])) { + $visibleApps[$id]['category'] = [$appCategoriesCustom[$id]]; + } + } + usort($visibleApps, function ($a, $b) { return ($a['name'] < $b['name']) ? -1 : 1; }); diff --git a/lib/Service/CategoryRepository.php b/lib/Service/CategoryRepository.php index 3e67e63..7559854 100644 --- a/lib/Service/CategoryRepository.php +++ b/lib/Service/CategoryRepository.php @@ -5,6 +5,7 @@ namespace OCA\SideMenu\Service; use OC\App\AppStore\Fetcher\CategoryFetcher; use OCA\SideMenu\AppInfo\Application; use OCP\IConfig; +use OCP\IUserSession; use OCP\L10N\IFactory; /** @@ -34,16 +35,23 @@ class CategoryRepository */ protected $iConfig; + /** + * @var IUserSession + */ + protected $userSession; + public function __construct( CategoryFetcher $categoryFetcher, ConfigProxy $config, IConfig $iConfig, - IFactory $l10nFactory + IFactory $l10nFactory, + IUserSession $userSession ) { $this->categoryFetcher = $categoryFetcher; $this->l10nFactory = $l10nFactory; $this->config = $config; $this->iConfig = $iConfig; + $this->userSession = $userSession; } /** @@ -56,8 +64,8 @@ class CategoryRepository $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); $type = $this->config->getAppValue('categories-order-type', 'default'); $order = $this->config->getAppValueArray('categories-order', '[]'); - $categoriesLabels = $this->config->getAppValueArray('cache-categories', '[]'); + $customCategories = $this->config->getAppValueArray('categories-custom', '[]'); if (empty($categoriesLabels)) { $categoriesLabels = $this->categoryFetcher->get(); @@ -74,6 +82,18 @@ class CategoryRepository $categoriesLabels['external_links'] = $this->l10nFactory->get('external')->t('External sites'); $categoriesLabels['other'] = ''; + $user = $this->userSession->getUser(); + + if ($user) { + $lang = $this->iConfig->getUserValue($user->getUid(), 'core', 'lang'); + } else { + $lang = 'en'; + } + + foreach ($customCategories as $category) { + $categoriesLabels[$category['id']] = $category[$lang] ?? $category['en']; + } + asort($categoriesLabels); if ('custom' === $type) { diff --git a/lib/Service/Color.php b/lib/Service/Color.php new file mode 100644 index 0000000..cf90dd6 --- /dev/null +++ b/lib/Service/Color.php @@ -0,0 +1,34 @@ + + */ +class Color +{ + /** + * @thanks https://stackoverflow.com/posts/54393956/revision + */ + public function adjustBrightness(string $hexCode, float $adjustPercent): string + { + $hexCode = ltrim($hexCode, '#'); + + if (3 == strlen($hexCode)) { + $hexCode = $hexCode[0].$hexCode[0].$hexCode[1].$hexCode[1].$hexCode[2].$hexCode[2]; + } + + $hexCode = array_map('hexdec', str_split($hexCode, 2)); + + foreach ($hexCode as &$color) { + $adjustableLimit = $adjustPercent < 0 ? $color : 255 - $color; + $adjustAmount = ceil($adjustableLimit * $adjustPercent); + + $color = str_pad(dechex($color + $adjustAmount), 2, '0', STR_PAD_LEFT); + } + + return '#'.implode($hexCode); + } +} diff --git a/lib/Service/LangRepository.php b/lib/Service/LangRepository.php new file mode 100644 index 0000000..3c379f0 --- /dev/null +++ b/lib/Service/LangRepository.php @@ -0,0 +1,48 @@ + + */ +class LangRepository +{ + /** + * @var IDBConnection + */ + protected $db; + + public function __construct(IDBConnection $db) + { + $this->db = $db; + } + + public function getUsedLangs(): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select($qb->createFunction('DISTINCT configvalue')) + ->where('configkey=:configkey and appid=:appid and configvalue<>:configvalue') + ->setParameters([ + 'configkey' => 'lang', + 'appid' => 'core', + 'configvalue' => 'en', + ]) + ->from('preferences') + ; + + $stmt = $qb->execute(); + + $langs = ['en']; + + foreach ($stmt->fetchAll() as $result) { + $langs[] = $result['configvalue']; + } + + return $langs; + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index d77978c..a68d30c 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -26,6 +26,9 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; use OCP\ILogger; use OCP\Settings\ISettings; +use OCA\Theming\ThemingDefaults; +use OCA\SideMenu\Service\Color; +use OCA\SideMenu\Service\LangRepository; class Admin implements ISettings { @@ -54,18 +57,39 @@ class Admin implements ISettings */ private $categoryRepository; + /** + * @var ThemingDefaults + */ + protected $theming; + + /** + * @var Color + */ + protected $color; + + /** + * @var LangRepository + */ + protected $langRepository; + public function __construct( IL10N $l, ILogger $logger, ConfigProxy $config, AppRepository $appRepository, - CategoryRepository $categoryRepository + CategoryRepository $categoryRepository, + ThemingDefaults $theming, + Color $color, + LangRepository $langRepository ) { $this->l = $l; $this->logger = $logger; $this->config = $config; $this->appRepository = $appRepository; $this->categoryRepository = $categoryRepository; + $this->theming = $theming; + $this->color = $color; + $this->langRepository = $langRepository; } /** @@ -73,35 +97,54 @@ class Admin implements ISettings */ public function getForm() { - $backgroundColor = $this->config->getAppValue('background-color', '#333333'); - $backgroundColorTo = $this->config->getAppValue('background-color-to', $backgroundColor); + $primaryColor = $this->theming->getColorPrimary(); + $lightenPrimaryColor = $this->color->adjustBrightness($primaryColor, 0.2); + $darkenPrimaryColor = $this->color->adjustBrightness($primaryColor, -0.2); + $darkenPrimaryColor2 = $this->color->adjustBrightness($primaryColor, -0.3); + $textColor = $this->theming->getTextColorPrimary(); - $darkModeBackgroundColor = $this->config->getAppValue('dark-mode-background-color', '#333333'); - $darkModeBackgroundColorTo = $this->config->getAppValue('dark-mode-background-color-to', $darkModeBackgroundColor); + $backgroundColor = $this->config->getAppValue('background-color', $darkenPrimaryColor); + $backgroundColorTo = $this->config->getAppValue('background-color-to', $darkenPrimaryColor); + + $darkModeBackgroundColor = $this->config->getAppValue('dark-mode-background-color', $darkenPrimaryColor); + $darkModeBackgroundColorTo = $this->config->getAppValue('dark-mode-background-color-to', $darkenPrimaryColor); $parameters = [ + 'defaults' => [ + 'background-color' => $darkenPrimaryColor, + 'background-color-to' => $darkenPrimaryColor, + 'current-app-background-color' => $darkenPrimaryColor2, + 'text-color' => $textColor, + 'loader-color' => $lightenPrimaryColor, + 'dark-mode-background-color' => $darkenPrimaryColor, + 'dark-mode-background-color-to' => $darkenPrimaryColor, + 'dark-mode-current-app-background-color' => $darkenPrimaryColor2, + 'dark-mode-text-color' => $textColor, + 'dark-mode-loader-color' => $textColor, + ], 'background-color' => $backgroundColor, 'background-color-to' => $backgroundColorTo, 'background-color-opacity' => $this->config->getAppValueInt('background-color-opacity', '100'), - 'current-app-background-color' => $this->config->getAppValue('current-app-background-color', '#444444'), - 'loader-color' => $this->config->getAppValue('loader-color', '#0e75ac'), + 'current-app-background-color' => $this->config->getAppValue('current-app-background-color', $darkenPrimaryColor2), + 'loader-color' => $this->config->getAppValue('loader-color', $lightenPrimaryColor), 'icon-invert-filter' => $this->config->getAppValueInt('icon-invert-filter', '0'), 'icon-opacity' => $this->config->getAppValueInt('icon-opacity', '100'), - 'text-color' => $this->config->getAppValue('text-color', '#FFFFFF'), + 'text-color' => $this->config->getAppValue('text-color', $textColor), 'dark-mode-background-color' => $darkModeBackgroundColor, 'dark-mode-background-color-to' => $darkModeBackgroundColorTo, 'dark-mode-background-color-opacity' => $this->config->getAppValueInt('dark-mode-background-color-opacity', '100'), - 'dark-mode-current-app-background-color' => $this->config->getAppValue('dark-mode-current-app-background-color', '#444444'), - 'dark-mode-loader-color' => $this->config->getAppValue('dark-mode-loader-color', '#cccccc'), + 'dark-mode-current-app-background-color' => $this->config->getAppValue('dark-mode-current-app-background-color', $darkenPrimaryColor2), + 'dark-mode-loader-color' => $this->config->getAppValue('dark-mode-loader-color', $textColor), 'dark-mode-icon-invert-filter' => $this->config->getAppValueInt('dark-mode-icon-invert-filter', '0'), 'dark-mode-icon-opacity' => $this->config->getAppValueInt('dark-mode-icon-opacity', '100'), - 'dark-mode-text-color' => $this->config->getAppValue('dark-mode-text-color', '#FFFFFF'), + 'dark-mode-text-color' => $this->config->getAppValue('dark-mode-text-color', $textColor), 'dark-mode-opener' => $this->config->getAppValue('dark-mode-opener', 'side-menu-opener'), 'opener' => $this->config->getAppValue('opener', 'side-menu-opener'), 'loader-enabled' => $this->config->getAppValue('loader-enabled', '1'), 'cache' => $this->config->getAppValue('cache', '0'), 'always-displayed' => $this->config->getAppValue('always-displayed', '0'), 'big-menu' => $this->config->getAppValue('big-menu', '0'), + 'side-with-categories' => $this->config->getAppValue('side-with-categories', '0'), 'big-menu-hidden-apps' => $this->config->getAppValueArray('big-menu-hidden-apps', '[]'), 'display-logo' => $this->config->getAppValue('display-logo', '1'), 'add-logo-link' => $this->config->getAppValue('add-logo-link', '1'), @@ -116,11 +159,15 @@ class Admin implements ISettings 'force' => $this->config->getAppValue('force', '0'), 'target-blank-apps' => $this->config->getAppValueArray('target-blank-apps', '[]'), 'top-menu-apps' => $this->config->getAppValueArray('top-menu-apps', '[]'), + 'top-side-menu-apps' => $this->config->getAppValueArray('top-side-menu-apps', '[]'), 'default-enabled' => $this->config->getAppValue('default-enabled', '1'), + 'apps' => $this->appRepository->getVisibleApps(), + 'apps-categories-custom' => $this->config->getAppValueArray('apps-categories-custom', '[]'), 'categories-order-type' => $this->config->getAppValue('categories-order-type', 'default'), 'categories-order' => $this->config->getAppValueArray('categories-order', '[]'), - 'apps' => $this->appRepository->getVisibleApps(), + 'categories-custom' => $this->config->getAppValueArray('categories-custom', '[]'), 'categories' => $this->categoryRepository->getOrderedCategories(), + 'langs' => $this->langRepository->getUsedLangs(), ]; return new TemplateResponse(Application::APP_ID, 'settings/admin-form', $parameters, ''); diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 2b34bb2..9ac24c7 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -78,6 +78,7 @@ class Personal implements ISettings $this->config->getAppValue('default-enabled', '1') ), 'top-menu-apps' => $this->config->getUserValueArray($user, 'top-menu-apps', '[]'), + 'top-side-menu-apps' => $this->config->getUserValueArray($user, 'top-side-menu-apps', '[]'), 'target-blank-mode' => $this->config->getUserValue($user, 'target-blank-mode', '1'), 'target-blank-apps' => $this->config->getUserValueArray($user, 'target-blank-apps', '[]'), 'apps' => $this->appRepository->getVisibleApps(), diff --git a/lib/Settings/PersonalSection.php b/lib/Settings/PersonalSection.php index 101d95f..58a773f 100644 --- a/lib/Settings/PersonalSection.php +++ b/lib/Settings/PersonalSection.php @@ -19,6 +19,7 @@ namespace OCA\SideMenu\Settings; use OCA\SideMenu\AppInfo\Application; +use OCA\SideMenu\Service\ConfigProxy; use OCP\IL10N; use OCP\IURLGenerator; use OCP\Settings\IIconSection; @@ -35,10 +36,16 @@ class PersonalSection implements IIconSection */ private $url; - public function __construct(IURLGenerator $url, IL10N $l) + /** + * @var ConfigProxy + */ + private $configProxy; + + public function __construct(IURLGenerator $url, IL10N $l, ConfigProxy $configProxy) { $this->url = $url; $this->l = $l; + $this->configProxy = $configProxy; } /** @@ -49,6 +56,10 @@ class PersonalSection implements IIconSection */ public function getID() { + if ($this->configProxy->getAppValueBool('force', '0')) { + return ''; + } + return Application::APP_ID; } @@ -60,6 +71,10 @@ class PersonalSection implements IIconSection */ public function getName() { + if ($this->configProxy->getAppValueBool('force', '0')) { + return ''; + } + return $this->l->t(Application::APP_NAME); } @@ -72,6 +87,10 @@ class PersonalSection implements IIconSection */ public function getPriority() { + if ($this->configProxy->getAppValueBool('force', '0')) { + return null; + } + return 70; } diff --git a/package.json b/package.json index 38edace..cf982d8 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "stylelint:fix": "stylelint src --fix" }, "dependencies": { - "@nextcloud/axios": "^1.3.2", - "@nextcloud/vue": "^1.4.0", - "axios": "^0.19.2", + "@nextcloud/axios": "^1.8.0", + "@nextcloud/vue": "^1.5.0", + "axios": "^0.24.0", "trim": "0.0.1", "vue": "^2.6.11" }, @@ -21,7 +21,7 @@ "extends @nextcloud/browserslist-config" ], "engines": { - "node": ">=10.0.0" + "node": ">=16.0.0" }, "devDependencies": { "@babel/core": "^7.9.0", @@ -43,9 +43,9 @@ "eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^5.2.3", "file-loader": "^6.0.0", - "node-sass": "^4.13.1", "sass-loader": "^8.0.2", "stylelint": "^8.4.0", + "sass": "^1.49.9", "stylelint-config-recommended-scss": "^3.3.0", "stylelint-scss": "^3.16.0", "stylelint-webpack-plugin": "^0.10.5", diff --git a/screenshots/admin_settings.png b/screenshots/admin_settings.png index 23d69d9..ba7a744 100644 Binary files a/screenshots/admin_settings.png and b/screenshots/admin_settings.png differ diff --git a/screenshots/personal_settings.png b/screenshots/personal_settings.png index 6790db7..ba62403 100644 Binary files a/screenshots/personal_settings.png and b/screenshots/personal_settings.png differ diff --git a/src/AdminCategoriesCustom.vue b/src/AdminCategoriesCustom.vue new file mode 100644 index 0000000..909580d --- /dev/null +++ b/src/AdminCategoriesCustom.vue @@ -0,0 +1,182 @@ + + + + + + diff --git a/src/SideMenu.js b/src/SideMenu.js index 05d5e17..a9848c4 100644 --- a/src/SideMenu.js +++ b/src/SideMenu.js @@ -18,11 +18,9 @@ import Vue from 'vue' import SideMenu from './SideMenu.vue' import SideMenuBig from './SideMenuBig.vue' +import SideMenuWithCategories from './SideMenuWithCategories.vue' -// Vue.prototype.t = t Vue.prototype.OC = OC -// Vue.prototype.OC = OCP - const mountSideMenuComponent = () => { const sideMenuContainer = document.querySelector('#side-menu') @@ -32,6 +30,8 @@ const mountSideMenuComponent = () => { if (sideMenuContainer.getAttribute('data-bigmenu')) { component = SideMenuBig + } else if(sideMenuContainer.getAttribute('data-sidewithcategories')) { + component = SideMenuWithCategories } else { component = SideMenu } @@ -41,7 +41,7 @@ const mountSideMenuComponent = () => { sideMenu.$mount('#side-menu') - $('body').trigger('side-menu.ready') + document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.ready')) } else { window.setTimeout(mountSideMenuComponent, 50) } diff --git a/src/SideMenu.vue b/src/SideMenu.vue index 68fcccb..e47df87 100644 --- a/src/SideMenu.vue +++ b/src/SideMenu.vue @@ -100,7 +100,7 @@ export default { var dataId = parent.getAttribute('data-id') dataId = dataId !== null ? dataId : '' - if (!parent.classList.contains('app-hidden') && !menuIsHidden) { + if (!parent.classList.contains('app-top-side-menu') && !parent.classList.contains('app-hidden') && !menuIsHidden) { continue } @@ -122,13 +122,15 @@ export default { name: trim(element.querySelector('span').innerHTML), icon: svg, active: element.classList.contains('active') - }); + }) } } (function(apps) { window.setTimeout(function() { - jQuery('body').trigger('side-menu.apps', [apps]) + document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', { + detail: {apps: apps}, + })) }, 1000) })(this.apps) }, @@ -147,7 +149,7 @@ export default { that.logo = config['logo'] that.logoLink = config['logo-link'] that.settings = config['settings'] - }); + }) }, }, mounted() { diff --git a/src/SideMenuBig.vue b/src/SideMenuBig.vue index 9f6d8f2..72e6321 100644 --- a/src/SideMenuBig.vue +++ b/src/SideMenuBig.vue @@ -97,8 +97,10 @@ export default { } } - jQuery('body').trigger('side-menu.apps', [apps]) - }); + document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', { + detail: {apps: apps}, + })) + }) }, retrieveActiveApp() { @@ -116,7 +118,7 @@ export default { that.targetBlankApps = config['target-blank-apps'] that.settings = config['settings'] - }); + }) }, }, mounted() { diff --git a/src/SideMenuWithCategories.vue b/src/SideMenuWithCategories.vue new file mode 100644 index 0000000..7accbf0 --- /dev/null +++ b/src/SideMenuWithCategories.vue @@ -0,0 +1,128 @@ + + + + diff --git a/src/admin.js b/src/admin.js index 2a0a668..9231c9a 100644 --- a/src/admin.js +++ b/src/admin.js @@ -15,15 +15,32 @@ * along with this program. If not, see . */ +import AdminCategoriesCustom from './AdminCategoriesCustom.vue' +import Vue from 'vue' + +Vue.prototype.OC = window.OC +Vue.prototype.OCA = window.OCA + let elements = [] -const selector = '#side-menu-message'; +const selector = '#side-menu-message' const userConfig = (name, value, callbacks) => { const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet') + const formData = [] - $.post(url, {name: name, value: value}, callbacks.success) - .fail(callbacks.error) + formData.push('name=' + encodeURIComponent(name)) + formData.push('value=' + encodeURIComponent(value)) + + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData.join('&') + }) + .then(callbacks.success) + .catch(callbacks.error) } const appConfig = (name, value, callbacks) => { @@ -31,23 +48,29 @@ const appConfig = (name, value, callbacks) => { } const saveSettings = (key) => { - const element = elements.get(key) + const element = elements[key] + + if (!element) { + return + } + let value let name - if (jQuery(element).is('[data-checkbox]')) { - name = jQuery(element).attr('data-name') - const inputs = jQuery('input[name="' + name + '[]"]:checked') + if (element.hasAttribute('data-checkbox')) { + name = element.getAttribute('data-name') value = [] - inputs.each((i, v) => { - value.push(v.value) - }) + const inputs = document.querySelectorAll('input[name="' + name + '[]"]:checked') + + for (let input of inputs) { + value.push(input.value) + } value = JSON.stringify(value) } else { - name = jQuery(element).attr('name') - value = jQuery(element).val() + name = element.getAttribute('name') + value = element.value } const size = elements.length @@ -56,17 +79,21 @@ const saveSettings = (key) => { ++value } + const progress = document.querySelector('#side-menu-save-progress') + + progress.style.width = '40px'; + progress.style.marginLeft = '5px'; + const callbacks = { success: () => { - OC.msg.finishedSuccess( - selector, - t('side_menu', (key + 1) + '/' + size) - ) + const percent = parseInt((key + 1) * 100 / size); + + progress.setAttribute('value', percent) if (key < size - 1) { - saveSettings(++key) + saveSettings(key + 1) } else { - OC.msg.finishedSuccess(selector, t('side_menu', 'Saved')) + location.reload() } }, error: () => { @@ -74,7 +101,7 @@ const saveSettings = (key) => { } } - if (jQuery(element).is('[data-personal]')) { + if (element.hasAttribute('data-personal')) { userConfig(name, value, callbacks) } else { appConfig(name, value, callbacks) @@ -82,83 +109,152 @@ const saveSettings = (key) => { } const elementToggler = (element) => { - jQuery(element).toggle() + let display = 'none' + + if (window.getComputedStyle(element).display === 'none') { + display = 'block' + } + + element.style.display = display } -jQuery(document).ready(() => { - elements = jQuery('.side-menu-setting') +const updateAppsCategoriesCustom = () => { + let values = {} - jQuery('#side-menu-save').on('click', (event) => { + for (let item of document.querySelectorAll('.apps-categories-custom')) { + let app = item.getAttribute('data-app') + let value = item.value + + if (value) { + values[app] = value + } + } + + document.querySelector('#apps-categories-custom').value = JSON.stringify(values) +} + +document.addEventListener('DOMContentLoaded', () => { + $('*[data-toggle="tooltip"]').tooltip(); + + if (document.querySelector('#side-menu-categories-custom')) { + const View = Vue.extend(AdminCategoriesCustom) + const adminCategoriesCustom = new View({}) + + adminCategoriesCustom.$mount('#side-menu-categories-custom') + } + + elements = document.querySelectorAll('.side-menu-setting') + + document.querySelector('#side-menu-save').addEventListener('click', (event) => { event.preventDefault() OC.msg.startSaving(selector) saveSettings(0) - }); - - jQuery('.side-menu-display').on('click', (event) => { - var target = jQuery(event.target) - - jQuery('.side-menu-display').removeClass('is-active') - target.addClass('is-active') - - jQuery('#side-menu-always-displayed').val(target.attr('data-alwaysdiplayed')) - jQuery('#side-menu-big-menu').val(target.attr('data-bigmenu')) }) - jQuery('.side-menu-setting-live').on('change', (event) => { - var target = jQuery(event.target) - var name = target.attr('name') - var value = target.val() + const resets = document.querySelectorAll('.btn-reset') - if ('background-color-opacity' === name) { - return $('#side-menu-background-color, #side-menu-background-color-to').trigger('change'); - } else if ('dark-mode-background-color-opacity' === name) { - return $('#side-menu-dark-mode-background-color, #side-menu-dark-mode-background-color-to').trigger('change'); - } + for (let btn of resets) { + btn.addEventListener('click', (event) => { + const target = event.target + const values = JSON.parse(target.getAttribute('data-reset')) - if (name === 'opener') { - var url = OC.generateUrl(`/apps/side_menu/img/${value}.svg`).replace('/index.php', '') + for (let i in values) { + document.querySelector(`#${i}`).value = values[i] + } + }) + } - value = `url(${url})`; - } + const displays = document.querySelectorAll('.side-menu-display') - if (name === 'icon-invert-filter' || name === 'icon-opacity') { - value/=100; - } + for (let display of displays) { + display.addEventListener('click', (event) => { + const target = event.target - if (['dark-mode-background-color', 'dark-mode-background-color-to'].indexOf(name) > -1) { - var opacity = parseInt($('#side-menu-dark-mode-background-color-opacity').val() * 255 / 100); + for (let d of displays) { + d.classList.toggle('is-active', d === display) + } - value = [value, opacity.toString(16)].join(''); - } else if (['background-color', 'background-color-to'].indexOf(name) > -1) { - var opacity = parseInt($('#side-menu-background-color-opacity').val() * 255 / 100); + document.querySelector('#side-menu-always-displayed').value = target.getAttribute('data-alwaysdiplayed') + document.querySelector('#side-menu-big-menu').value = target.getAttribute('data-bigmenu') + document.querySelector('#side-menu-side-with-categories').value = target.getAttribute('data-sidewithcategories') + }) + } - value = [value, opacity.toString(16)].join(''); - } + for (let item of document.querySelectorAll('.apps-categories-custom')) { + item.addEventListener('change', (event) => { + updateAppsCategoriesCustom() + }) + } - document.documentElement.style.setProperty('--side-menu-' + name, value) + for (let item of document.querySelectorAll('.side-menu-setting-live')) { + item.addEventListener('change', (event) => { + const target = event.target + const name = target.getAttribute('name') + + let value = target.value + let id = null + + if (name === 'background-color-opacity') { + id = '#side-menu-background-color, #side-menu-background-color-to' + } else if (name === 'dark-mode-background-color-opacity') { + id = '#side-menu-dark-mode-background-color, #side-menu-dark-mode-background-color-to' + } + + if (id) { + document.querySelector(id).dispatchEvent(new CustomEvent('change')) + + return + } + + if (name === 'opener') { + const url = OC.generateUrl(`/apps/side_menu/img/${value}.svg`).replace('/index.php', '') + + value = `url(${url})` + } + + if (name === 'icon-invert-filter' || name === 'icon-opacity') { + value/=100 + } + + if (['dark-mode-background-color', 'dark-mode-background-color-to'].indexOf(name) > -1) { + const opacity = parseInt(document.querySelector('#side-menu-dark-mode-background-color-opacity').value * 255 / 100) + + value = [value, opacity.toString(16)].join('') + } else if (['background-color', 'background-color-to'].indexOf(name) > -1) { + const opacity = parseInt(document.querySelector('#side-menu-background-color-opacity').value * 255 / 100) + + value = [value, opacity.toString(16)].join('') + } + + document.documentElement.style.setProperty('--side-menu-' + name, value) + }) + } + + for (let toggler of document.querySelectorAll('.side-menu-toggler')) { + toggler.addEventListener('click', (event) => { + const target = event.target + const element = document.querySelector(target.getAttribute('data-target')) + + elementToggler(element) + }) + } + + sortable('#categories-list .side-menu-setting-list', { + placeholderClass: 'side-menu-setting-list-drop' }) - jQuery('.side-menu-toggler').on('click', (event) => { - var target = jQuery(event.target) - var element = target.attr('data-target') - - elementToggler(element) - }) - - jQuery("#categories-list .side-menu-setting-list").sortable({ - forcePlaceholderSize: true, - placeholder: 'placeholder', - stop: function (event, ui) { + try { + sortable('#categories-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => { let value = [] - jQuery('#categories-list .side-menu-setting-list-item').each(function() { - value.push(jQuery(this).attr('data-id')) - }); + for (let item of document.querySelectorAll('#categories-list .side-menu-setting-list-item')) { + console.log(item.getAttribute('data-id')) + value.push(item.getAttribute('data-id')) + } - value = JSON.stringify(value) - - jQuery('input[name="categories-order"]').val(value) - } - }); -}); + document.querySelector('input[name="categories-order"]').value = JSON.stringify(value) + }) + } catch (e) { + } +}) diff --git a/src/l10n/fixtures/cs.yaml b/src/l10n/fixtures/cs.yaml index a16b4ca..6958c09 100644 --- a/src/l10n/fixtures/cs.yaml +++ b/src/l10n/fixtures/cs.yaml @@ -40,7 +40,7 @@ "Panel": "Panel" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Otevřít nabídku při najetím ukazatelem na tlačítko nabídky (automaticky vypnuto pro dotykové obrazovky)." "Display the big menu": "Zobrazit velkou nabídku" -"The big menu is not compatible with AppOrder.": "Velká nabídka není kompatibilní s jinou aplikací (doplňkem) „Pořadí aplikací“." +"This menu is not compatible with AppOrder.": "Nabídka není kompatibilní s jinou aplikací (doplňkem) „Pořadí aplikací“." "Display the logo": "Zobrazit logo" "This feature is not compatible with the big menu display.": "Tato funkce není kompatibilní se zobrazením velké nabídky." "Icons and texts": "Ikony a texty" @@ -76,3 +76,18 @@ "Show and hide the list of categories": "Zobrazit/skrýt seznam kategorií" "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Tyto parametry jsou použity v případě, že je zapnutý (Breeze) tmavý motiv vzhledu." "Dark mode colors": "Barvy tmavého režimu" +"With categories": "S kategoriemi" +"Custom categories": "Vlastní kategorie" +"Customize application categories": "Personnaliser les catégories des applications" +"Customize application categories": "Přizpůsobte kategorie aplikací" +"Apps only visible in the top menu": "Aplikace jsou viditelné pouze v horní nabídce " +"Apps visible in the top and side menus": "Aplikace viditelné v horní a boční nabídce" +"Reset to default": "Vrátit zpět na výchozí hodnoty" +"Hidden icon": "Skrytá ikona" +"Small icon": "Malá ikona" +"Normal icon": "Normální ikona" +"Big icon": "Velká ikona" +"Hidden text": "Skrytý text" +"Small text": "Malý text" +"Normal text": "Normální text" +"Big text": "Velký text" diff --git a/src/l10n/fixtures/de.yaml b/src/l10n/fixtures/de.yaml index 254e5b7..afffdbb 100644 --- a/src/l10n/fixtures/de.yaml +++ b/src/l10n/fixtures/de.yaml @@ -1,31 +1,31 @@ "Custom menu": "Benutzerdefiniertes Menü" -"Enable the custom menu": "Aktiviere das Benutzerdefiniertes Menü" +"Enable the custom menu": "Benutzerdefiniertes Menü aktivieren" "No": "Nein" "Yes": "Ja" "Menu": "Menü" 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.': 'Verwende die Tastenkombination Strg+o, um das Seitenmenü ein- und auszublenden. Verwende tab zum Navigieren.' -"Top menu": "Hauptmenü" -"Apps that not must be moved in the side menu": "Apps, die nicht ins Seitenmenü verschoben werden müssen" +"Top menu": "Obere Navigationsleiste" +"Apps that not must be moved in the side menu": "Anwendungen, die nicht ins Seitenmenü verschoben werden sollen" "If there is no selection then the global configuration is applied.": "Wenn keine Auswahl vorhanden ist, wird die globale Konfiguration angewendet." "Experimental": "Experimentell" "Save": "Speichern" -"You like this app and you want to support me?": "Du magst diese App und möchtest mich unterstützen?" -"Buy me a coffee ☕": "Gib mir einen Kaffee ☕" -"Hidden": "Versteckt" +"You like this app and you want to support me?": "Du magst diese Anwendung und möchtest mich unterstützen?" +"Buy me a coffee ☕": "Gib mir einen Kaffee aus ☕" +"Hidden": "Ausblenden" "Small": "Klein" "Normal": "Normal" "Big": "Groß" "Colors": "Farben" "Background color": "Hintergrundfarbe" -"Background color of current app": "Hintergrundfarbe der aktuellen App" +"Background color of current app": "Hintergrundfarbe der aktuellen Anwendung" "Text color": "Textfarbe" "Loader": "Ladestandanzeige" "Icon": "Symbol" "Same color": "Selbe Farbe" "Opposite color": "Gegenfarbe" "Transparent": "Transparent" -"Opaque": "Undurchsichtig" -"Opener": "Öffner" +"Opaque": "Nicht transparent" +"Opener": "Menü-Symbol" "Default": "Standard" "Default (dark)": "Standard (dunkel)" "Hamburger": "Hamburger" @@ -35,44 +35,58 @@ "Before the logo": "Vor dem Logo" "After the logo": "Nach dem Logo" "Position": "Position" -"Show only the opener (hidden logo)": "Nur den Öffner anzeigen (verstecktes Logo)" -"Do not display the side menu and the opener if there is no application (eg: public pages).": "Zeige das Seitenmenü und den Öffner nicht an, wenn keine Anwendung vorhanden ist (z.B. bei öffentlichen Seiten)." +"Show only the opener (hidden logo)": "Nur das Menü-Symbol anzeigen (Logo wird ausgeblendet)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Zeige das Seitenmenü und das Menü-Symbol nicht an, wenn keine Anwendung vorhanden ist (z.B. bei öffentlichen Seiten)." "Panel": "Panel" -"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Öffne das Menü, wenn die Maus über den Öffner bewegt wird (auf Touchscreens automatisch deaktiviert)." +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Öffne das Menü, wenn die Maus über das Menü-Symbol bewegt wird (auf Touchscreens automatisch deaktiviert)." "Display the big menu": "Großes Menü anzeigen" -"The big menu is not compatible with AppOrder.": "Das große Menü ist nicht mit AppOrder kompatibel." +"This menu is not compatible with AppOrder.": "Dieses Menü ist nicht mit AppOrder kompatibel." "Display the logo": "Logo anzeigen" -"This feature is not compatible with the big menu display.": "Diese Funktion ist nicht mit großes Menü kompatibel." +"This feature is not compatible with the big menu display.": "Diese Funktion ist nicht mit dem großen Menü kompatibel." "Icons and texts": "Symbole und Texte" -"Loader enabled": "Loader aktiviert" +"Loader enabled": "Ladestandanzeige aktiviert" "Tips": "Tipps" -"Always displayed": "Wird immer angezeigt" +"Always displayed": "Immer anzeigen" "The logo will be hidden when the menu is always displayed.": "Das Logo wird ausgeblendet, wenn das Menü immer angezeigt wird." "This is the automatic behavior when the menu is always displayed.": "Dies ist das automatische Verhalten, wenn das Menü immer angezeigt wird." "Not compatible with touch screens.": "Nicht kompatibel mit Touchscreens." "Big menu": "Großes Menü" -"Live preview": "Live Vorschau" -"Open apps in new tab": "Öffne Apps in einem neuen Tab" +"Live preview": "Live-Vorschau" +"Open apps in new tab": "Öffne Anwendungen in einem neuen Tab" "Use the global setting": "Verwende die globale Einstellung" "Use my selection": "Verwende meine Auswahl" "Show and hide the list of applications": "Ein- und Ausblenden der Anwendungsliste" -"Use the avatar instead of the logo": "Verwenden Sie den Avatar anstelle des Logos" -"You do not have permission to change the settings.": "Sie haben keine Berechtigung zum Ändern der Einstellungen." -"Force this configuration to users": "Erzwingen Sie diese Konfiguration für Benutzer" -"Export the configuration": "Exportieren Sie die Konfiguration" -"Purge the cache": "Leeren Sie den Cache" -"Show the link to settings": "Zeigen Sie den Link zu den Einstellungen an" -"The menu is enabled by default for users": "Das Menü ist standardmäßig für Benutzer aktiviert" -"Except when the configuration is forced.": "Außer wenn die Konfiguration erzwungen wird." -"Apps that should not be displayed in the menu": "Apps, die nicht im Menü angezeigt werden sollen" -"This feature is only compatible with the big menu display.": "Kompatibel mit der Anzeige Großes Menü ." +"Use the avatar instead of the logo": "Avatar anstelle des Logos anzeigen" +"You do not have permission to change the settings.": "Du hast keine Berechtigung, die Einstellungen dieser Anwendung zu ändern." +"Force this configuration to users": "Konfiguration für alle Benutzer erzwingen" +"Export the configuration": "Konfiguration exportieren" +"Purge the cache": "Cache leeren" +"Show the link to settings": "Link zu den Einstellungen anzeigen" +"The menu is enabled by default for users": "Das Menü ist standardmäßig für alle Benutzer aktiviert" +"Except when the configuration is forced.": "Gilt nicht, wenn die Konfiguration erzwungen wird." +"Apps that should not be displayed in the menu": "Anwendungen, die nicht im Menü angezeigt werden sollen" +"This feature is only compatible with the big menu display.": "Kompatibel mit dem großen Menü." "The logo is a link to the default app": "Das Logo ist ein Link zur Standard-App" "Others": "Andere" "Categories": "Kategorien" "Customize sorting": "Sortierung anpassen" "Order by": "Sortieren nach" "Name": "Name" -"Customed": "Kundenspezifisch" +"Customed": "Benutzerdefiniert" "Show and hide the list of categories": "Liste der Kategorien ein- und ausblenden" -"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Diese Parameter werden verwendet, wenn Dark Theme oder Breeze Dark Theme aktiviert sind." -"Dark mode colors": "Dunkle Modusfarben" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Diese Optionen werden auf Dark Theme oder Breeze Dark Theme angewendet." +"Dark mode colors": "Farben für den dunklen Modus" +"With categories": "Mit Kategorien" +"Custom categories": "Benutzerdefinierte Kategorien" +"Customize application categories": "Anwendungskategorien anpassen" +"Apps only visible in the top menu": "Apps nur im oberen Menü sichtbar " +"Apps visible in the top and side menus": "Apps im oberen und seitlichen Menü sichtbar" +"Reset to default": "Auf Standard zurücksetzen" +"Hidden icon": "Verstecktes Symbol" +"Small icon": "Kleines Symbol" +"Normal icon": "Normales Symbol" +"Big icon": "Große Ikone" +"Hidden text": "Versteckter Text" +"Small text": "Kleiner Text" +"Normal text": "Normaler Text" +"Big text": "Großer Text" diff --git a/src/l10n/fixtures/fr.yaml b/src/l10n/fixtures/fr.yaml index 778a0cf..3d8fece 100644 --- a/src/l10n/fixtures/fr.yaml +++ b/src/l10n/fixtures/fr.yaml @@ -15,6 +15,14 @@ "Small": "Petit" "Normal": "Normal" "Big": "Gros" +"Hidden icon": "Icône masqué" +"Small icon": "Petit icône" +"Normal icon": "Icône normal" +"Big icon": "Gros icône" +"Hidden text": "Text masqué" +"Small text": "Texte petit" +"Normal text": "Texte normal" +"Big text": "Gros texte" "Colors": "Couleurs" "Background color": "Couleur de fond" "Background color of current app": "Couleur de fond de l'application en cours" @@ -40,7 +48,7 @@ "Panel": "Panneau" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Ouvrir le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)" "Display the big menu": "Afficher le menu large" -"The big menu is not compatible with AppOrder.": "Le menu large n'est pas compatible avec l'application AppOrder" +"This menu is not compatible with AppOrder.": "Ce menu n'est pas compatible avec l'application AppOrder" "Display the logo": "Afficher le logo" "This feature is not compatible with the big menu display.": "Cette fonctionnalité n'est pas compatible avec l'affichage du menu large." "Icons and texts": "Icônes et textes" @@ -76,3 +84,9 @@ "Show and hide the list of categories": "Afficher et masquer la liste des catégories" "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Ces paramètres sont utilisés lorsque le thème sombre ou le thème Breeze Dark sont activés." "Dark mode colors": "Couleurs du mode sombre" +"With categories": "Avec les catégories" +"Custom categories": "Catégories personnalisées" +"Customize application categories": "Personnaliser les catégories des applications" +"Apps only visible in the top menu": "Applications visibles uniquement dans le menu supérieur" +"Apps visible in the top and side menus": "Applications visibles dans le menus supérieur et latéral" +"Reset to default": "Restaurer les valeurs par défaut" diff --git a/src/l10n/fixtures/zh_CN.yaml b/src/l10n/fixtures/zh_CN.yaml index 9a43ff0..4e8cb36 100644 --- a/src/l10n/fixtures/zh_CN.yaml +++ b/src/l10n/fixtures/zh_CN.yaml @@ -40,7 +40,7 @@ "Panel": "面板" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)" "Display the big menu": "显示大型菜单" -"The big menu is not compatible with AppOrder.": "大型菜单与应用顺序不兼容" +"This menu is not compatible with AppOrder.": "型菜单与应用顺序不兼容" "Display the logo": "显示logo" "This feature is not compatible with the big menu<\/code> display.": "此功能与显示大型菜单<\/code>不兼容。" "Icons and texts": "图标与文字" @@ -76,3 +76,17 @@ "Show and hide the list of categories": "显示或隐藏类别列表" "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "此参数将应用于暗黑主题激活时。" "Dark mode colors": "暗黑模式颜色" +"With categories": "有类别" +"Custom categories": "自定义类别" +"Customize application categories": "自定义应用程序类别" +"Apps only visible in the top menu": "应用程序仅在顶部菜单中可见" +"Apps visible in the top and side menus": "顶部和侧边菜单中可见的应用程序" +"Reset to default": "重置为默认设置" +"Hidden icon": "隐藏图标" +"Small icon": "小图标" +"Normal icon": "正常图标" +"Big icon": "大图标" +"Hidden text": "隐藏文字" +"Small text": "小文本" +"Normal text": "普通文本" +"Big text": "大文本" diff --git a/templates/css/stylesheet.php b/templates/css/stylesheet.php index e8b6a7e..379fb42 100644 --- a/templates/css/stylesheet.php +++ b/templates/css/stylesheet.php @@ -8,7 +8,7 @@ } - + #appmenu { display: none; } diff --git a/templates/js/_alwaysDisplayed.js b/templates/js/_alwaysDisplayed.js index 2b58bc7..e37230a 100644 --- a/templates/js/_alwaysDisplayed.js +++ b/templates/js/_alwaysDisplayed.js @@ -1,21 +1,19 @@ -var alwaysDisplayed = function() { - var elements = document.querySelectorAll('*'); - var fixedElements = [] - - for (var i in elements) { - var element = elements[i] +const alwaysDisplayed = function() { + const elements = querySelectorAll('*') + const fixedElements = [] + for (var element of elements) { if (typeof element !== 'object') { continue } - var position = window.getComputedStyle(element, null).getPropertyValue('position'); + const position = window.getComputedStyle(element, null).getPropertyValue('position') if (position !== 'fixed') { continue } - var id = element.getAttribute('id') + const id = element.getAttribute('id') if (id === 'header' || id === 'side-menu' || id === 'side-menu-loader') { continue @@ -25,7 +23,21 @@ var alwaysDisplayed = function() { continue } - if (jQuery(element).parents('#side-menu').length) { + let elementIsInSideMenu = false + let parent = element.parentNode + + while (parent && !elementIsInSideMenu) { + try { + if (parent.getAttribute('id') === 'side-menu') { + elementIsInSideMenu = true + } + } catch (e) { + } + + parent = parent.parentNode + } + + if (elementIsInSideMenu) { continue } @@ -33,19 +45,19 @@ var alwaysDisplayed = function() { } for (var i in fixedElements) { - var element = fixedElements[i] - var computedStyle = window.getComputedStyle(element, null) - var left = computedStyle.getPropertyValue('left') - var right = computedStyle.getPropertyValue('right') + const element = fixedElements[i] + const computedStyle = window.getComputedStyle(element, null) + const left = computedStyle.getPropertyValue('left') + const right = computedStyle.getPropertyValue('right') if (right !== '0px') { - var intValue = parseInt(left.replace('px', '')) - element.style.setProperty('transform', 'translateX(' + (intValue + 50) + 'px)') + const intValue = parseInt(left.replace('px', '')) + 50 + element.style.setProperty('transform', 'translateX(' + intValue.toString() + 'px)') } } } -let content = document.getElementById('content') +const content = querySelector('#content') if (content && content.classList.contains('app-settings')) { let loaded = false @@ -56,7 +68,7 @@ if (content && content.classList.contains('app-settings')) { } const observer = new MutationObserver(() => { if (loaded) { - return; + return } const element = content.querySelector('#app-category-your-apps') || content.querySelector('#app-navigation ul') diff --git a/templates/js/_loaderEnabled.js b/templates/js/_loaderEnabled.js index b391e4b..1d5ff4e 100644 --- a/templates/js/_loaderEnabled.js +++ b/templates/js/_loaderEnabled.js @@ -1,15 +1,14 @@ -var pageLoader = jQuery('
') -var pageLoaderBar = jQuery('
') +let pageLoader = createElement('div', {id: 'side-menu-loader'}) +let pageLoaderBar = createElement('div', {id: 'side-menu-loader-bar'}) -body.append(pageLoader) -pageLoader.append(pageLoaderBar) +pageLoader.appendChild(pageLoaderBar) +querySelector('body').appendChild(pageLoader) -var pageLoaderValue = 0 - -$(window).on('beforeunload', function() { - setInterval(function() { - pageLoaderBar.width(pageLoaderValue.toString() + '%') +let pageLoaderValue = 0 +window.addEventListener('beforeunload', () => { + setInterval(() => { + pageLoaderBar.style.width = pageLoaderValue.toString() + '%' pageLoaderValue = Math.min(pageLoaderValue + .2, 100) }, 25) }) diff --git a/templates/js/_topMenuApps.js b/templates/js/_topMenuApps.js index 57b7088..103a453 100644 --- a/templates/js/_topMenuApps.js +++ b/templates/js/_topMenuApps.js @@ -1,60 +1,91 @@ -var menuCache = null +let menuCache = null -var updateTopMenu = function() { - var breakpointMobileWidth = 1024 - var menu = jQuery('#appmenu') - var apps = menu.find('li') - var minAppsDesktop = 8 - var usePercentualAppMenuLimit = 0.8 - var isMobile = jQuery(window).width() < breakpointMobileWidth - var lastShownApp = null - var appShown = [] - var moreApps = jQuery('#more-apps') - var navigation = jQuery('#navigation') - var navigationApps = jQuery('#apps ul') - var appCount = null +const breakpointMobileWidth = 1024 +const usePercentualAppMenuLimit = 0.8 +const minAppsDesktop = 8 - var currentMenuCache = menu.html() + menu.next().html() +const handleMenuClick = (e, icon) => { + let element = e.target - if (currentMenuCache === menuCache) { + while (element.tagName !== 'LI') { + element = element.parentNode + } + + const a = querySelector('a', element) + + if (a.getAttribute('target') !== '_blank' && e.which === 1 && !e.ctrlKey && !e.metaKey) { + for (let tag of ['svg', 'div']) { + let el = querySelector(tag, element) + + if (el) { + el.remove() + } + } + + const loader = createElement('div', {'class': icon}) + + a.insertBefore(loader, querySelector('span', a)) + } +} + +const updateTopMenu = function() { + const isMobile = window.innerWidth < breakpointMobileWidth + const menu = querySelector('#appmenu') + const moreApps = querySelector('#more-apps') + const navigation = querySelector('#navigation') + const navigationApps = querySelector('#apps ul') + + let apps = querySelectorAll('li', menu) + let lastShownApp = null + let appShown = [] + + if ((menu.innerHTML + menu.nextSibling.innerHTML) === menuCache) { return } - navigationApps.html('') + navigationAppsHtml = '' - apps.each(function(i, app) { - var dataId = app.getAttribute('data-id') + for (let app of apps) { + const dataId = app.getAttribute('data-id') if (dataId === null) { - return + continue } - if (topMenuApps.indexOf(dataId) === -1) { + if (topMenuApps.indexOf(dataId) === -1 && topSideMenuApps.indexOf(dataId) === -1) { app.classList.add('hidden') app.classList.add('app-hidden') } else { app.classList.remove('hidden') app.classList.add('app-external-site') + + if (topSideMenuApps.indexOf(dataId) !== -1) { + app.classList.add('app-top-side-menu') + } + appShown.push(app) - navigationApps.append(app.outerHTML) + + navigationAppsHtml = navigationAppsHtml + app.outerHTML } if (targetBlankApps.indexOf(dataId) !== -1) { - jQuery(app).children('a').attr('target', '_blank'); + querySelector('a', app).setAttribute('target', '_blank') } - }) - - var rightHeaderWidth = jQuery('.header-right').outerWidth() - var headerWidth = jQuery('header').outerWidth() - var availableWidth = headerWidth - jQuery('#nextcloud').outerWidth() - - jQuery('#header .side-menu-opener').outerWidth() - - (rightHeaderWidth > 230 ? rightHeaderWidth : 230) - - if (!isMobile) { - availableWidth = availableWidth * usePercentualAppMenuLimit } - appCount = Math.floor(availableWidth / jQuery('#appmenu li').width()) + navigationApps.innerHTML = navigationAppsHtml + + const rightHeaderWidth = querySelector('.header-right').offsetWidth + const headerWidth = querySelector('header').offsetWidth + + let availableWidth = headerWidth + + availableWidth -= nextcloud.offsetWidth + availableWidth -= querySelector('#header .side-menu-opener').offsetWidth + availableWidth -= rightHeaderWidth > 230 ? rightHeaderWidth : 230 + availableWidth *= isMobile ? usePercentualAppMenuLimit : 1 + + let appCount = Math.floor(availableWidth / querySelector('#appmenu li:not(.hidden)').offsetWidth) if (isMobile && appCount > minAppsDesktop) { appCount = minAppsDesktop @@ -62,111 +93,124 @@ var updateTopMenu = function() { appCount = minAppsDesktop } - if (appCount === 0) { - menu.addClass('hidden') - } - - menu.removeClass('hidden') - menu.css('opacity', 1) + menu.style.opacity = 1 if (appShown.length - 1 - appCount >= 1) { appCount-- } - moreApps.find('a').removeClass('active') + for (let item of querySelectorAll('a', moreApps)) { + item.classList.remove('active') + } - var k = 0 - var notInHeader = 0 - var name + let k = 0 + let notInHeader = 0 - jQuery(appShown).each(function(i, app) { - app = jQuery(app) - name = app.data('id') + for (let app of appShown) { + const name = app.getAttribute('data-id') + const li = querySelector('#apps li[data-id=' + name + '].app-external-site') if (k < appCount && appCount > 0) { - app.removeClass('hidden') - lastShownApp = app + app.classList.remove('hidden') + li.classList.add('in-header') - jQuery('#apps li[data-id=' + name + '].app-external-site').addClass('in-header') + lastShownApp = app } else { - app.addClass('hidden') + app.classList.add('hidden') + li.classList.remove('in-header') + notInHeader++ - jQuery('#apps li[data-id=' + name + '].app-external-site').removeClass('in-header') + const a = querySelector('a', app) - if (appCount > 0 && app.children('a').hasClass('active')) { - lastShownApp.addClass('hidden') - app.removeClass('hidden') + if (appCount > 0 && a.classList.contains('active')) { + lastShownApp.classList.add('hidden') + app.classList.remove('hidden') notInHeader++ - jQuery('#apps li[data-id=' + name + '].app-external-site') - .removeClass('in-header') - .addClass('in-header') + li.classList.add('in-header') } } k++ - }) + } - // Hack for https://github.com/nextcloud/server/blob/23b0b63c213f5b31eecae817ffd4a9e26f6624d0/core/src/components/MainMenu.js#L74-L96 - menu.undelegate('li:not(#more-apps) > a', 'click') - menu.delegate('li:not(#more-apps) > a', 'click', function(e) { - var a = $(e.target) + // Hack for: + // - https://github.com/nextcloud/server/blob/master/core/src/components/MainMenu.js#L97-L119 + // - https://github.com/nextcloud/server/blob/master/core/src/components/MainMenu.js#L97-L119 + jQuery(menu).undelegate('li:not(#more-apps) > a', 'click') + jQuery(navigation).undelegate('a', 'click') - if (!a.is('a')) { - a = a.closest('a') + const confs = [ + { + items: querySelectorAll('#navigation li'), + icon: 'icon-loading-small' + }, + { + items: querySelectorAll('li:not(#more-apps)', menu), + icon: OCA.Theming && OCA.Theming.inverted ? 'icon-loading-small' : 'icon-loading-small-dark' + }, + ] + + for (let conf of confs) { + for (let item of conf.items) { + item.addEventListener('click', (e) => { + handleMenuClick(e, conf.icon) + }) } + } - if (a.attr('target') !== '_blank' && e.which === 1 && !e.ctrlKey && !e.metaKey && a.parent('#more-apps').length === 0) { - a.find('svg').remove() - a.find('div').remove() - a.prepend(jQuery('
').addClass( - OCA.Theming && OCA.Theming.inverted - ? 'icon-loading-small' - : 'icon-loading-small-dark' - )) + for (let app of querySelectorAll('#apps li.app-external-site')) { + const appId = app.getAttribute('data-id') - window.location.href = a.attr('href') - } - }) - - jQuery('#apps li.app-external-site').each(function(i, app) { - app = jQuery(app) - var appId = app.attr('data-id') - - if (app.hasClass('in-header')) { - app.find('svg').find('defs').remove() + if (app.classList.contains('in-header')) { + for (let defs of querySelectorAll('svg defs', app)) { + defs.remove() + } } else { - var svg = app.find('svg'); + const svg = querySelector('svg', app) - if (svg.find('defs').length > 0) { - return; + if (querySelectorAll('svg defs', app).length > 0) { + continue } - var defs = ` + const defs = ` ` - svg.prepend(defs) - svg.find('image').attr('filter', `url(#invertMenuMore-${appId})`) + svg.innerHTML = defs + svg.innerHTML - var html = svg.get(0).innerHTML.replace(/fecolormatrix/g, 'feColorMatrix'); + for (let image of querySelectorAll('image', svg)) { + image.setAttribute('filter', `url(#invertMenuMore-${appId})`) + } - svg.html(html) + svg.innerHTML = svg.innerHTML.replace(/fecolormatrix/g, 'feColorMatrix') } - }) - - if (notInHeader === 0) { - moreApps.hide() - navigation.hide() - } else { - moreApps.show() } - menuCache = menu.html() + menu.next().html() + if (notInHeader === 0) { + moreApps.style.display = 'none' + navigation.style.display = 'none' + } else { + moreApps.style.display = 'flex' + } + + menuCache = menu.innerHTML + menu.nextSibling.innerHTML } -setInterval(updateTopMenu, 50) +for (let i = 0; i < 4000; i+= 100) { + setTimeout(updateTopMenu, i) +} + +let resizeTimeout = null; + +window.addEventListener('resize', () => { + if (resizeTimeout !== null) { + clearTimeout(resizeTimeout) + } + + resizeTimeout = setTimeout(updateTopMenu, 100) +}) diff --git a/templates/js/script.php b/templates/js/script.php index 3f834fe..f62b7da 100644 --- a/templates/js/script.php +++ b/templates/js/script.php @@ -1,59 +1,110 @@ -(function() { - var sideMenuContainer = jQuery('
') - var sideMenuOpener = jQuery('') - var sideMenu = jQuery('
') - var body = jQuery('body') - var html = jQuery('html') - var isTouchDevice = window.matchMedia("(pointer: coarse)").matches + - sideMenu.attr('data-bigmenu', '1') +$display = 'default'; + +if ($_['always-displayed']) { + $display = 'always-displayed'; +} elseif ($_['big-menu']) { + $display = 'big-menu'; +} elseif ($_['side-with-categories']) { + $display = 'side-with-categories'; +} + +?> + +(function() { + const querySelector = function(selector, element) { + if (element) { + return element.querySelector(selector) + } + + return document.querySelector(selector) + } + + const querySelectorAll = function(selector, element) { + if (element) { + return element.querySelectorAll(selector) + } + + return document.querySelectorAll(selector) + } + + const createElement = function(tagName, attributes) { + const element = document.createElement(tagName) + + if (typeof attributes === 'object') { + for (let i in attributes) { + element.setAttribute(i, attributes[i]) + } + } + + return element + } + + const sideMenuContainer = createElement('div', {id: 'side-menu-container'}) + const sideMenuOpener = createElement('button', {'class': 'side-menu-opener'}) + const sideMenu = createElement('div', {id: 'side-menu'}) + + const body = querySelector('body') + const html = querySelector('html') + const nextcloud = querySelector('#nextcloud') + + const isTouchDevice = window.matchMedia("(pointer: coarse)").matches + const targetBlankApps = + + + sideMenu.setAttribute('data-bigmenu', '1') + + sideMenu.setAttribute('data-sidewithcategories', '1') - var targetBlankApps = ; + querySelector('body').addEventListener('side-menu.apps', (e) => { + const apps = e.detail.apps; - body.on('side-menu.apps', function(e, apps) { - sideMenu = jQuery('#side-menu') + const sideMenu = querySelector('#side-menu') if (apps.length === 0) { - sideMenu.removeClass('open') - sideMenu.addClass('hide') - sideMenuOpener.addClass('hide') + sideMenu.classList.remove('open') + sideMenu.classList.add('hide') + sideMenuOpener.classList.add('hide') } else { - sideMenu.removeClass('hide') - sideMenuOpener.removeClass('hide') + sideMenu.classList.remove('hide') + sideMenuOpener.classList.remove('hide') } - + if (apps.length === 0) { - html.removeClass('side-menu-always-displayed'); + html.classList.remove('side-menu-always-displayed') } else { - html.addClass('side-menu-always-displayed'); + html.classList.add('side-menu-always-displayed') } - + if (apps.length === 0) { - html.removeClass('side-menu-always-displayed'); + html.classList.remove('side-menu-always-displayed') } else { - html.addClass('side-menu-always-displayed'); + html.classList.add('side-menu-always-displayed') } }) - body.on('side-menu.ready', function() { - sideMenu = jQuery('#side-menu') + body.addEventListener('side-menu.ready', () => { + const sideMenu = querySelector('#side-menu') + const headerMenuOpener = querySelector('#header .side-menu-opener') + const sideMenuOpener = querySelectorAll('#side-menu .side-menu-opener') - var headerMenuOpener = jQuery('#header .side-menu-opener') - var sideMenuOpener = jQuery('#side-menu .side-menu-opener') + sideMenuFocus = () => { + let a = querySelector('.side-menu-app.active a', sideMenu) - sideMenuFocus = function() { - var a = sideMenu.find('.side-menu-app.active a') + if (!a) { + return + } if (a.length === 0) { - a = sideMenu.find('.side-menu-app:first-child a') + a = querySelector('.side-menu-app:first-child a', sideMenu) } if (a.length > 0) { @@ -61,84 +112,105 @@ } } - - var sideMenuMouseLeave = function() { - sideMenu - .removeClass('open') - .off('mouseleave', sideMenuMouseLeave) + + const sideMenuMouseLeave = () => { + sideMenu.classList.remove('open') + sideMenu.removeEventListener('mouseleave', sideMenuMouseLeave) } - var sideMenuMouseEnter = function() { - sideMenu.on('mouseleave', sideMenuMouseLeave) + const sideMenuMouseEnter = () => { + sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) } - var sideMenuOpenerMouseEnter = function() { - sideMenu - .addClass('open') - .on('mouseenter', sideMenuMouseEnter) + const sideMenuOpenerMouseEnter = () => { + sideMenu.classList.add('open') + sideMenu.addEventListener('mouseenter', sideMenuMouseEnter) sideMenuFocus() } if (!isTouchDevice) { - headerMenuOpener.on('mouseenter', sideMenuOpenerMouseEnter) + headerMenuOpener.addEventListener('mouseenter', sideMenuOpenerMouseEnter) - sideMenu.addClass('hide-opener') + sideMenu.classList.add('hide-opener') - sideMenu.on('mouseleave', sideMenuMouseLeave) - sideMenu.on('mouseenter', sideMenuOpenerMouseEnter) + sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) + sideMenu.addEventListener('mouseenter', sideMenuOpenerMouseEnter) } - headerMenuOpener.on('click', function() { - sideMenu.addClass('open') - sideMenu.find('.side-menu-app.active a').focus() + headerMenuOpener.addEventListener('click', () => { + sideMenu.classList.add('open') + + const a = querySelector('.side-menu-app.active a', sideMenu) + + if (a !== null) { + a.focus() + } + + headerMenuOpener.blur() }) - - sideMenuOpener.on('click', function() { - sideMenu.toggleClass('open') + for (let opener of sideMenuOpener) { + opener.addEventListener('click', () => { + + sideMenu.classList.toggle('open') + + sideMenu.classList.remove('open') + }) - - sideMenuOpener.on('click', function() { - sideMenu.removeClass('open') - }) - + } - jQuery(document).keydown(function(e) { + document.addEventListener('keydown', (e) => { var key = e.key || e.keyCode if ((key === 'o' || key === 79) && e.ctrlKey === true) { e.preventDefault() - sideMenu.toggleClass('open') + sideMenu.classList.toggle('open') sideMenuFocus() } }) + + const sideMenuObserver = new MutationObserver((e) => { + if (body.getAttribute('id') !== 'body-settings') { + return + } + + body.classList.toggle('body-settings-side-menu', sideMenu.classList.contains('open')) + }) + + sideMenuObserver.observe(sideMenu, { + attributes: true, + attributeFilter: ['class'], + childList: false, + characterData: false + }) }) - body.append(sideMenuContainer) - sideMenuContainer.append(sideMenu) + body.appendChild(sideMenuContainer) + sideMenuContainer.appendChild(sideMenu) - sideMenuOpener.insertBefore('#nextcloud') + nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud) - sideMenuOpener.insertAfter('#nextcloud') + nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling) - - var topMenuApps = ; + + const topMenuApps = + const topSideMenuApps = - + })(); diff --git a/templates/settings/admin-form.php b/templates/settings/admin-form.php index 82196eb..59a5143 100644 --- a/templates/settings/admin-form.php +++ b/templates/settings/admin-form.php @@ -20,6 +20,7 @@ use OCP\IURLGenerator; use OCP\IConfig; use OCA\SideMenu\AppInfo\Application; +vendor_script('side_menu', 'html5sortable.min'); script('side_menu', 'admin'); style('side_menu', 'admin'); @@ -39,7 +40,6 @@ $choicesSizes = [ ]; ?> -

@@ -67,6 +67,10 @@ $choicesSizes = [ class="side-menu-setting side-menu-setting-live" value=""> +
t('Transparent')); ?> @@ -102,6 +106,10 @@ $choicesSizes = [ type="color" class="side-menu-setting side-menu-setting-live" value=""> + +

@@ -118,6 +126,10 @@ $choicesSizes = [ type="color" class="side-menu-setting side-menu-setting-live" value=""> + +
@@ -134,6 +146,10 @@ $choicesSizes = [ type="color" class="side-menu-setting" value=""> + +
@@ -217,7 +233,7 @@ $choicesSizes = [

- t('This parameters are used when Dark theme or Breeze Dark Theme are enabled.')); ?> + t('This parameters are used when Dark theme or Breeze Dark Theme are enabled.'); ?>

@@ -227,16 +243,23 @@ $choicesSizes = [
+
+
t('Transparent')); ?> @@ -266,10 +289,15 @@ $choicesSizes = [
+ +
@@ -281,10 +309,15 @@ $choicesSizes = [
+ +
@@ -296,10 +329,15 @@ $choicesSizes = [
+ +
@@ -441,64 +479,95 @@ $choicesSizes = [ t('Panel')); ?> + !$_['always-displayed'] && !$_['big-menu'] && !$_['side-with-categories'], + 'always-displayed' => $_['always-displayed'] && !$_['big-menu'] && !$_['side-with-categories'], + 'side-with-categories' => $_['side-with-categories'] && !$_['always-displayed'] && !$_['big-menu'], + 'big-menu' => $_['big-menu'] && !$_['always-displayed'] && !$_['side-with-categories'], + ]; + ?> +
- - !$_['always-displayed'] && !$_['big-menu'], - 'always-displayed' => $_['always-displayed'] && !$_['big-menu'], - 'big-menu' => $_['big-menu'], - ]; - ?> -

<?php p($l->t('Default')); ?>

+
+ +
+

t('This menu is not compatible with AppOrder.'); ?>

+

+ <?php p($l->t('With categories')); ?> +

+
- -

t('The big menu is not compatible with AppOrder.')); ?>

- +

t('This menu is not compatible with AppOrder.'); ?>

<?php p($l->t('Big menu')); ?>

-

t('Not compatible with touch screens.')); ?>

-

<?php p($l->t('Always displayed')); ?>

- - - + + +
@@ -628,7 +697,7 @@ $choicesSizes = [ @@ -636,7 +705,7 @@ $choicesSizes = [ @@ -699,7 +768,7 @@ $choicesSizes = [
- t('Apps that not must be moved in the side menu')); ?> + t('Apps only visible in the top menu')); ?>
+ + @@ -758,6 +860,60 @@ $choicesSizes = [ +
+
+ t('Custom categories')); ?> +
+
+ + +
+
+
+
+ +
+
+ t('Customize application categories')); ?> +
+
+ + 🖱️ t('Show and hide the list of applications')); ?> + + + + + +
+
+
t('Customize sorting')); ?> @@ -767,7 +923,7 @@ $choicesSizes = [ 🖱️ t('Show and hide the list of categories')); ?> -