diff --git a/.gitea/issue_template/FEATURE_TEMPLATE.md b/.gitea/issue_template/FEATURE_TEMPLATE.md deleted file mode 100644 index ed2269a..0000000 --- a/.gitea/issue_template/FEATURE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: "New feature" -about: "Use this template if you want to request a feature" -title: "[FEATURE] " -labels: - - enhancement ---- -## Feature - -### Description - -... - -### Benefits - -... diff --git a/.gitea/issue_template/FEATURE_TEMPLATE.yml b/.gitea/issue_template/FEATURE_TEMPLATE.yml new file mode 100644 index 0000000..872865f --- /dev/null +++ b/.gitea/issue_template/FEATURE_TEMPLATE.yml @@ -0,0 +1,34 @@ +name: New feature +about: Use this template if you want to request a feature +title: "[FEATURE] " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature. + validations: + required: true + + - type: textarea + id: benefits + attributes: + label: Benefits + description: Describe the benefits of this feature. + validations: + required: true + + - type: textarea + id: extra + attributes: + label: More informations + description: If you want to share more things, this is here! + validations: + required: false diff --git a/.gitea/issue_template/ISSUE_TEMPLATE.md b/.gitea/issue_template/ISSUE_TEMPLATE.md deleted file mode 100644 index f970300..0000000 --- a/.gitea/issue_template/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: "New issue" -about: "Use this template if you have a bug" -title: "[BUG] " -labels: - - bug ---- -## Issue - -### Environment - -* Custom menu version: -* Nextcloud version: -* PHP version: -* Web server (Nginx, Apache2): -* Web browser and version (Firefox 80, Google Chrome 74, etc): - -``` -Insert your configuration here. You can export the configuration using the admin page. -``` - -### Steps to reproduce - -... - -### Observed Results - -... - -### Expected Results - -... diff --git a/.gitea/issue_template/ISSUE_TEMPLATE.yml b/.gitea/issue_template/ISSUE_TEMPLATE.yml new file mode 100644 index 0000000..8486a5f --- /dev/null +++ b/.gitea/issue_template/ISSUE_TEMPLATE.yml @@ -0,0 +1,69 @@ +name: New issue +about: Use this template if you have a bug +title: "[Bug] " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + - type: textarea + id: environment + attributes: + label: Environment + value: | + * Custom menu version: + * Nextcloud version: + * PHP version: + * Web server (Nginx, Apache2): + * Web browser and version (Firefox 80, Google Chrome 74, etc): + validations: + required: true + + - type: textarea + id: configuration + attributes: + label: Configuration + description: Export the configuration using the admin page and copy/paste here ([documentation](https://deblan.gitnet.page/side_menu_doc/docs/FAQ/export-config/)). + value: | + ``` + { + ... + } + ``` + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: How reproduce the bug? + validations: + required: false + + - type: textarea + id: resuts + attributes: + label: Observed Results + description: What happened? + validations: + required: false + + - type: textarea + id: expected + attributes: + label: Expected Results + description: What should happen? + validations: + required: false + + - type: textarea + id: extra + attributes: + label: More informations + description: If you want to share more things, this is here! + validations: + required: false diff --git a/.gitea/issue_template/QUESTION_TEMPLATE.yml b/.gitea/issue_template/QUESTION_TEMPLATE.yml new file mode 100644 index 0000000..41301ae --- /dev/null +++ b/.gitea/issue_template/QUESTION_TEMPLATE.yml @@ -0,0 +1,30 @@ +name: New question +about: Use this template when you don't know how to do something +title: "[Question] " +labels: + - question +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill information. + + - type: textarea + id: environment + attributes: + label: Environment + value: | + * Custom menu version: + * Nextcloud version: + * PHP version: + * Web server (Nginx, Apache2): + * Web browser and version (Firefox 80, Google Chrome 74, etc): + validations: + required: true + + - type: textarea + id: question + attributes: + label: Question + validations: + required: true diff --git a/.gitea/issue_template/config.yml b/.gitea/issue_template/config.yml new file mode 100644 index 0000000..d065f39 --- /dev/null +++ b/.gitea/issue_template/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://deblan.gitnet.page/side_menu_doc/ + about: Official documentation web site + - name: Ask a question in our Matrix room + about: If you prefer a chat-like conversation or in need for quick help, this might be an alternative to opening an issue. + url: https://matrix.to/#/#custommenu:neutralnetwork.org diff --git a/.gitignore b/.gitignore index 89695bf..d10e458 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /releases /package-lock.json !/l10n/.gitkeep +/yarn*.log diff --git a/.stylelintrc.json b/.stylelintrc.json index 252cc97..f364910 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,5 +1,5 @@ { "rules": { - "indentation": 4 + "indentation": 2 } } diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index 8b8609f..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,42 +0,0 @@ -pipeline: - dependencies: - image: deblan/devenv - commands: - - npm install - when: - event: [tag, push, pull_request] - branch: [master, develop, feature/*] - - 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/.woodpecker/.build.yml b/.woodpecker/.build.yml new file mode 100644 index 0000000..38526c5 --- /dev/null +++ b/.woodpecker/.build.yml @@ -0,0 +1,22 @@ +variables: + volumes: &volumes + - /data/${CI_REPO}:/builds + +when: + event: [tag, push, pull_request, manual] + branch: [master, develop, feature/*, fix/*, bugfix/*, translations] + +steps: + "Build JS": + image: node:20 + commands: + - make build + + "Build translations": + image: deblan/php:8.3 + commands: + - php bin/generate_l10n.php + + "Build cache": + image: gitnet.fr/deblan/woodpecker-cache + volumes: *volumes diff --git a/.woodpecker/.publish.yml b/.woodpecker/.publish.yml new file mode 100644 index 0000000..d369dcf --- /dev/null +++ b/.woodpecker/.publish.yml @@ -0,0 +1,66 @@ +variables: + volumes: &volumes + - /data/${CI_REPO}:/builds + - /var/www/html/artifacts:/var/www/html/artifacts + +depends_on: + - build + +when: + event: [tag] + +steps: + "Verify tag and app version": + image: alpine + commands: + - TAG=${CI_COMMIT_TAG/v//} + - grep "$TAG" appinfo/info.xml + + "Create signature": + image: nextcloud:25 + volumes: *volumes + environment: + APP_CERTIFICATE: + from_secret: app_certificate + APP_PUBLIC_CERTIFICATE: + from_secret: app_public_certificate + SQLITE_DATABASE: /var/www/data/data.db + NEXTCLOUD_ADMIN_USER: admin + NEXTCLOUD_ADMIN_PASSWORD: admin + commands: + - cd "/builds/$CI_COMMIT_SHA" + - echo "$APP_CERTIFICATE" > "/tmp/side_menu.key" + - echo "$APP_PUBLIC_CERTIFICATE" > "/tmp/side_menu.crt" + - mkdir /tmp/app + - cp -r README.md CHANGELOG.md appinfo css lib img l10n js src templates screenshots vendor /tmp/app + - /usr/src/nextcloud/occ integrity:sign-app + --privateKey=/tmp/side_menu.key + --certificate=/tmp/side_menu.crt + --path=/tmp/app + - mv /tmp/app/appinfo/signature.json appinfo/ + + "Create package": + image: deblan/php:8.3 + volumes: *volumes + environment: + APP_CERTIFICATE: + from_secret: app_certificate + commands: + - cd "/builds/$CI_COMMIT_SHA" + - apt-get update + - apt-get install -y zip make + - 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 + + "Push release": + image: plugins/gitea-release + volumes: *volumes + 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//}/* diff --git a/.woodpecker/.security.yml b/.woodpecker/.security.yml new file mode 100644 index 0000000..50612c6 --- /dev/null +++ b/.woodpecker/.security.yml @@ -0,0 +1,17 @@ +variables: + volumes: &volumes + - /data/${CI_REPO}:/builds + +depends_on: + - build + +skip_clone: true + +steps: + "Check dependencies": + image: gitnet.fr/deblan/osv-detector:v0.10 + volumes: *volumes + commands: + - cd "/builds/$CI_COMMIT_SHA" + - osv-detector package-lock.json + failure: ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a79237..d55690a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,232 @@ ## [Unreleased] +## 4.1.1 +### Fixed +* fix(CssController): add missing NoCSRFRequired import (#397) +* fix(SideMenu): ncApps must be an array (#369) + +## 4.1.0 +### Added +* add compatibility with NC31 +### Fixed +* fix(service): add service constructor arguments +* fix(settings): remove non-existing and unused ILogger service +### Changed +* refactor(controller): usage of attributes instead of annotations + +## 4.0.1 +### Fixed +* fix top menu labels (fix #368) +* fix #369: The menu is displayed even if there are no apps + +## 4.0.0 +### Added +* add compatibility with NC30 + +## 3.13.1 +### Fixed +* fix #354: remove the opener when the menu is always displayed +* fix extra margin between the logo and the opener + +## 3.13.0 +### Added +* show apps generated with Tables (fix #349) +* add constructor property promotion +### Fixed +* remove .app-navigation--close translationX for always-displayed menu (fix #348) + +## 3.12.0 +### Added +* add compatibility with NC29 + +## 3.11.8 +### Fixed +* move the logo inside #nextcloud element (fix #278 #239) [NC26] + +## 3.11.7 +### Added +* update translations +* update ci steps names +### Fixed +* add accessibility to open and close buttons (#311) +* fully apply Nextcloud AppMenu.vue updated (#326) +* add missing label on the 'save' button in personal settings (fix #318) +### Changed +* upgrade axios +* upgrade css-loader + +## 3.11.6 +### Fixed +* add --background-invert-if-bright in top menu (fix #326) + +## 3.11.5 +### Fixed +* add missing label on buttons for accessiblity (fix #311) + +## 3.11.4 +### Fixed +* add label on buttons for accessiblity (fix #311) + +## 3.11.3 +### Fixed +* fix menu icon in decks, collectives and other apps (#302) + +## 3.11.2 +### Fixed +* add default translations for Slovak - fix #298 + +## 3.11.1 +### Added +* add Portuguese (Brazil) translations - Thanks to igorfreire +### Fixed +* add width to .side-menu-categories for side menu with categories display - fix #294 + +## 3.11.0 +### Added +* add a search component in menus - fix #282 +### Fixed +* remove the label of the link to personal settings - fix #283 + +## 3.10.3 +### Fixed +* change the way to load nextcloud components (NcActionLink/NcActions) - fix #274 +* update @nexcloud/* packages + +## 3.10.2 +### Fixed +* add missing properties + +## 3.10.1 +### Fixed +* fix #269: use php7 syntax + +## 3.10.0 +### Added +* add compatibility with NC28 +### Fixed +* fix NC28 error: remove deprecated method `OC_App::getNavigation()` + +## 3.9.1 +### Fixed +* fix fixed menu on dashboard (#262) + +## 3.9.0 +### Added +* add compatibility with NC27 +### Fixed +* fix app redirect (#261) + +## 3.8.0 +### Added +* add option to show hovered label only on top menu (fix #253) + +## 3.7.4 +### Fixed +* fix Integrity failed (#247) + +## 3.7.3 +### Fixed +* fix #244: use app href for redirection +### Added +* add signature on build + +## 3.7.2 +### Added +* update pipeline conditions allowing `fix/*` +### Fixed +* fix #233: load configuration and then retrieve apps in default side menu display + +## 3.7.1 +### Fixed +* fix build process (#230) + +## 3.7.0 +### Added +* add translations (thanks to AHOHNMYC) +* add compatibility with NC26 + +## 3.6.0 +### Added +* add hidden apps compatible with default menu (#219) + +## 3.5.2 +### Fixed +* add check if menu exists before adding event listeners (#210) + +## 3.5.1 +### Added +* add translations (thanks to p-bo adn gallegonovato) +### Fixed +* fix #189: sorting not applied on mobile + +## 3.5.0 +### Added +* add dependency check (ci) +* add code quality check (ci) +* add translations (thanks to gallegonovato) +* add option to disable the display labels in the top menu (#194) +### Fixed +* fix missing img alt (settings image) +* fix code quality alerts + +## 3.4.1 +### Added +* add translations (thanks to zonorti, jorisvandijk, jak2k) +### Fixed +* fix #183: hide custom categories list when empty (admin page) + +## 3.4.0 +### Added +* add translations (thanks to Pavelb, nier, Timur, p-bo) +* add possibility to define Custom Menu as default app and redirect to the first top menu app (#177) + +## 3.3.2 +### Fixed +* fix #173: reduce the height of categories list + +## 3.3.1 +### Fixed +* fix #162: top and side apps does work correctly + +## 3.3.0 +### Added +* add documentation in admin page +* add app sorter in user config side (#160) +### Fixed +* fix #164: open apps in new tab does not work +* fix #162 #159: top and side apps does work correctly + +## 3.2.1 +### Fixed +* fix #150: active app is not visible has active in menu (except in default menu) +* fix #151: opener position + +## 3.2.0 +### Added +* use custom app names using 'app.navigation.name' (#148) +* app sorting with all displays (#147) + +## 3.1.0 +### Added +* add global custom app sorting for the top menu +### Fixed +* fix admin list/modal look + +## 3.0.1 +### Fixed +* Remove the gap between the window's top and menu categories (large menu) + +## 3.0.0 +### Added +* Add compatibility with NC25 (#136/#135) +### Removed +* Nextcloud 20-24 are not supported anymore +* AppOrder is not supported anymore + ## 2.5.1 ### Fixed * fix icon render (#133) - ## 2.5.0 ### Changed * upgrade dependencies diff --git a/Makefile b/Makefile index 1639f5c..e5b6267 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,15 @@ -npm-build: +build: dep npm run build -npm-watch: +watch: dep npm run watch +dep: + npm i + npm link @nextcloud/vue || sudo npm link @nextcloud/vue + .ONESHELL: -release: npm-build translations +release: if [ -z "$$VERSION" ]; then echo "VERSION required" exit 1 @@ -30,4 +34,4 @@ translations: .ONESHELL: run-code-quality-analysis: export SONAR_TOKEN="$$SONAR_TOKEN_DEBLAN_SIDE_MENU" - sonar-scanner -Dsonar.projectKey=deblan-side_menu -Dsonar.sources=. -Dsonar.host.url=https://cq.gitnet.fr + sonar-scanner -Dsonar.projectKey=deblan-side_menu -Dsonar.sources=. -Dsonar.host.url=$$SONAR_SERVER -Dsonar.branch.name=$$(git branch --show-current) diff --git a/README.md b/README.md index 92f05f7..bf8abbb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ 🤙 Nextcloud app / Custom menu 🎨 =============================== +[![Build Status](https://ci.gitnet.fr/api/badges/deblan/side_menu/status.svg)](https://ci.gitnet.fr/deblan/side_menu) +[![Translations](https://translate.codeberg.org/widgets/custom-menu/-/application/svg-badge.svg)](https://translate.codeberg.org/engage/custom-menu/) +![Downloads](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=downloads&query=%24.K_downloads&suffix=K&url=https%3A%2F%2Fapi-side-menu.deblan.org%2Fdownloads.php) + 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 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. -You can customize colors depending of the theme (Dark theme and Breeze Dark). Comptatible with AppOrder. +You can customize colors depending of the theme (Dark theme and Breeze Dark). * [Installation and upgrade](#installation-and-upgrade) * [How to contribute?](#how-to-contribute) @@ -15,12 +19,12 @@ You can customize colors depending of the theme (Dark theme and Breeze Dark). Co 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) +## [📘 Read the documentation](https://deblan.gitnet.page/side_menu_doc/) Requirements ------------ -* PHP >= 7.4 +* PHP >= 8.0 * App `theming` enabled Installation and upgrade @@ -46,19 +50,33 @@ Users can disable the menu using the page of personal settings. Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate. +### Use first top menu app as default app + +You can easily let Custom Menu redirect to the first app in the top menu by changing the following parameter in your `config/config.php`: + +``` +'defaultapp' => 'side_menu', +``` + +If the top menu is empty then it redirects to files. + How to contribute? ------------------ You can report a bug or request a feature by opening an issue: https://gitnet.fr/deblan/side_menu/issues -If you are a developer: +### You are a translator + +Translations are managed from [translate.codeberg.org](https://translate.codeberg.org/projects/custom-menu/application/). + +### You are a developer * fork the repository * install an instance of Nextcloud * go to `apps/` and clone your repository -* go to `apps/side_menu` and run `npm install` +* go to `apps/side_menu` and run `make dep` -Build javascripts using `make npm-build` (or `make npm-watch` to build them in real time). +Build javascripts using `make build` (or `make watch` to build them in real time). Then commit and create a pull request. @@ -66,3 +84,10 @@ Support ------- You can join the official room on Matrix: [#custommenu:neutralnetwork.org](https://matrix.to/#/#custommenu:neutralnetwork.org). + + +Notice +------ + +Because I believe in a free and decentralized Internet, [Gitnet](https://gitnet.fr) is **self-hosted at home**. +In case of downtime, you can download **Custom Menu** from [here](https://kim.deblan.fr/~side_menu/). diff --git a/appinfo/info.xml b/appinfo/info.xml index f4c1969..d6a0f5f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -1,6 +1,5 @@ - - + + side_menu Custom menu Modify the display of the menu. @@ -11,13 +10,13 @@ This application is rather suitable for instances that activate a lot of applica Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate. -You can customize colors depending of the theme (Dark theme and Breeze Dark). Comptatible with AppOrder. +You can customize colors depending of the theme (Dark theme and Breeze Dark). You can report a bug or request a feature by opening an issue. Requirements: -* PHP >= 7.4 +* PHP >= 8.1 * App `theming` enabled If you like this application and if you want to support the development: @@ -25,29 +24,37 @@ If you like this application and if you want to support the development: * [Buy me a coffee](https://www.buymeacoffee.com/deblan) * [Donate with liberapay](https://liberapay.com/deblan) * [Leave a comment](https://apps.nextcloud.com/apps/side_menu#comments) + +Notice +------ + +Because I believe in a free and decentralized Internet, [Gitnet](https://gitnet.fr) is **self-hosted at home**. +In case of downtime, you can download **Custom Menu** from [here](https://kim.deblan.fr/~side_menu/). ]]> - 2.5.1 + 4.1.1 agpl - Simon Vieille + Simon Vieille SideMenu - https://gitnet.fr/deblan/side_menu/src/branch/master/README.md + https://deblan.gitnet.page/side_menu_doc/ https://gitnet.fr/deblan/side_menu/src/branch/master/README.md customization https://gitnet.fr/deblan/side_menu - https://matrix.to/#/!TFPucDATKODpHNVAtu:neutralnetwork.org?via=neutralnetwork.org + https://gitnet.fr/deblan/side_menu/issues https://gitnet.fr/deblan/side_menu - https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc19_default_menu.png - https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/admin_settings.png - https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/n19_big_menu.png - https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc18_menu_always_displayed.png - 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/appinfo/routes.php b/appinfo/routes.php deleted file mode 100644 index 1efd4b6..0000000 --- a/appinfo/routes.php +++ /dev/null @@ -1,30 +0,0 @@ -. - */ - -return [ - 'routes' => [ - ['name' => 'Css#stylesheet', 'url' => '/css/stylesheet', 'verb' => 'GET'], - ['name' => 'Js#script', 'url' => '/js/script', 'verb' => 'GET'], - ['name' => 'Js#config', 'url' => '/js/config', 'verb' => 'GET'], - ['name' => 'Nav#items', 'url' => '/nav/items', 'verb' => 'GET'], - ['name' => 'PersonalSetting#valueSet', 'url' => '/personalSetting/valueSet', 'verb' => 'POST'], - ['name' => 'AdminSetting#removeCache', 'url' => '/admin/cache/remove', 'verb' => 'GET'], - ['name' => 'AdminSetting#exportConfiguration', 'url' => '/admin/config/export', 'verb' => 'GET'], - ], -]; diff --git a/bin/import_config.php b/bin/import_config.php index f2fbd14..db5857d 100644 --- a/bin/import_config.php +++ b/bin/import_config.php @@ -1,21 +1,75 @@ prepare('UPDATE oc_appconfig SET configvalue=:value WHERE configkey=:key and appid=:appId'); + exit($code); +} -foreach ($config as $key => $value) { +function value(string $shortName, string $longName, array $options, bool $required = true): ?string +{ + $value = $options[$shortName] ?? $options[$longName] ?? null; + + if (is_array($value)) { + echo "To much --{$longName}\n"; + showUsageAndExit(1); + } + + if (empty($value) && $required) { + echo "--{$longName} is missing\n"; + showUsageAndExit(1); + } + + return $value; +} + +$options = getopt('t:f:c:h', [ + 'type:', + 'file:', + 'config:', + 'help', +]); + +$help = value('h', 'help', $options, false); +$config = value('c', 'config', $options); +$file = value('f', 'file', $options); + +if (!is_readable($config) && !is_file($config)) { + echo "No such file: {$config}\n"; + + exit(1); +} + +if (!is_readable($file) && !is_file($file)) { + echo "No such file: {$file}\n"; + + exit(1); +} + +$appConfig = json_decode(file_get_contents($file), true); + +require $config; + +if ('mysql' === $CONFIG['dbtype']) { + $pdo = new \PDO( + 'mysql:host='.$CONFIG['dbhost'].';dbname='.$CONFIG['dbname'], + $CONFIG['dbuser'], + $CONFIG['dbpassword'] + ); +} elseif ($CONFIG['dbtype']) { + $pdo = new \PDO(sprintf('sqlite:%s', $CONFIG['datadirectory'].'/owncloud.db')); +} else { + echo "dbtype is not valid\n"; + + exit(1); +} + +$stmt = $pdo->prepare('UPDATE '.$CONFIG['dbtableprefix'].'appconfig SET configvalue=:value WHERE configkey=:key and appid=:appId'); + +foreach ($appConfig as $key => $value) { $stmt->execute([ 'appId' => 'side_menu', 'key' => $key, diff --git a/css/admin.css b/css/admin.css index b35455a..b3d0b7c 100644 --- a/css/admin.css +++ b/css/admin.css @@ -16,131 +16,205 @@ */ #side-menu-section input[type="color"] { - width: 100px; - margin: 10px 0 10px 0; + width: 100px; + margin: 10px 0 10px 0; + padding: 0; + border-radius: 0; } #-dropside-menu-section input[type="checkbox"] { - vertical-align: middle; + vertical-align: middle; } #side-menu-section input[type="range"] { - vertical-align: middle; + vertical-align: middle; } #side-menu-section select { - margin: 10px 0 10px 0; + margin: 10px 0 10px 0; } .keyboard-key { - padding: 1px 9px; - margin: 0 2px; - background: #eee; - border: 1px solid #aaa; - color: #555; - border-radius: 3px; + padding: 1px 9px; + margin: 0 2px; + background: #eee; + border: 1px solid #aaa; + color: #555; + border-radius: 3px; } .side-menu-display { - padding: 10px; - border: 2px solid transparent; - max-width: 100%; - cursor: pointer; + padding: 10px; + border: 2px solid transparent; + max-width: 100%; + cursor: pointer; } .side-menu-display.is-active { - border: 2px solid #91cb7f; + border: 2px solid #91cb7f; } .info { - margin-top: 8px; - padding: 5px; - background: #91cb7f; - color: #fff; - border-radius: var(--border-radius); + margin-top: 8px; + padding: 5px; + background: #91cb7f; + color: #fff; + border-radius: var(--border-radius); } #side-menu-section h2 small { - font-size: 11px; - font-weight: normal; + font-size: 11px; + font-weight: normal; } .side-menu-toggler { - cursor: pointer; + cursor: pointer; } .side-menu-setting-list { - margin: 10px 4px 4px 0px; + margin: 10px 4px 4px 0px; + border: 2px solid var(--color-border-dark); + border-radius: 15px; } .side-menu-setting-list-item { - padding: 5px 10px; - border: 1px solid var(--color-border-dark); - max-width: 300px; - margin: -1px 0 0 0; - cursor: pointer; + padding: 5px 10px; + border-bottom: 1px solid var(--color-border-dark); + max-width: 300px; + margin: -1px 0 0 0; + cursor: pointer; + line-height: 32px; +} + +.side-menu-setting-list-item:last-child { + border-bottom: 0; } .side-menu-setting-list-drop { - background: yellow; - border-color: yellow; - height: 34px; + background: yellow; + border-color: yellow; + height: 34px; } .side-menu-setting.arrow { - color: #ccc; - padding-right: 5px; + color: #ccc; + padding-right: 5px; } .side-menu-setting-list-item input { - min-height: auto; - margin-top: -1px; + margin-top: 0; + height: 21px !important; + min-height: auto !important; } #apps-categories-custom-list select { - width: 100%; + width: 100%; } - .side-menu-setting-table { - display: table; - width: 100%; + display: table; + width: 100%; } .side-menu-setting-row { - display: table; + display: table; + margin-bottom: 10px; +} + +.side-menu-setting-row code { + margin-left: 2px; + margin-bottom: 1px; + padding: 3px 10px; + border-radius: 5px; + display: inline-block; + right: 2px; + border: 1px solid var(--color-border-dark); } .side-menu-setting-label { - display: table-cell; - width: 400px; - padding-right: 20px; + display: table-cell; + width: 430px; + padding-right: 20px; +} + +.side-menu-setting-label--top { + vertical-align: top; } .side-menu-setting-form { - display: table-cell; - min-width: 300px; + display: table-cell; + min-width: 300px; } .side-menu-setting-label-short { - width: 300px; + width: 300px; } .side-menu-setting-form-long { - width: 400px; + width: 400px; } #side-menu-save-progress { - display: inline-block; - width: 0; - height: 15px; - background: #fff; + display: inline-block; + width: 0; + height: 15px; + background: #fff; } .btn-reset { - display: inline-block; - cursor: pointer; - position: absolute; - margin-top: 17px; - margin-left: 5px; + display: inline-block; + cursor: pointer; + position: relative; + top: -8px; + left: 5px; + transition-duration: 0.8s; + transition-property: transform; + transform: rotate(360deg); +} + +.btn-reset--down { + top: 2px; +} + +.btn-reset--progress { + transform: rotate(-359deg); +} + +.badges { + margin-bottom: 14px; + margin-top: 4px; +} + +.badge { + border-width: 1px; + padding: 2px 8px; + margin-right: 2px; + margin-bottom: 5px; + display: inline-block; + border-radius: 4px; + font-size: 13px; +} + +.badge-1 { + background: #d4ce14; + border-color: #cad413; + color: #373a05; +} + +.badge-2 { + background: #96d47f; + border-color: #7ed49b; + color: #333; +} + +.badge-3 { + background: #d4540a; + border-color: #d4700c; + color: #fff; +} + +.badge-4 { + background: #9d81d4; + border-color: #c681d4; + color: #fff; } diff --git a/css/sideMenu.css b/css/sideMenu.css index 80ed6b7..66a1a8c 100644 --- a/css/sideMenu.css +++ b/css/sideMenu.css @@ -16,326 +16,371 @@ */ #side-menu { - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 100%; - max-width: 250px; - background: linear-gradient(90deg, var(--side-menu-background-color, #333) 0%, var(--side-menu-background-color-to, #333) 100%); - z-index: 3000; - color: var(--side-menu-text-color, #fff); - box-shadow: rgba(0, 0, 0, 0.22) 0px 25.6px 57.6px 0px, rgba(0, 0, 0, 0.18) 0px 4.8px 14.4px 0px; - display: none; + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100%; + max-width: 290px; + background: linear-gradient(90deg, var(--side-menu-background-color, #333) 0%, var(--side-menu-background-color-to, #333) 100%); + z-index: 3000; + color: var(--side-menu-text-color, #fff); + box-shadow: rgba(0, 0, 0, 0.22) 0px 25.6px 57.6px 0px, rgba(0, 0, 0, 0.18) 0px 4.8px 14.4px 0px; + display: none; } #side-menu a { - transition: 0.2s; + transition: 0.2s; } #side-menu.open { - display: block; + display: block; } #header .side-menu-opener { - margin-left: 0px; + margin-left: 0px; + margin-top: -1px; } .side-menu-settings { - margin-right: 9px; - margin-top: 2px; - float: right; - line-height: 34px; - height: 28px; - display: none; + margin-right: 9px; + margin-top: 2px; + float: right; + line-height: 34px; + height: 42px; + display: none; } .side-menu-settings a { - color: var(--side-menu-text-color, #fff); - display: block; - padding: 4px 7px; + color: var(--side-menu-text-color, #fff); + display: block; + padding: 4px 7px; } .side-menu-settings:hover a, .side-menu-settings a:active, .side-menu-settings a:focus { - background: var(--side-menu-current-app-background-color, #444); + background: var(--side-menu-current-app-background-color, #444); } .side-menu-settings img { - vertical-align: bottom; - margin-left: 3px; - width: 32px; - height: 32px; + vertical-align: bottom; + margin-left: 3px; + width: 32px; + height: 32px; } #side-menu.open .side-menu-settings { - display: block; + display: block; } .side-menu-opener { - background: var(--side-menu-opener, url('../img/side-menu-opener.svg')); - 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; + background: var(--side-menu-opener, url('../img/side-menu-opener.svg')); + 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; + overflow: hidden; +} + +.side-menu-opener span { + position: relative; + left: 50px; + display: block; + width: 1px; + height: 1px; + overflow: hidden; } .side-menu-opener:active, .side-menu-opener:focus { - background-color: var(--side-menu-current-app-background-color, #444) !important; + background-color: var(--side-menu-current-app-background-color, #444) !important; } .side-menu-closer { - background: url('../img/side-menu-opener-closer.svg'); - display: none; + background: url('../img/side-menu-opener-closer.svg'); + display: none; } #side-menu.hide-opener .side-menu-opener, .side-menu-opener.hide, #side-menu.hide { - display: none !important; + display: none !important; } .side-menu-apps-list { - height: calc(100vh - 150px); - z-index: 2200; - position: fixed; - top: 150px; - width: 100%; - max-width: 250px; - overflow: auto; + height: calc(100vh - 150px); + z-index: 2200; + position: fixed; + top: 150px; + width: 100%; + max-width: 290px; + overflow: auto; } .side-menu-app-icon { - width: 20px; - vertical-align: top; - margin-right: 10px; - filter: invert(var(--side-menu-icon-invert-filter, 0%)); - opacity: var(--side-menu-icon-opacity, 1); -} - -.side-menu-app-icon svg { - vertical-align: middle; - margin-top: -3px; -} - -.side-menu-app-icon .app-icon-notification { - display: none; + width: 20px; + vertical-align: middle; + margin-top: -4px; + margin-right: 10px; + filter: invert(var(--side-menu-icon-invert-filter, 0%)); + opacity: var(--side-menu-icon-opacity, 1); } .side-menu-app a { - line-height: 30px; - color: var(--side-menu-text-color, #fff); - display: block; - padding: 7px 0 5px 15px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + line-height: 30px; + color: var(--side-menu-text-color, #fff); + display: block; + padding: 7px 0 5px 15px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .side-menu-app a:hover, .side-menu-app.active, .side-menu-app a:focus { - background: var(--side-menu-current-app-background-color, #444); + background: var(--side-menu-current-app-background-color, #444); } .side-menu-logo { - text-align: center; + text-align: center; } .side-menu-logo img { - max-width: 60%; - max-height: 100px; + max-width: 60%; + max-height: 100px; } -.side-menu-header { - height: 150px; - width: 100%; - z-index: 2300; - max-width: 250px; - position: fixed; - padding-top: 2px; - top: 0; +.enu-header { + height: 150px; + width: 100%; + z-index: 2300; + max-width: 290px; + position: fixed; + padding-top: 2px; + top: 0; } #side-menu.side-menu-with-categories .side-menu-header { - max-width: 295px; + max-width: 295px; } #side-menu.hide-opener .side-menu-logo { - margin-top: 20px; + margin-top: 10px; } #side-menu-loader { - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 3001; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 3001; } #side-menu-loader-bar { - height: 4px; - background: var(--side-menu-loader-color, #0e75ac); - width: 0; - transition-property: width; + height: 4px; + background: var(--side-menu-loader-color, #0e75ac); + width: 0; + transition-property: width; } #side-menu.side-menu-big, #side-menu.side-menu-with-categories { - max-width: 100%; - height: auto; + max-width: 100%; + height: auto; } .side-menu-big .side-menu-header, .side-menu-with-categories .side-menu-header { - height: auto; + height: auto; } .side-menu-big .side-menu-apps-list, .side-menu-with-categories .side-menu-apps-list { - height: auto; - position: static; - max-width: 100vw; - overflow: auto; + height: auto; + position: static; + max-width: 100vw; + overflow: auto; } .side-menu-big .side-menu-app a, .side-menu-with-categories .side-menu-app a { - padding: 7px 0 7px 7px; + padding: 7px 0 7px 7px; } .side-menu-categories-wrapper { - padding-bottom: 70px; + padding-bottom: 70px; } .side-menu-categories { - max-height: calc(100vh - 50px); - overflow: auto; - position: relative; - top: 50px; - display: flex; - flex-wrap: wrap; - justify-content: center; - padding: 0 10% 0 10%; + max-height: calc(100vh - 55px); + overflow: auto; + position: relative; + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: 0 10% 0 10%; } .side-menu-category { - padding: 10px 20px; - flex: 1 1 auto; + padding: 10px 20px; + flex: 1 1 auto; } .side-menu-category-title { - padding-left: 10px; - color: var(--side-menu-text-color, #fff); + padding-left: 10px; + color: var(--side-menu-text-color, #fff); + font-weight: bold; + font-size: 20px; + margin-bottom: 12px; + line-height: 30px; + margin-top: 0; } .side-menu-loader { - text-align: center; + text-align: center; } .side-menu-loader svg { - width: 38px; - margin: auto; - stroke: var(--side-menu-text-color, #fff); + width: 45px; + margin: auto; + stroke: var(--side-menu-text-color, #fff); } .side-menu-with-categories .side-menu-app-icon, .side-menu-big .side-menu-app-icon { - vertical-align: middle; - margin-top: -2px; -} - -.side-menu-always-displayed #header, -.side-menu-always-displayed body { - width: calc(100% - 50px) !important; + vertical-align: middle; + margin-top: -2px; } .side-menu-always-displayed body { - position: absolute; - left: 50px; + width: calc(100% - 50px) !important; + position: absolute; + left: 50px; +} + +.side-menu-always-displayed #header { + position: absolute !important; } .side-menu-always-displayed #side-menu { - display: block; + display: block; } .side-menu-always-displayed .side-menu-apps-list { - height: calc(100vh - 49px); - top: 49px; - overflow: hidden; + height: 100vh; + top: 0; + overflow: hidden; +} + +.side-menu-always-displayed .side-menu-apps-list--with-settings { + height: calc(100vh - 49px); + top: 49px; } .side-menu-always-displayed .side-menu-apps-list:hover { - overflow: auto; + overflow: auto; } .side-menu-always-displayed #side-menu, .side-menu-always-displayed .side-menu-header, .side-menu-always-displayed .side-menu-apps-list { - width: 50px; + width: 50px; } .side-menu-always-displayed #side-menu .side-menu-app-text, .side-menu-always-displayed #header .side-menu-opener, .side-menu-always-displayed .side-menu-logo { - display: none; + display: none; } .side-menu-always-displayed #side-menu .side-menu-header { - height: 49px; + height: 49px; } .side-menu-always-displayed #side-menu.open, .side-menu-always-displayed #side-menu.open .side-menu-apps-list, .side-menu-always-displayed #side-menu.open .side-menu-header { - width: 100%; + width: 100%; } .side-menu-always-displayed #side-menu.open .side-menu-app-text { - display: inline; + display: inline; } -.side-menu-always-displayed .app-navigation--close { - transform: translateX(calc(-100% + 50px)) !important; +.side-menu-always-displayed .app-navigation-toggle-wrapper { + right: 0 !important; + margin-left: 0 !important; } #side-menu.side-menu-with-categories { - max-width: 290px; - height: 100vh; + max-width: 290px; + height: 100vh; } .side-menu-with-categories .side-menu-categories { - display: block; - padding: 0; + display: block; + padding: 0; + width: 100%; } .side-menu-with-categories .side-menu-category { - padding: 10px 0; + padding: 10px 0; } .side-menu-always-displayed #body-settings, #body-settings.body-settings-side-menu { - overflow-x: visible; + overflow-x: visible; +} + +.app-menu { + visibility: hidden; +} + +.app-menu.show { + visibility: visible; +} + +.side-menu-search { + float: right; +} + +.side-menu-search input { + background: none; + border: 0; + border-radius: 0; + color: var(--side-menu-text-color); +} + +.side-menu-search input::placeholder { + color: var(--side-menu-text-color); +} + +.side-menu-always-displayed .side-menu-search { + display: none; } @media screen and (max-width: 1024px) { - #side-menu.side-menu-big { - max-width: 290px; - height: 100vh; - } + #side-menu.side-menu-big { + max-width: 290px; + height: 100vh; + } - .side-menu-categories { - display: block; - padding: 0; - } + #side-menu.hide-opener.side-menu-big .side-menu-search { + float: none; + } - .side-menu-category { - padding: 10px 0; - } + .side-menu-categories { + display: block; + padding: 0; + } + + .side-menu-category { + padding: 10px 0; + } } @media screen and (min-width: 1024px) { - .side-menu-closer { - display: block; - float: right; - margin-right: 9px; - } + .side-menu-closer { + display: block; + float: right; + margin-right: 9px; + } - .side-menu-big .side-menu-header { - max-width: 100%; - } + .side-menu-big .side-menu-header { + max-width: 100%; + } } diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b812fd7..c5181ef 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -3,6 +3,7 @@ namespace OCA\SideMenu\AppInfo; use OC; +use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\User\User; use OCA\SideMenu\Service\AppRepository; @@ -12,7 +13,11 @@ use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\INavigationManager; use OCP\IUserSession; +use OCP\L10N\IFactory; use OCP\Util; use Psr\Container\ContainerInterface; @@ -26,6 +31,7 @@ class Application extends App implements IBootstrap public const APP_ID = 'side_menu'; public const APP_NAME = 'Custom menu'; + /** * @var OC\AllConfig */ @@ -41,14 +47,55 @@ class Application extends App implements IBootstrap */ protected $user; - /** - * {@inheritdoc} - */ public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } + public function register(IRegistrationContext $context): void + { + $context->registerService(CategoryRepository::class, function (ContainerInterface $c) { + return new CategoryRepository( + $c->get(CategoryFetcher::class), + $c->get(ConfigProxy::class), + $c->get(IConfig::class), + $c->get(IFactory::class), + $c->get(IUserSession::class) + ); + }); + + $context->registerService(AppRepository::class, function (ContainerInterface $c) { + return new AppRepository( + $c->get(\OC_App::class), + $c->get(INavigationManager::class), + $c->get(IFactory::class), + $c->get(ConfigProxy::class), + $c->get(CategoryRepository::class), + $c->get(IEventDispatcher::class), + $c->get(IUserSession::class) + ); + }); + + $context->registerService(ConfigProxy::class, function (ContainerInterface $c) { + return new ConfigProxy( + $c->get(IConfig::class), + ); + }); + } + + 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(); + } + protected function isEnabled(): bool { $enabled = true; @@ -97,38 +144,14 @@ class Application extends App implements IBootstrap $cache = $this->config->getAppValue(self::APP_ID, 'cache', '0'); foreach ($assets as $value) { - $route = OC::$server->getURLGenerator()->linkToRoute($value['route'], ['v' => $cache]); + $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 () { - return new AppRepository(); - }); - - $context->registerService('CategoryRepository', function () { - return new CategoryRepository(); - }); - - $context->registerService('ConfigProxy', function () { - 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/AdminSettingController.php b/lib/Controller/AdminSettingController.php index aa11d9a..279a34c 100644 --- a/lib/Controller/AdminSettingController.php +++ b/lib/Controller/AdminSettingController.php @@ -1,4 +1,5 @@ config = $config; - $this->urlGenerator = $urlGenerator; } - /** - * @NoCSRFRequired - * - * @return RedirectResponse - */ - public function removeCache() + #[NoCSRFRequired] + #[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')] + public function removeCache(): RedirectResponse { $this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]'); @@ -61,12 +51,9 @@ class AdminSettingController extends Controller ]).'#more'); } - /** - * @NoCSRFRequired - * - * @return Response - */ - public function exportConfiguration() + #[NoCSRFRequired] + #[FrontpageRoute(verb: 'GET', url: '/admin/config/export')] + public function exportConfiguration(): DataDownloadResponse { $keys = $this->config->getAppKeys(Application::APP_ID); $appConfig = []; diff --git a/lib/Controller/AppController.php b/lib/Controller/AppController.php new file mode 100644 index 0000000..f8cb357 --- /dev/null +++ b/lib/Controller/AppController.php @@ -0,0 +1,98 @@ +. + */ + +namespace OCA\SideMenu\Controller; + +use OCA\SideMenu\Service\AppRepository; +use OCA\SideMenu\Service\ConfigProxy; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserSession; + +class AppController extends Controller +{ + public function __construct( + string $appName, + IRequest $request, + protected AppRepository $appRepository, + protected IURLGenerator $urlGenerator, + protected ConfigProxy $config + ) { + parent::__construct($appName, $request); + } + + #[NoCSRFRequired] + #[NoAdminRequired] + #[FrontpageRoute(verb: 'GET', url: '/')] + public function index(): RedirectResponse + { + $user = \OC::$server[IUserSession::class]->getUser(); + $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); + $hiddenApps = $this->config->getAppValueArray('big-menu-hidden-apps', '[]'); + $isForced = $this->config->getAppValueBool('force', '0'); + $userTopMenuApps = $this->config->getUserValueArray($user, 'top-menu-apps', '[]'); + $apps = $this->appRepository->getOrderedApps($user); + + if (!$isForced && !empty($userTopMenuApps)) { + $topMenuApps = $userTopMenuApps; + } + + foreach ($apps as $app) { + $inTopMenuApps = in_array($app['id'], $topMenuApps); + $inHiddenApps = in_array($app['id'], $hiddenApps); + + if (!$inTopMenuApps && $inHiddenApps) { + continue; + } + + return $this->redirectToApp($app, true); + } + + return $this->redirectToApp('files'); + } + + protected function redirectToApp($app, bool $isHref = false): RedirectResponse + { + if (!$isHref) { + $isIgnoreFrontController = true === \OC::$server->getConfig()->getSystemValue( + 'htaccess.IgnoreFrontController', + false + ); + + $isFrontControllerActive = 'true' === getenv('front_controller_active'); + + if ($isIgnoreFrontController || $isFrontControllerActive) { + $path = '/apps/%s/'; + } else { + $path = '/index.php/apps/%s/'; + } + + $url = $this->urlGenerator->getAbsoluteURL(sprintf($path, $app)); + } else { + $url = $app['href']; + } + + return new RedirectResponse($url); + } +} diff --git a/lib/Controller/CssController.php b/lib/Controller/CssController.php index 028ac11..5b3a2dd 100644 --- a/lib/Controller/CssController.php +++ b/lib/Controller/CssController.php @@ -1,4 +1,5 @@ user = OC::$server[IUserSession::class]->getUser(); - $this->config = $config; - $this->theming = $theming; - $this->color = $color; + $this->user = \OC::$server[IUserSession::class]->getUser(); } - /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * - * @return Response - */ - public function stylesheet() + #[NoCSRFRequired] + #[NoAdminRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: '/css/stylesheet')] + public function stylesheet(): TemplateResponse { $response = new TemplateResponse(Application::APP_ID, 'css/stylesheet', $this->getConfig(), 'blank'); $response->addHeader('Content-Type', 'text/css'); @@ -105,16 +82,18 @@ class CssController extends Controller $topSideMenuApps = $userTopSideMenuApps; } - $isDarkThemeUserEnabled = $this->config->getUserValue($this->user, 'theme', '', 'accessibility') === 'dark'; + $isDarkThemeUserEnabled = 'dark' === $this->config->getUserValue($this->user, 'theme', '', 'accessibility'); $isBreezeDarkUserEnabled = $this->config->getUserValue($this->user, 'theme_enabled', '', 'breezedark'); - $isBreezeDarkUserEnabled = $isBreezeDarkUserEnabled === '1' || ($isBreezeDarkGlobalEnabled && $isBreezeDarkUserEnabled === ''); + $isBreezeDarkUserEnabled = '1' === $isBreezeDarkUserEnabled + || ($isBreezeDarkGlobalEnabled && '' === $isBreezeDarkUserEnabled); } else { $isDarkThemeUserEnabled = false; $isBreezeDarkUserEnabled = false; } - $isDarkMode = ($isAccessibilityAppEnabled && $isDarkThemeUserEnabled) || ($isBreezeDarkAppEnabled && $isBreezeDarkUserEnabled); + $isDarkMode = ($isAccessibilityAppEnabled && $isDarkThemeUserEnabled) + || ($isBreezeDarkAppEnabled && $isBreezeDarkUserEnabled); $primaryColor = $this->theming->getColorPrimary(); $lightenPrimaryColor = $this->color->adjustBrightness($primaryColor, 0.2); @@ -125,25 +104,33 @@ class CssController extends Controller if ($isDarkMode) { $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); + $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); + $opacity = $this->config->getAppValueInt('dark-mode-background-color-opacity', '100'); + $backgroundOpacity = dechex($opacity * 255 / 100); } else { $backgroundColor = $this->config->getAppValue('background-color', $darkenPrimaryColor); $backgroundColorTo = $this->config->getAppValue('background-color-to', $darkenPrimaryColor); - $currentAppBackgroundColor = $this->config->getAppValue('current-app-background-color', $darkenPrimaryColor2); + $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'); - $backgroundOpacity = dechex($this->config->getAppValueInt('background-color-opacity', '100') * 255 / 100); + $opacity = $this->config->getAppValueInt('background-color-opacity', '100'); + $backgroundOpacity = dechex($opacity * 255 / 100); } $backgroundColor .= $backgroundOpacity; diff --git a/lib/Controller/JsController.php b/lib/Controller/JsController.php index c873826..8f06345 100644 --- a/lib/Controller/JsController.php +++ b/lib/Controller/JsController.php @@ -1,4 +1,5 @@ themingDefaults = $themingDefaults; - $this->user = OC::$server[IUserSession::class]->getUser(); + $this->user = \OC::$server[IUserSession::class]->getUser(); $this->config = $config; $this->l10nFactory = $l10nFactory; } - /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ + #[NoCSRFRequired] + #[NoAdminRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: '/js/script')] public function script(): TemplateResponse { $response = new TemplateResponse(Application::APP_ID, 'js/script', $this->getConfig(), 'blank'); @@ -81,11 +66,10 @@ class JsController extends Controller return $response; } - /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ + #[NoCSRFRequired] + #[NoAdminRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: '/js/config')] public function config(): JSONResponse { return new JSONResponse($this->getConfig()); @@ -99,10 +83,12 @@ class JsController extends Controller $useAvatar = $this->config->getAppValueBool('use-avatar', '0'); $isForced = $this->config->getAppValueBool('force', '0'); $addLogoLink = $this->config->getAppValueBool('add-logo-link', '1'); + $appsOrder = $this->config->getAppValueArray('apps-order', '[]'); $avatar = null; $settings = null; if ($this->user) { + $userAppsOrder = $this->config->getUserValueArray($this->user, 'apps-order', '[]'); $userTopMenuApps = $this->config->getUserValueArray($this->user, 'top-menu-apps', '[]'); $userTopSideMenuApps = $this->config->getUserValueArray($this->user, 'top-side-menu-apps', '[]'); @@ -114,6 +100,10 @@ class JsController extends Controller $topSideMenuApps = $userTopSideMenuApps; } + if (!empty($userAppsOrder) && !$isForced) { + $appsOrder = $userAppsOrder; + } + $userTargetBlankMode = $this->config->getUserValueInt($this->user, 'target-blank-mode', '1'); $userTargetBlankApps = $this->config->getUserValueArray($this->user, 'target-blank-apps', '[]'); @@ -121,10 +111,10 @@ class JsController extends Controller $targetBlankApps = $userTargetBlankApps; } - $isAvatarSet = OC::$server->getAvatarManager()->getAvatar($this->user->getUid())->exists(); + $isAvatarSet = \OC::$server->getAvatarManager()->getAvatar($this->user->getUid())->exists(); if ($useAvatar && $isAvatarSet) { - $avatar = OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [ + $avatar = \OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [ 'userId' => $this->user->getUid(), 'size' => 128, 'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0), @@ -132,13 +122,13 @@ class JsController extends Controller } if ($this->config->getAppValueBool('show-settings', '0')) { - $settingsNav = OC::$server->getNavigationManager()->getAll('settings'); + $settingsNav = \OC::$server->getNavigationManager()->getAll('settings'); if (isset($settingsNav['settings'])) { $settings = [ 'href' => $settingsNav['settings']['href'], 'name' => $settingsNav['settings']['name'], - 'avatar' => OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [ + 'avatar' => \OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [ 'userId' => $this->user->getUid(), 'size' => 32, 'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0), @@ -148,7 +138,7 @@ class JsController extends Controller } } - $indexUrl = OC::$server->getURLGenerator()->linkTo('', 'index.php'); + $indexUrl = \OC::$server->getURLGenerator()->linkTo('', 'index.php'); return [ 'opener-position' => $this->config->getAppValue('opener-position', 'before'), @@ -161,9 +151,14 @@ class JsController extends Controller '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', '[]'), + 'apps-order' => $appsOrder, 'avatar' => $avatar, 'top-menu-apps' => $topMenuApps, 'top-side-menu-apps' => $topSideMenuApps, + 'top-menu-mouse-over-hidden-label' => $this->config->getAppValueInt( + 'top-menu-mouse-over-hidden-label', + '0' + ), 'target-blank-apps' => $targetBlankApps, 'settings' => $settings, 'logo' => $this->themingDefaults->getLogo(), diff --git a/lib/Controller/NavController.php b/lib/Controller/NavController.php index 41aff0c..1d90849 100644 --- a/lib/Controller/NavController.php +++ b/lib/Controller/NavController.php @@ -1,4 +1,5 @@ config = $config; - $this->appRepository = $appRepository; - $this->categoryRepository = $categoryRepository; - $this->l10nFactory = $l10nFactory; - $this->router = $router; } - /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * - * @return JSONResponse - */ - public function items() + #[NoCSRFRequired] + #[NoAdminRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: '/nav/items')] + public function items(): JSONResponse { - $user = OC::$server[IUserSession::class]->getUser(); + $user = \OC::$server[IUserSession::class]->getUser(); $items = []; if (!$user) { @@ -93,11 +62,13 @@ class NavController extends Controller ]); } - $apps = $this->appRepository->getVisibleApps(); + $apps = $this->appRepository->getOrderedApps($user); $categoriesLabels = $this->categoryRepository->getOrderedCategories(); $hiddenApps = $this->config->getAppValueArray('big-menu-hidden-apps', '[]'); $isForced = $this->config->getAppValueBool('force', '0'); $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); + $topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]'); + $userTopSideMenuApps = $this->config->getUserValueArray($user, 'top-side-menu-apps', '[]'); $userTopMenuApps = $this->config->getUserValueArray($user, 'top-menu-apps', '[]'); $appsCategories = []; $categoriesAppsCount = []; @@ -106,12 +77,16 @@ class NavController extends Controller $topMenuApps = $userTopMenuApps; } - foreach ($apps as $app) { - if (in_array($app['id'], $topMenuApps)) { - continue; - } + if (!$isForced && !empty($userTopSideMenuApps)) { + $topSideMenuApps = $userTopSideMenuApps; + } - if (in_array($app['id'], $hiddenApps)) { + foreach ($apps as $app) { + $inTopMenuApps = in_array($app['id'], $topMenuApps); + $inTopSideMenuApps = in_array($app['id'], $topSideMenuApps); + $inHiddenApps = in_array($app['id'], $hiddenApps); + + if (($inTopMenuApps && !$inTopSideMenuApps) || $inHiddenApps) { continue; } @@ -178,20 +153,16 @@ class NavController extends Controller foreach ($items as $category => $value) { if (empty($items[$category]['apps'])) { unset($items[$category]); - } else { - uasort($items[$category]['apps'], function ($a, $b) { - return ($a['name'] < $b['name']) ? -1 : 1; - }); } } usort($items, function ($a, $b) use ($categoriesLabels) { foreach ($categoriesLabels as $key => $value) { - if ($a['categoryId'] === 'other') { + if ('other' === $a['categoryId']) { return -1; } - if ($b['categoryId'] === 'other') { + if ('other' === $b['categoryId']) { return 1; } diff --git a/lib/Controller/PersonalSettingController.php b/lib/Controller/PersonalSettingController.php index bdc9fca..482a7c6 100644 --- a/lib/Controller/PersonalSettingController.php +++ b/lib/Controller/PersonalSettingController.php @@ -1,4 +1,5 @@ config = $config; - $this->configProxy = $configProxy; - $this->userSession = $userSession; } - /** - * @NoAdminRequired - * @NoCSRFRequired - * - * @param mixed $name - * @param mixed $value - * - * @return Response - */ - public function valueSet($name, $value) + #[NoCSRFRequired] + #[NoAdminRequired] + #[FrontpageRoute(verb: 'POST', url: '/personalSetting/valueSet')] + public function valueSet($name, $value): array { $doSave = false; $user = $this->userSession->getUser(); @@ -97,7 +80,7 @@ class PersonalSettingController extends Controller } } - if (in_array($name, ['top-menu-apps', 'top-side-menu-apps'])) { + if (in_array($name, ['top-menu-apps', 'top-side-menu-apps', 'apps-order'])) { $doSave = true; $data = json_decode($value, true); diff --git a/lib/Service/AppRepository.php b/lib/Service/AppRepository.php index 258c5c5..3c64f7d 100644 --- a/lib/Service/AppRepository.php +++ b/lib/Service/AppRepository.php @@ -2,6 +2,13 @@ namespace OCA\SideMenu\Service; +use OC\User\User; +use OCA\SideMenu\AppInfo\Application; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\INavigationManager; +use OCP\IUserSession; use OCP\L10N\IFactory; /** @@ -11,47 +18,27 @@ use OCP\L10N\IFactory; */ class AppRepository { - /** - * @var \OC_App - */ - protected $ocApp; - - /** - * @var IFactory - */ - protected $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; + protected \OC_App $ocApp, + protected INavigationManager $navigationManager, + protected IFactory $l10nFactory, + protected ConfigProxy $config, + protected CategoryRepository $categoryRepository, + protected IEventDispatcher $dispatcher, + protected IUserSession $userSession, + ) { + $this->dispatcher->dispatchTyped(new BeforeTemplateRenderedEvent( + $this->userSession->isLoggedIn(), + new TemplateResponse(Application::APP_NAME, '') + )); } /** * Retrieves visibles apps. - * - * @return array */ - public function getVisibleApps() + public function getVisibleApps(): array { - $navigation = $this->ocApp->getNavigation(); + $navigation = $this->navigationManager->getAll(); $appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]'); $categories = $this->categoryRepository->getOrderedCategories(); $apps = $this->ocApp->listAllApps(); @@ -62,7 +49,7 @@ class AppRepository foreach ([$app['id'], $app['id'].'_index'] as $id) { if (isset($navigation[$id])) { - $app['name'] = $this->l10nFactory->get($id)->t($app['name']); + $app['name'] = $this->getAppName($app); $app['href'] = $navigation[$id]['href']; $app['icon'] = $navigation[$id]['icon']; @@ -75,17 +62,25 @@ class AppRepository if ('external_index' === substr($app['id'], 0, 14)) { $visibleApps[$app['id']] = [ 'id' => $app['id'], - 'name' => $this->l10nFactory->get($app['id'])->t($app['name']), + 'name' => $this->getAppName($app), 'href' => $app['href'], 'icon' => $app['icon'], 'category' => [ 'external_links', ], ]; + } elseif ('tables_application' === substr($app['id'], 0, 18)) { + $visibleApps[$app['id']] = [ + 'id' => $app['id'], + 'name' => $this->getAppName($app), + 'href' => $app['href'], + 'icon' => $app['icon'], + 'category' => [], + ]; } elseif ('files' === $app['id']) { $visibleApps[$app['id']] = [ 'id' => $app['id'], - 'name' => $this->l10nFactory->get($app['id'])->t($app['name']), + 'name' => $this->getAppName($app), 'href' => $app['href'], 'icon' => $app['icon'], 'category' => [], @@ -99,10 +94,42 @@ class AppRepository } } - usort($visibleApps, function ($a, $b) { - return ($a['name'] < $b['name']) ? -1 : 1; - }); - return $visibleApps; } + + public function getAppName($app): string + { + return $this->config->getAppValue( + 'app.navigation.name', + $this->l10nFactory->get($app['id'])->t($app['name']), + $app['id'] + ); + } + + public function getOrderedApps(?User $user = null): array + { + $apps = $this->getVisibleApps(); + $orders = $this->config->getAppValueArray('apps-order', '[]'); + + if (null !== $user && !$this->config->getAppValueBool('force', '0')) { + $userOrders = $this->config->getUserValueArray($user, 'apps-order', '[]'); + + if (!empty($userOrders)) { + $orders = $userOrders; + } + } + + usort($apps, function ($a, $b) use ($orders) { + $ak = array_keys($orders, $a['id'])[0] ?? null; + $bk = array_keys($orders, $b['id'])[0] ?? null; + + if (null === $ak || null === $bk) { + return ($a['name'] < $b['name']) ? -1 : 1; + } + + return $ak < $bk ? -1 : 1; + }); + + return $apps; + } } diff --git a/lib/Service/CategoryRepository.php b/lib/Service/CategoryRepository.php index 7559854..81e44e2 100644 --- a/lib/Service/CategoryRepository.php +++ b/lib/Service/CategoryRepository.php @@ -15,51 +15,18 @@ use OCP\L10N\IFactory; */ class CategoryRepository { - /** - * @var CategoryFetcher - */ - protected $categoryFetcher; - - /** - * @var IFactory - */ - protected $l10nFactory; - - /** - * @var ConfigProxy - */ - protected $config; - - /** - * @var IConfig - */ - protected $iConfig; - - /** - * @var IUserSession - */ - protected $userSession; - public function __construct( - CategoryFetcher $categoryFetcher, - ConfigProxy $config, - IConfig $iConfig, - IFactory $l10nFactory, - IUserSession $userSession - ) { - $this->categoryFetcher = $categoryFetcher; - $this->l10nFactory = $l10nFactory; - $this->config = $config; - $this->iConfig = $iConfig; - $this->userSession = $userSession; - } + protected CategoryFetcher $categoryFetcher, + protected ConfigProxy $config, + protected IConfig $iConfig, + protected IFactory $l10nFactory, + protected IUserSession $userSession + ) {} /** * Retrieves categories. - * - * @return array */ - public function getOrderedCategories() + public function getOrderedCategories(): array { $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); $type = $this->config->getAppValue('categories-order-type', 'default'); @@ -74,7 +41,8 @@ class CategoryRepository } foreach ($categoriesLabels as $k => $category) { - $categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name']; + $categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ?? + $category['translations']['en']['name']; unset($categoriesLabels[$k]); } diff --git a/lib/Service/ConfigProxy.php b/lib/Service/ConfigProxy.php index d162dfd..6745851 100644 --- a/lib/Service/ConfigProxy.php +++ b/lib/Service/ConfigProxy.php @@ -13,12 +13,7 @@ use OCP\IConfig; */ class ConfigProxy { - /** - * @var IConfig - */ - protected $config; - - public function __construct(IConfig $config) + public function __construct(protected IConfig $config) { $this->config = $config; } diff --git a/lib/Service/LangRepository.php b/lib/Service/LangRepository.php index 3c379f0..1cde40a 100644 --- a/lib/Service/LangRepository.php +++ b/lib/Service/LangRepository.php @@ -11,12 +11,7 @@ use OCP\IDBConnection; */ class LangRepository { - /** - * @var IDBConnection - */ - protected $db; - - public function __construct(IDBConnection $db) + public function __construct(protected IDBConnection $db) { $this->db = $db; } diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index a68d30c..7796f72 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -1,4 +1,5 @@ l = $l; - $this->logger = $logger; - $this->config = $config; - $this->appRepository = $appRepository; - $this->categoryRepository = $categoryRepository; - $this->theming = $theming; - $this->color = $color; - $this->langRepository = $langRepository; - } + protected IL10N $l, + protected ConfigProxy $config, + protected AppRepository $appRepository, + protected CategoryRepository $categoryRepository, + protected ThemingDefaults $theming, + protected Color $color, + protected LangRepository $langRepository + ) {} /** * @return TemplateResponse @@ -125,15 +75,24 @@ class Admin implements ISettings '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', $darkenPrimaryColor2), + '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', $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', $darkenPrimaryColor2), + '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', + $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'), @@ -159,6 +118,12 @@ 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-menu-mouse-over-hidden-label' => $this->config->getAppValue( + 'top-menu-mouse-over-hidden-label', + '0' + ), + 'apps-order' => $this->config->getAppValueArray('apps-order', '[]'), + 'ordered-apps' => $this->appRepository->getOrderedApps(), 'top-side-menu-apps' => $this->config->getAppValueArray('top-side-menu-apps', '[]'), 'default-enabled' => $this->config->getAppValue('default-enabled', '1'), 'apps' => $this->appRepository->getVisibleApps(), diff --git a/lib/Settings/AdminSection.php b/lib/Settings/AdminSection.php index 8629e8a..67c8fcc 100644 --- a/lib/Settings/AdminSection.php +++ b/lib/Settings/AdminSection.php @@ -1,4 +1,5 @@ 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 Application::APP_ID; } - /** - * 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(Application::APP_NAME); } - /** - * @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 70; } - /** - * {@inheritdoc} - */ public function getIcon() { return $this->url->imagePath(Application::APP_ID, 'icon.svg'); diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 9ac24c7..2aebec8 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -1,4 +1,5 @@ l = $l; - $this->logger = $logger; - $this->config = $config; - $this->userSession = $userSession; - $this->appRepository = $appRepository; - } + public function __construct( + protected IL10N $l, + protected ConfigProxy $config, + protected IUserSession $userSession, + protected AppRepository $appRepository + ) {} /** * @return TemplateResponse @@ -81,7 +54,9 @@ class Personal implements ISettings '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-order' => $this->config->getUserValueArray($user, 'apps-order', '[]'), 'apps' => $this->appRepository->getVisibleApps(), + 'ordered-apps' => $this->appRepository->getOrderedApps($user), ]; return new TemplateResponse(Application::APP_ID, 'settings/personal-form', $parameters, ''); diff --git a/lib/Settings/PersonalSection.php b/lib/Settings/PersonalSection.php index 58a773f..efe180c 100644 --- a/lib/Settings/PersonalSection.php +++ b/lib/Settings/PersonalSection.php @@ -1,4 +1,5 @@ url = $url; - $this->l = $l; - $this->configProxy = $configProxy; - } - - /** - * returns the ID of the section. It is supposed to be a lower case string, - * e.g. 'ldap'. - * - * @returns string - */ public function getID() { if ($this->configProxy->getAppValueBool('force', '0')) { @@ -63,12 +42,6 @@ class PersonalSection implements IIconSection return Application::APP_ID; } - /** - * 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() { if ($this->configProxy->getAppValueBool('force', '0')) { @@ -78,13 +51,6 @@ class PersonalSection implements IIconSection return $this->l->t(Application::APP_NAME); } - /** - * @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() { if ($this->configProxy->getAppValueBool('force', '0')) { @@ -94,9 +60,6 @@ class PersonalSection implements IIconSection return 70; } - /** - * {@inheritdoc} - */ public function getIcon() { return $this->url->imagePath(Application::APP_ID, 'icon.svg'); diff --git a/package.json b/package.json index fb7e755..f002d50 100644 --- a/package.json +++ b/package.json @@ -2,20 +2,24 @@ "license": "agpl", "private": true, "scripts": { - "build": "NODE_ENV=production webpack --progress --config webpack.js", - "dev": "NODE_ENV=development webpack --progress --config webpack.js", - "watch": "NODE_ENV=development webpack --progress --watch --config webpack.js", - "lint": "eslint --ext .js,.vue src", - "lint:fix": "eslint --ext .js,.vue src --fix", - "stylelint": "stylelint src", - "stylelint:fix": "stylelint src --fix" + "build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.js", + "dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.js", + "watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.js", + "lint": "./node_modules/.bin/eslint --ext .js,.vue src", + "lint:fix": "./node_modules/.bin/eslint --ext .js,.vue src --fix", + "stylelint": "./node_modules/.bin/stylelint src", + "stylelint:fix": "./node_modules/.bin/stylelint src --fix" }, "dependencies": { - "@nextcloud/axios": "^1.8.0", - "@nextcloud/vue": "^1.5.0", - "axios": "^0.24.0", - "trim": "^1.0.1", - "vue": "^2.6.11" + "@nextcloud/axios": "^2.5.1", + "@nextcloud/browserslist-config": "^3.0.1", + "@nextcloud/event-bus": "^3.3.1", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^3.1.0", + "@nextcloud/vue": "^8.19.0", + "@vueuse/core": "^11.1.0", + "axios": "^1.6.7", + "trim": "^1.0.1" }, "browserslist": [ "extends @nextcloud/browserslist-config" @@ -24,37 +28,46 @@ "node": ">=16.0.0" }, "devDependencies": { - "@babel/core": "^7.9.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.9.0", - "@nextcloud/browserslist-config": "^1.0.0", - "@nextcloud/eslint-config": "^8.1.2", - "babel-eslint": "^10.1.0", - "babel-loader": "^8.1.0", - "css-loader": "^3.4.2", - "eslint": "^8.0.0", - "eslint-config-standard": "^17.0.0", - "eslint-import-resolver-webpack": "^0.12.1", - "eslint-plugin-import": "^2.20.0", - "eslint-plugin-nextcloud": "^0.3.0", - "eslint-plugin-node": "^10.0.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-standard": "^4.0.1", - "eslint-plugin-vue": "^9.0.0", - "eslint-webpack-plugin": "^3.0.0", - "file-loader": "^6.0.0", - "sass": "^1.49.9", - "sass-loader": "^13.0.2", - "stylelint": "^14.0.0", - "stylelint-config-recommended-scss": "^7.0.0", - "stylelint-scss": "^4.0.0", - "stylelint-webpack-plugin": "^3.3.0", - "url-loader": "^4.0.0", - "vue-loader": "^15.9.1", - "vue-template-compiler": "^2.6.11", - "webpack": "^5.0.0", - "webpack-cli": "^4.0.0", - "webpack-merge": "^4.2.2", - "webpack-node-externals": "^1.7.2" + "@babel/node": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/preset-typescript": "^7.24.7", + "@cypress/vue2": "^2.1.1", + "@cypress/webpack-preprocessor": "^6.0.2", + "@nextcloud/babel-config": "^1.2.0", + "@nextcloud/eslint-config": "^8.4.1", + "@nextcloud/stylelint-config": "^3.0.1", + "@nextcloud/typings": "^1.9.1", + "@nextcloud/webpack-vue-config": "^6.0.1", + "@simplewebauthn/types": "^10.0.0", + "@types/dockerode": "^3.3.29", + "@types/wait-on": "^5.3.4", + "@vue/tsconfig": "^0.5.1", + "babel-loader": "^9.2.1", + "babel-loader-exclude-node-modules-except": "^1.2.1", + "babel-plugin-module-resolver": "^5.0.2", + "colord": "^2.9.3", + "eslint-plugin-cypress": "^3.5.0", + "eslint-plugin-es": "^4.1.0", + "exports-loader": "^5.0.0", + "file-loader": "^6.2.0", + "handlebars-loader": "^1.7.3", + "jasmine-core": "~2.5.2", + "jasmine-sinon": "^0.4.0", + "jsdoc": "^4.0.2", + "raw-loader": "^4.0.2", + "sass": "^1.79.3", + "stylelint": "^16.9.0", + "stylelint-use-logical": "^2.1.2", + "ts-loader": "^9.5.0", + "ts-node": "^10.9.1", + "tslib": "^2.7.0", + "typescript": "^5.6.2", + "vue-loader": "^15.9.8", + "vue-template-compiler": "^2.7.16", + "wait-on": "^8.0.1", + "webpack": "^5.94.0", + "webpack-cli": "^5.0.2", + "webpack-merge": "^6.0.1", + "workbox-webpack-plugin": "^7.1.0" } } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} diff --git a/screenshots/admin_settings.png b/screenshots/admin_settings.png index ba7a744..4d7a0e0 100644 Binary files a/screenshots/admin_settings.png and b/screenshots/admin_settings.png differ diff --git a/screenshots/nc25_big_menu.png b/screenshots/nc25_big_menu.png new file mode 100644 index 0000000..915e5f4 Binary files /dev/null and b/screenshots/nc25_big_menu.png differ diff --git a/screenshots/nc25_default_menu.png b/screenshots/nc25_default_menu.png new file mode 100644 index 0000000..edd8ff5 Binary files /dev/null and b/screenshots/nc25_default_menu.png differ diff --git a/screenshots/personal_settings.png b/screenshots/personal_settings.png index ba62403..05a73ee 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 index ca5272a..21de363 100644 --- a/src/AdminCategoriesCustom.vue +++ b/src/AdminCategoriesCustom.vue @@ -15,168 +15,167 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> + + diff --git a/src/AppMenu.vue b/src/AppMenu.vue new file mode 100644 index 0000000..9558eb5 --- /dev/null +++ b/src/AppMenu.vue @@ -0,0 +1,393 @@ + + + + + + + + diff --git a/src/AppSearch.vue b/src/AppSearch.vue new file mode 100644 index 0000000..b2679a2 --- /dev/null +++ b/src/AppSearch.vue @@ -0,0 +1,32 @@ + + + + diff --git a/src/CloserButton.vue b/src/CloserButton.vue index f14e39b..80ed747 100644 --- a/src/CloserButton.vue +++ b/src/CloserButton.vue @@ -15,11 +15,18 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/Loader.vue b/src/Loader.vue index 7dbdb1d..d059f68 100644 --- a/src/Loader.vue +++ b/src/Loader.vue @@ -15,28 +15,28 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/Logo.vue b/src/Logo.vue index 0e76b5b..1aec19f 100644 --- a/src/Logo.vue +++ b/src/Logo.vue @@ -15,30 +15,30 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/OpenerButton.vue b/src/OpenerButton.vue index 7a134e8..538291c 100644 --- a/src/OpenerButton.vue +++ b/src/OpenerButton.vue @@ -15,11 +15,18 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/PageLoader.js b/src/PageLoader.js new file mode 100644 index 0000000..3c8bd13 --- /dev/null +++ b/src/PageLoader.js @@ -0,0 +1,20 @@ +const createElement = require('./lib/createElement') + +const PageLoader = () => { + const pageLoader = createElement('div', {id: 'side-menu-loader'}) + const pageLoaderBar = createElement('div', {id: 'side-menu-loader-bar'}) + + pageLoader.appendChild(pageLoaderBar) + document.querySelector('body').appendChild(pageLoader) + + let pageLoaderValue = 0 + + window.addEventListener('beforeunload', () => { + setInterval(() => { + pageLoaderBar.style.width = pageLoaderValue.toString() + '%' + pageLoaderValue = Math.min(pageLoaderValue + .2, 100) + }, 25) + }) +} + +module.exports = PageLoader diff --git a/src/SettingsButton.vue b/src/SettingsButton.vue index 5f18927..b8cb624 100644 --- a/src/SettingsButton.vue +++ b/src/SettingsButton.vue @@ -15,33 +15,35 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/SideMenu.js b/src/SideMenu.js index a9848c4..22ac3f5 100644 --- a/src/SideMenu.js +++ b/src/SideMenu.js @@ -16,35 +16,54 @@ */ import Vue from 'vue' +import AppMenu from './AppMenu.vue' import SideMenu from './SideMenu.vue' import SideMenuBig from './SideMenuBig.vue' import SideMenuWithCategories from './SideMenuWithCategories.vue' +import PageLoader from './PageLoader' Vue.prototype.OC = OC +Vue.prototype.t = OC.L10N.translate + +window.PageLoader = PageLoader const mountSideMenuComponent = () => { - const sideMenuContainer = document.querySelector('#side-menu') + const container = document.querySelector('#side-menu') - if (sideMenuContainer) { - let component + if (!container) { + return window.setTimeout(mountSideMenuComponent, 50) + } - if (sideMenuContainer.getAttribute('data-bigmenu')) { - component = SideMenuBig - } else if(sideMenuContainer.getAttribute('data-sidewithcategories')) { - component = SideMenuWithCategories - } else { - component = SideMenu - } - - const View = Vue.extend(component) - const sideMenu = new View({}) - - sideMenu.$mount('#side-menu') - - document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.ready')) + const component = (() => { + if (container.getAttribute('data-bigmenu')) { + return SideMenuBig + } else if(container.getAttribute('data-sidewithcategories')) { + return SideMenuWithCategories } else { - window.setTimeout(mountSideMenuComponent, 50) + return SideMenu } + })() + + const View = Vue.extend(component) + const App = new View({}) + + App.$mount('#side-menu') + + document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.ready')) +} + +const mountAppMenu = () => { + const container = document.querySelector('#header .app-menu') + + if (!container) { + return window.setTimeout(mountAppMenu, 50) + } + + const View = Vue.extend(AppMenu) + const App = new View({}) + + App.$mount('#header .app-menu') } mountSideMenuComponent() +mountAppMenu() diff --git a/src/SideMenu.vue b/src/SideMenu.vue index b369f87..00bd3d1 100644 --- a/src/SideMenu.vue +++ b/src/SideMenu.vue @@ -15,154 +15,156 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/SideMenuApp.vue b/src/SideMenuApp.vue index 48e2a60..c910dce 100644 --- a/src/SideMenuApp.vue +++ b/src/SideMenuApp.vue @@ -15,38 +15,38 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/SideMenuBig.vue b/src/SideMenuBig.vue index d1f0b1f..f9c0783 100644 --- a/src/SideMenuBig.vue +++ b/src/SideMenuBig.vue @@ -15,41 +15,42 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/SideMenuBigApp.vue b/src/SideMenuBigApp.vue index bab78e6..a0fcdde 100644 --- a/src/SideMenuBigApp.vue +++ b/src/SideMenuBigApp.vue @@ -15,38 +15,38 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/SideMenuWithCategories.vue b/src/SideMenuWithCategories.vue index e0a3c41..07530a8 100644 --- a/src/SideMenuWithCategories.vue +++ b/src/SideMenuWithCategories.vue @@ -15,113 +15,138 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> diff --git a/src/admin.js b/src/admin.js index 9231c9a..66fb71d 100644 --- a/src/admin.js +++ b/src/admin.js @@ -26,235 +26,257 @@ let elements = [] const selector = '#side-menu-message' const userConfig = (name, value, callbacks) => { - const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet') - const formData = [] + const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet') + const formData = [] - formData.push('name=' + encodeURIComponent(name)) - formData.push('value=' + encodeURIComponent(value)) + 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) + 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) => { - OCP.AppConfig.setValue('side_menu', name, value, callbacks) + OCP.AppConfig.setValue('side_menu', name, value, callbacks) } const saveSettings = (key) => { - const element = elements[key] + const element = elements[key] - if (!element) { - return + if (!element) { + return + } + + let value + let name + + if (element.hasAttribute('data-checkbox')) { + name = element.getAttribute('data-name') + value = [] + + const inputs = document.querySelectorAll('input[name="' + name + '[]"]:checked') + + for (let input of inputs) { + value.push(input.value) } - let value - let name + value = JSON.stringify(value) + } else { + name = element.getAttribute('name') + value = element.value + } - if (element.hasAttribute('data-checkbox')) { - name = element.getAttribute('data-name') - value = [] + const size = elements.length - const inputs = document.querySelectorAll('input[name="' + name + '[]"]:checked') + if (name === 'cache') { + ++value + } - for (let input of inputs) { - value.push(input.value) - } + const progress = document.querySelector('#side-menu-save-progress') - value = JSON.stringify(value) - } else { - name = element.getAttribute('name') - value = element.value + progress.style.width = '40px'; + progress.style.marginLeft = '5px'; + + const callbacks = { + success: () => { + const percent = parseInt((key + 1) * 100 / size); + + progress.setAttribute('value', percent) + + if (key < size - 1) { + saveSettings(key + 1) + } else { + location.reload() + } + }, + error: () => { + OC.msg.finishedError(selector, t('side_menu', 'Error while saving "' + element + '"')) } + } - const size = elements.length - - if (name === 'cache') { - ++value - } - - const progress = document.querySelector('#side-menu-save-progress') - - progress.style.width = '40px'; - progress.style.marginLeft = '5px'; - - const callbacks = { - success: () => { - const percent = parseInt((key + 1) * 100 / size); - - progress.setAttribute('value', percent) - - if (key < size - 1) { - saveSettings(key + 1) - } else { - location.reload() - } - }, - error: () => { - OC.msg.finishedError(selector, t('side_menu', 'Error while saving "' + element + '"')) - } - } - - if (element.hasAttribute('data-personal')) { - userConfig(name, value, callbacks) - } else { - appConfig(name, value, callbacks) - } + if (element.hasAttribute('data-personal')) { + userConfig(name, value, callbacks) + } else { + appConfig(name, value, callbacks) + } } const elementToggler = (element) => { - let display = 'none' + let display = 'none' - if (window.getComputedStyle(element).display === 'none') { - display = 'block' - } + if (window.getComputedStyle(element).display === 'none') { + display = 'block' + } - element.style.display = display + element.style.display = display } const updateAppsCategoriesCustom = () => { - let values = {} + let values = {} - for (let item of document.querySelectorAll('.apps-categories-custom')) { - let app = item.getAttribute('data-app') - let value = item.value + for (let item of document.querySelectorAll('.apps-categories-custom')) { + let app = item.getAttribute('data-app') + let value = item.value - if (value) { - values[app] = value - } + if (value) { + values[app] = value } + } - document.querySelector('#apps-categories-custom').value = JSON.stringify(values) + document.querySelector('#apps-categories-custom').value = JSON.stringify(values) } document.addEventListener('DOMContentLoaded', () => { - $('*[data-toggle="tooltip"]').tooltip(); + $('*[data-toggle="tooltip"]').tooltip(); - if (document.querySelector('#side-menu-categories-custom')) { - const View = Vue.extend(AdminCategoriesCustom) - const adminCategoriesCustom = new View({}) + if (document.querySelector('#side-menu-categories-custom')) { + const View = Vue.extend(AdminCategoriesCustom) + const adminCategoriesCustom = new View({}) - adminCategoriesCustom.$mount('#side-menu-categories-custom') - } + adminCategoriesCustom.$mount('#side-menu-categories-custom') + } - elements = document.querySelectorAll('.side-menu-setting') + elements = document.querySelectorAll('.side-menu-setting') - document.querySelector('#side-menu-save').addEventListener('click', (event) => { - event.preventDefault() - OC.msg.startSaving(selector) + document.querySelector('#side-menu-save').addEventListener('click', (event) => { + event.preventDefault() + OC.msg.startSaving(selector) - saveSettings(0) + saveSettings(0) + }) + + const resets = document.querySelectorAll('.btn-reset') + + for (let btn of resets) { + btn.addEventListener('click', (event) => { + const target = event.target + const values = JSON.parse(target.getAttribute('data-reset')) + + target.classList.toggle('btn-reset--progress', true) + + for (let i in values) { + document.querySelector(`#${i}`).value = values[i] + } + + window.setTimeout(() => { + target.classList.toggle('btn-reset--progress', false) + }, 800) }) + } - const resets = document.querySelectorAll('.btn-reset') + const displays = document.querySelectorAll('.side-menu-display') - for (let btn of resets) { - btn.addEventListener('click', (event) => { - const target = event.target - const values = JSON.parse(target.getAttribute('data-reset')) + for (let display of displays) { + display.addEventListener('click', (event) => { + const target = event.target - for (let i in values) { - document.querySelector(`#${i}`).value = values[i] - } - }) - } + for (let d of displays) { + d.classList.toggle('is-active', d === display) + } - const displays = document.querySelectorAll('.side-menu-display') - - for (let display of displays) { - display.addEventListener('click', (event) => { - const target = event.target - - for (let d of displays) { - d.classList.toggle('is-active', d === display) - } - - 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') - }) - } - - for (let item of document.querySelectorAll('.apps-categories-custom')) { - item.addEventListener('change', (event) => { - updateAppsCategoriesCustom() - }) - } - - 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' + 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') }) + } - try { - sortable('#categories-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => { - let value = [] + for (let item of document.querySelectorAll('.apps-categories-custom')) { + item.addEventListener('change', (event) => { + updateAppsCategoriesCustom() + }) + } - 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')) - } + for (let item of document.querySelectorAll('.side-menu-setting-live')) { + item.addEventListener('change', (event) => { + const target = event.target + const name = target.getAttribute('name') - document.querySelector('input[name="categories-order"]').value = JSON.stringify(value) - }) - } catch (e) { - } + 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' + }) + + try { + sortable('#categories-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => { + let value = [] + + for (let item of document.querySelectorAll('#categories-list .side-menu-setting-list-item')) { + value.push(item.getAttribute('data-id')) + } + + document.querySelector('input[name="categories-order"]').value = JSON.stringify(value) + }) + } catch (e) { + } + + sortable('#apps-order-list .side-menu-setting-list', { + placeholderClass: 'side-menu-setting-list-drop' + }) + + try { + sortable('#apps-order-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => { + let value = [] + + for (let item of document.querySelectorAll('#apps-order-list .side-menu-setting-list-item')) { + value.push(item.getAttribute('data-id')) + } + + document.querySelector('input[name="apps-order"]').value = JSON.stringify(value) + }) + } catch (e) { + } }) diff --git a/src/l10n/fixtures/cs.yaml b/src/l10n/fixtures/cs.yaml index 01e862f..e96e546 100644 --- a/src/l10n/fixtures/cs.yaml +++ b/src/l10n/fixtures/cs.yaml @@ -3,7 +3,8 @@ "No": "Ne" "Yes": "Ano" "Menu": "Nabídka" -'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.': 'Pro otevření/skrytí postranní nabídky použijte zkratku Ctrl+O („O“ jako otevřít). Pro pohyb po použijte klávesu Tab.' +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Pro otevření/skrytí postranní nabídky použijte zkratku Ctrl+O („O“ jako otevřít). Pro pohyb po použijte klávesu Tab.' "Top menu": "Horní nabídka" "Apps that not must be moved in the side menu": "Aplikace, které nepřesouvat do postranní nabídky" "If there is no selection then the global configuration is applied.": "Pokud neexistuje žádný výběr, je uplatněno globální nastavení." @@ -38,16 +39,13 @@ "Show only the opener (hidden logo)": "Zobrazovat pouze otevírací tlačítko (logo skryto)" "Do not display the side menu and the opener if there is no application (eg: public pages).": "Nezobrazovat postranní nabídku a její otevírací tlačítko pokud nejsou dostupné žádné aplikace (např. na veřejných stránkách)." "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)." +"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" -"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" "Loader enabled": "Načítání zapnuto" "Tips": "Tipy" "Always displayed": "Vždy zobrazeno" -"The logo will be hidden when the menu is always displayed.": "Pokud je nabídka zobrazena trvale, logo bude skryto." "This is the automatic behavior when the menu is always displayed.": "Toto je automatické chování, kdy je nabídka vždy zobrazena." "Not compatible with touch screens.": "Nekompatibilní s dotykovými obrazovkami." "Big menu": "Velká nabídka" @@ -65,7 +63,7 @@ "The menu is enabled by default for users": "Nabídka je ve výchozím stavu pro uživatele zapnutá" "Except when the configuration is forced.": "S výjimkou, kdy je nastavení vynuceno." "Apps that should not be displayed in the menu": "Aplikace, které by neměly být v nabídce zobrazeny" -"This feature is only compatible with the big menu display.": "Tato funkce je kompatibilní pouze s velkou nabídkou." +"This feature is only compatible with the big menu display.": "Tato funkce je kompatibilní pouze s velkou nabídkou." "The logo is a link to the default app": "Logo je odkaz na výchozí aplikaci" "Others": "Ostatní" "Categories": "Kategorie" @@ -79,8 +77,6 @@ "With categories": "S kategoriemi" "Custom categories": "Vlastní kategorie" "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" @@ -90,3 +86,11 @@ "Small text": "Malý text" "Normal text": "Normální text" "Big text": "Velký text" +"Applications": "Aplikace" +"Applications kept in the top menu": "Aplikace ponechané v horní nabídce" +"Applications kept in the top menu but also shown in side menu": "Aplikace ponechané v horní nabídce ale také zobrazené v té boční" +"These applications must be selected in the previous option.": "Tyto aplikace je třeba vybrat v předchozí volbě." +"Hide labels on mouse over": "Skrýt popisky při najetím ukazatele myši" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/de.yaml b/src/l10n/fixtures/de.yaml index afffdbb..62ceae2 100644 --- a/src/l10n/fixtures/de.yaml +++ b/src/l10n/fixtures/de.yaml @@ -3,13 +3,14 @@ "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.' +? '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": "Obere Navigationsleiste" -"Apps that not must be moved in the side menu": "Anwendungen, die nicht ins Seitenmenü verschoben werden sollen" +"Apps that not must be moved in the side menu": "Apps, 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 Anwendung und möchtest mich unterstützen?" +"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 aus ☕" "Hidden": "Ausblenden" "Small": "Klein" @@ -17,9 +18,9 @@ "Big": "Groß" "Colors": "Farben" "Background color": "Hintergrundfarbe" -"Background color of current app": "Hintergrundfarbe der aktuellen Anwendung" +"Background color of current app": "Hintergrundfarbe der aktuellen App" "Text color": "Textfarbe" -"Loader": "Ladestandanzeige" +"Loader": "Fortschrittsbalken" "Icon": "Symbol" "Same color": "Selbe Farbe" "Opposite color": "Gegenfarbe" @@ -36,35 +37,32 @@ "After the logo": "Nach dem Logo" "Position": "Position" "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)." +"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 App 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 das Menü-Symbol 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" -"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 dem großen Menü kompatibel." "Icons and texts": "Symbole und Texte" -"Loader enabled": "Ladestandanzeige aktiviert" +"Loader enabled": "Fortschrittsbalken anzeigen" "Tips": "Tipps" "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 Anwendungen in einem neuen Tab" +"Open apps in new tab": "Öffne Apps 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" +"Show and hide the list of applications": "Ein- und Ausblenden der Appliste" "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." +"You do not have permission to change the settings.": "Du hast keine Berechtigung, die Einstellungen dieser App 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" +"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 dem großen Menü." "The logo is a link to the default app": "Das Logo ist ein Link zur Standard-App" "Others": "Andere" @@ -78,15 +76,21 @@ "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" +"Customize application categories": "App-Kategorien anpassen" "Reset to default": "Auf Standard zurücksetzen" "Hidden icon": "Verstecktes Symbol" "Small icon": "Kleines Symbol" "Normal icon": "Normales Symbol" -"Big icon": "Große Ikone" +"Big icon": "Großes Icon" "Hidden text": "Versteckter Text" "Small text": "Kleiner Text" "Normal text": "Normaler Text" "Big text": "Großer Text" +"Applications": "Apps" +"Applications kept in the top menu": "Apps in der oberen Navigationsleiste" +"Applications kept in the top menu but also shown in side menu": "Apps in der oberen Navigationsleiste, die auch im Seitenmenü angezeigt werden sollen" +"These applications must be selected in the previous option.": "Diese Apps müssen auch in der vorherigen Einstellung ausgewählt werden." +"Hide labels on mouse over": "Labels ausblenden, wenn sich die Maus darüber befindet (Hover)" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/es.yaml b/src/l10n/fixtures/es.yaml new file mode 100644 index 0000000..3d0aae1 --- /dev/null +++ b/src/l10n/fixtures/es.yaml @@ -0,0 +1,96 @@ +"Custom menu": "Menú personalizado" +"Enable the custom menu": "Habilitar el menú personalizado" +"No": "No" +"Yes": "Sí" +"Menu": "Menú" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Usa la combinación de teclas Ctrl+o para activar y desactivar el menú lateral. Use tab para navegar.' +"Top menu": "Menu principal" +"Apps that not must be moved in the side menu": "Aplicaciones que no se deben mover al menú lateral" +"If there is no selection then the global configuration is applied.": "Si no hay selección, se aplica la configuración global." +"Experimental": "En pruebas" +"Save": "Guardar" +"You like this app and you want to support me?": "¿Te gusta esta aplicación y quieres apoyarme?" +"Buy me a coffee ☕": "Cómprame un café ☕" +"Hidden": "Oculto" +"Small": "Pequeño" +"Normal": "Normal" +"Big": "Grande" +"Hidden icon": "Ocultar Icono" +"Small icon": "Icono pequeño" +"Normal icon": "Icono normal" +"Big icon": "Icono grande" +"Hidden text": "Texto oculto" +"Small text": "Texto pequeño" +"Normal text": "Texto normal" +"Big text": "Texto grande" +"Colors": "Colores" +"Background color": "Color de fondo" +"Background color of current app": "Color de fondo de la aplicación actual" +"Text color": "Color del texto" +"Loader": "Cargador" +"Icon": "Icono" +"Same color": "El mismo color" +"Opposite color": "Color opuesto" +"Transparent": "Transparente" +"Opaque": "Opaco" +"Opener": "Abrir" +"Default": "Por defecto" +"Default (dark)": "Por defecto (oscuro)" +"Hamburger": "Hamburguesa" +"Hamburger (dark)": "Hamburger (negro)" +"Hamburger 2": "Hamburguesa 2" +"Hamburger 2 (dark)": "Hamburger 2 (negro)" +"Before the logo": "Antes del logotipo" +"After the logo": "Después del logotipo" +"Position": "Posición" +"Show only the opener (hidden logo)": "Mostrar solo abrir (ocultar logotipo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "No mostrar el menú lateral y el abridor si no hay aplicación (por ejemplo: páginas públicas)." +"Panel": "Panel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Abra el menú cuando el ratón esté sobre el icono (se desactiva automáticamente en las pantallas táctiles)" +"Display the big menu": "Mostrar el menú grande" +"Display the logo": "Mostrar el logotipo" +"Icons and texts": "Iconos y textos" +"Loader enabled": "Cargador activado" +"Tips": "Consejos" +"Always displayed": "Siempre se muestra" +"This is the automatic behavior when the menu is always displayed.": "Este es el comportamiento automático cuando aún se muestra el menú." +"Not compatible with touch screens.": "No es compatible con las pantallas táctiles." +"Big menu": "Menú grande" +"Live preview": "Previsualización en directo" +"Open apps in new tab": "Abrir las aplicaciones en una nueva pestaña" +"Use the global setting": "Utilizar la configuración global" +"Use my selection": "Utilizar mi selección" +"Show and hide the list of applications": "Mostrar y ocultar la lista de aplicaciones" +"Use the avatar instead of the logo": "Utilizar un avatar en lugar de un logotipo" +"You do not have permission to change the settings.": "No tienes permiso para cambiar la configuración." +"Force this configuration to users": "Forzar esta configuración a todos los usuarios" +"Export the configuration": "Exportar la configuración" +"Purge the cache": "Vaciar la caché" +"Show the link to settings": "Mostrar un enlace a la configuración" +"The menu is enabled by default for users": "El menú está activado por defecto para los usuarios" +"Except when the configuration is forced.": "Excepto cuando la configuración es forzada." +"Apps that should not be displayed in the menu": "Aplicaciones que no deben aparecer en el menú" +"This feature is only compatible with the big menu display.": "Esta función sólo es compatible con la pantalla del menú grande." +"The logo is a link to the default app": "El logotipo es un enlace a la aplicación por defecto" +"Others": "Otros" +"Categories": "Categorías" +"Customize sorting": "Personalizar la clasificación" +"Order by": "Ordenar por" +"Name": "Nombre" +"Customed": "Personalizado" +"Show and hide the list of categories": "Mostrar y ocultar la lista de categorías" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Estos parámetros se utilizan cuando el tema oscuro o el tema oscuro de Breeze están activados." +"Dark mode colors": "Colores del modo oscuro" +"With categories": "Con categorías" +"Custom categories": "Categorías personalizadas" +"Customize application categories": "Personalizar las categorías de las aplicaciones" +"Reset to default": "Restablecer los valores por defecto" +"Applications": "Aplicaciones" +"Applications kept in the top menu": "Aplicaciones guardadas en el menú superior" +"Applications kept in the top menu but also shown in side menu": "Las aplicaciones se mantienen en el menú superior pero también se muestran en el menú lateral" +"These applications must be selected in the previous option.": "Estas aplicaciones deben ser seleccionadas en las opciones anteriores." +"Hide labels on mouse over": "Ocultar las etiquetas al pasar el ratón" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/fr.yaml b/src/l10n/fixtures/fr.yaml index 3d8fece..c9af165 100644 --- a/src/l10n/fixtures/fr.yaml +++ b/src/l10n/fixtures/fr.yaml @@ -3,10 +3,11 @@ "No": "Non" "Yes": "Oui" "Menu": "Menu" -'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.': 'Utiliser le raccourcis clavier Ctrl+o pour ouvrir et fermer le menu latéral. Utiliser tab pour naviguer.' +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Utiliser le raccourcis clavier Ctrl+o pour ouvrir et fermer le menu latéral. Utiliser tab pour naviguer.' "Top menu": "Menu supérieur" "Apps that not must be moved in the side menu": "Les applications qui ne doivent pas être affichées dans le menu latéral" -"If there is no selection then the global configuration is applied.": "Si il n'y a aucune sélection alors la configuration globale sera appliquée" +"If there is no selection then the global configuration is applied.": "Si il n'y a aucune sélection alors la configuration globale sera appliquée." "Experimental": "Expérimental" "Save": "Sauvegarder" "You like this app and you want to support me?": "Vous aimer cette application et vous souhaitez m'aider ?" @@ -48,14 +49,11 @@ "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" -"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" "Loader enabled": "Activation de l'indicateur de chargement" "Tips": "Astuces" "Always displayed": "Toujours affiché" -"The logo will be hidden when the menu is always displayed.": "Le logo sera masque si le menu est toujours affiché." "This is the automatic behavior when the menu is always displayed.": "C'est le comportement automatique lorsque le menu est toujours affiché." "Not compatible with touch screens.": "Incompatible avec les écrans tactiles." "Big menu": "Menu large" @@ -87,6 +85,12 @@ "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" +"Applications": "Applications" +"Applications kept in the top menu": "Applications conservées dans le menu supérieur" +"Applications kept in the top menu but also shown in side menu": "Applications conservées dans le menu supérieur mais également affichées dans le menu latéral" +"These applications must be selected in the previous option.": "Ces applications doivent également être sélectionnées dans l'option précédente." +"Hide labels on mouse over": "Masquer le libellé des applications au passage de la souris" +"Except the hovered app": "À l'exception de l'application survolée" +"Search": "Rechercher" +"Toggle the menu": "Basculer le menu" diff --git a/src/l10n/fixtures/gl.yaml b/src/l10n/fixtures/gl.yaml new file mode 100644 index 0000000..a25bf54 --- /dev/null +++ b/src/l10n/fixtures/gl.yaml @@ -0,0 +1,96 @@ +"Custom menu": "Custom menu" +"Enable the custom menu": "Enable the custom menu" +"No": "No" +"Yes": "Yes" +"Menu": "Menu" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +"Top menu": "Top menu" +"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" +"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." +"Experimental": "Experimental" +"Save": "Save" +"You like this app and you want to support me?": "You like this app and you want to support me?" +"Buy me a coffee ☕": "Buy me a coffee ☕" +"Hidden": "Hidden" +"Small": "Small" +"Normal": "Normal" +"Big": "Big" +"Hidden icon": "Hidden icon" +"Small icon": "Small icon" +"Normal icon": "Normal icon" +"Big icon": "Big icon" +"Hidden text": "Hidden text" +"Small text": "Small text" +"Normal text": "Normal text" +"Big text": "Big text" +"Colors": "Colors" +"Background color": "Background color" +"Background color of current app": "Background color of current app" +"Text color": "Text color" +"Loader": "Loader" +"Icon": "Icon" +"Same color": "Same color" +"Opposite color": "Opposite color" +"Transparent": "Transparent" +"Opaque": "Opaque" +"Opener": "Opener" +"Default": "Default" +"Default (dark)": "Default (dark)" +"Hamburger": "Hamburger" +"Hamburger (dark)": "Hamburger (dark)" +"Hamburger 2": "Hamburger 2" +"Hamburger 2 (dark)": "Hamburger 2 (dark)" +"Before the logo": "Before the logo" +"After the logo": "After the logo" +"Position": "Position" +"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Do not display the side menu and the opener if there is no application (eg: public pages)." +"Panel": "Panel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)" +"Display the big menu": "Display the big menu" +"Display the logo": "Display the logo" +"Icons and texts": "Icons and texts" +"Loader enabled": "Loader enabled" +"Tips": "Tips" +"Always displayed": "Always displayed" +"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." +"Not compatible with touch screens.": "Not compatible with touch screens." +"Big menu": "Big menu" +"Live preview": "Live preview" +"Open apps in new tab": "Open apps in new tab" +"Use the global setting": "Use the global setting" +"Use my selection": "Use my selection" +"Show and hide the list of applications": "Show and hide the list of applications" +"Use the avatar instead of the logo": "Use the avatar instead of the logo" +"You do not have permission to change the settings.": "You do not have permission to change the settings." +"Force this configuration to users": "Force this configuration to users" +"Export the configuration": "Export the configuration" +"Purge the cache": "Purge the cache" +"Show the link to settings": "Show the link to settings" +"The menu is enabled by default for users": "The menu is enabled by default for users" +"Except when the configuration is forced.": "Except when the configuration is forced." +"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" +"This feature is only compatible with the big menu display.": "This feature is only compatible with the big menu display." +"The logo is a link to the default app": "The logo is a link to the default app" +"Others": "Others" +"Categories": "Categories" +"Customize sorting": "Customize sorting" +"Order by": "Order by" +"Name": "Name" +"Customed": "Customed" +"Show and hide the list of categories": "Show and hide the list of categories" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." +"Dark mode colors": "Dark mode colors" +"With categories": "With categories" +"Custom categories": "Custom categories" +"Customize application categories": "Customize application categories" +"Reset to default": "Reset to default" +"Applications": "Applications" +"Applications kept in the top menu": "Applications kept in the top menu" +"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" +"These applications must be selected in the previous option.": "These applications must be selected in the previous option." +"Hide labels on mouse over": "Hide labels on mouse over" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/nl.yaml b/src/l10n/fixtures/nl.yaml new file mode 100644 index 0000000..bf6ee2c --- /dev/null +++ b/src/l10n/fixtures/nl.yaml @@ -0,0 +1,96 @@ +"Custom menu": "Aangepast menu" +"Enable the custom menu": "Het aangepaste menu inschakelen" +"No": "Nee" +"Yes": "Ja" +"Menu": "Menu" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Gebruik de snelkoppeling Ctrl+o om het zijmenu te openen en te verbergen. Gebruik tab om te navigeren.' +"Top menu": "Bovenste menu" +"Apps that not must be moved in the side menu": "Apps die niet moeten worden verplaatst in het zijmenu" +"If there is no selection then the global configuration is applied.": "Als er geen keuze is, wordt de globale configuratie toegepast." +"Experimental": "Experimenteel" +"Save": "Opslaan" +"You like this app and you want to support me?": "Vind je deze app leuk en wil je me steunen?" +"Buy me a coffee ☕": "Koop een koffie voor me ☕" +"Hidden": "Verborgen" +"Small": "Klein" +"Normal": "Normaal" +"Big": "Groot" +"Hidden icon": "Verborgen icoon" +"Small icon": "Klein icoon" +"Normal icon": "Normaal icoon" +"Big icon": "Groot icoon" +"Hidden text": "Verborgen tekst" +"Small text": "Kleine tekst" +"Normal text": "Normale tekst" +"Big text": "Grote tekst" +"Colors": "Kleuren" +"Background color": "Achtergrond kleur" +"Background color of current app": "Achtergrondkleur van huidige app" +"Text color": "Tekst kleur" +"Loader": "Lader" +"Icon": "Icoon" +"Same color": "Zelfde kleur" +"Opposite color": "Tegenovergestelde kleur" +"Transparent": "Transparant" +"Opaque": "Ondoorzichtig" +"Opener": "Opener" +"Default": "Standaard" +"Default (dark)": "Standaard (donker)" +"Hamburger": "Hamburger" +"Hamburger (dark)": "Hamburger (donker)" +"Hamburger 2": "Hamburger 2" +"Hamburger 2 (dark)": "Hamburger 2 (donker)" +"Before the logo": "Voor het logo" +"After the logo": "Na het logo" +"Position": "Positie" +"Show only the opener (hidden logo)": "Toon alleen de opener (verborgen logo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Geef het zijmenu en de opener niet weer als er geen toepassing is (bijv. openbare pagina's)." +"Panel": "Paneel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open het menu wanneer de muis over de opener gaat (automatisch uitgeschakeld op aanraakschermen)" +"Display the big menu": "Toon het grote menu" +"Display the logo": "Toon het logo" +"Icons and texts": "Iconen en teksten" +"Loader enabled": "Lader ingeschakeld" +"Tips": "Tips" +"Always displayed": "Altijd weergegeven" +"This is the automatic behavior when the menu is always displayed.": "Dit is het automatische gedrag wanneer het menu altijd wordt weergegeven." +"Not compatible with touch screens.": "Niet compatibel met aanraakschermen." +"Big menu": "Groot menu" +"Live preview": "Live voorbeeld" +"Open apps in new tab": "Open apps in nieuwe tab" +"Use the global setting": "Gebruik de globale instellingen" +"Use my selection": "Gebruik mijn selectie" +"Show and hide the list of applications": "De lijst met toepassingen tonen en verbergen" +"Use the avatar instead of the logo": "Gebruik avatar in plaats van het logo" +"You do not have permission to change the settings.": "Je hebt geen toestemming om de instellingen te veranderen." +"Force this configuration to users": "Forceer deze configuratie aan gebruikers" +"Export the configuration": "Exporteer de configuratie" +"Purge the cache": "De cache wissen" +"Show the link to settings": "Toon de link naar de instellingen" +"The menu is enabled by default for users": "Het menu is standaard ingeschakeld voor gebruikers" +"Except when the configuration is forced.": "Behalve als de configuratie geforceerd is." +"Apps that should not be displayed in the menu": "Apps die niet in het menu weergegeven mogen worden" +"This feature is only compatible with the big menu display.": "Deze functie is alleen compatibel met het grote menu scherm." +"The logo is a link to the default app": "Het logo is een link naar de standaard app" +"Others": "Overige" +"Categories": "Categorieën" +"Customize sorting": "Sortering aanpassen" +"Order by": "Sorteer op" +"Name": "Naam" +"Customed": "Aangepast" +"Show and hide the list of categories": "De lijst met categorieën tonen en verbergen" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Deze parameters worden gebruikt wanneer Dark theme of Breeze Dark Theme zijn ingeschakeld." +"Dark mode colors": "Donkere modus kleuren" +"With categories": "Met categorieën" +"Custom categories": "Aangepaste categorieën" +"Customize application categories": "Toepassingscategorieën aanpassen" +"Reset to default": "Terugzetten naar standaard" +"Applications": "Applicaties" +"Applications kept in the top menu": "Applicaties bewaard in het bovenste menu" +"Applications kept in the top menu but also shown in side menu": "Applicaties blijven in het topmenu maar worden ook in het zijmenu getoond" +"These applications must be selected in the previous option.": "Deze toepassingen moeten bij de vorige optie zijn geselecteerd." +"Hide labels on mouse over": "Hide labels on mouse over" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/pt_BR.yaml b/src/l10n/fixtures/pt_BR.yaml new file mode 100644 index 0000000..b93608f --- /dev/null +++ b/src/l10n/fixtures/pt_BR.yaml @@ -0,0 +1,94 @@ +"Custom menu": "Menu personalizado" +"Enable the custom menu": "Habilitar o menu personalizado" +"No": "Não" +"Yes": "Sim" +"Menu": "Menu" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Use o atalho Ctrl+o para exibir e para esconder o menu lateral. Use tab para navegar.' +"Top menu": "Menu superior" +"Apps that not must be moved in the side menu": "Apps que não devem ser movidos para o menu lateral" +"If there is no selection then the global configuration is applied.": "Se não houver seleção, a configuração global será aplicada." +"Experimental": "Experimental" +"Save": "Salvar" +"You like this app and you want to support me?": "Você gosta deste aplicativo e quer me apoiar?" +"Buy me a coffee ☕": "Me pague um café ☕" +"Hidden": "Oculto" +"Small": "Pequeno" +"Normal": "Normal" +"Big": "Grande" +"Hidden icon": "Ícone oculto" +"Small icon": "Ícone pequeno" +"Normal icon": "Ícone normal" +"Big icon": "Ícone grance" +"Hidden text": "Texto oculto" +"Small text": "Texto pequeno" +"Normal text": "Texto normal" +"Big text": "Texto grande" +"Colors": "Cores" +"Background color": "Cor de fundo" +"Background color of current app": "Cor de fundo do app atual" +"Text color": "Cor do texto" +"Loader": "Progresso" +"Icon": "Ícone" +"Same color": "Mesma cor" +"Opposite color": "Cor oposta" +"Transparent": "Transparente" +"Opaque": "Opaco" +"Opener": "Abrir" +"Default": "Padrão" +"Default (dark)": "Padrão (escuro)" +"Hamburger": "Hamburger" +"Hamburger (dark)": "Hamburger (escuro)" +"Hamburger 2": "Hamburger 2" +"Hamburger 2 (dark)": "Hamburger 2 (escuro)" +"Before the logo": "Antes da logo" +"After the logo": "Depois da logo" +"Position": "Posição" +"Show only the opener (hidden logo)": "Mostrar apenas o Abrir (ocultar logo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Não mostrar o menu lateral e o Abrir se não houver aplicação (p.ex. páginas públicas)." +"Panel": "Painel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Abrir o menu quando o mouse passar sobre o Abrir (desativado automaticamente em telas de toque)" +"Display the big menu": "Mostrar o menu grande" +"Display the logo": "Mostrar a logo" +"Icons and texts": "Ícones e textos" +"Loader enabled": "Progresso ativado" +"Tips": "Dicas" +"Always displayed": "Sempre visível" +"This is the automatic behavior when the menu is always displayed.": "Este é o comportamento automático quando o menu está sempre visível." +"Not compatible with touch screens.": "Não compatível com telas de toque." +"Big menu": "Menu grande" +"Live preview": "Visualização ao vivo" +"Open apps in new tab": "Abrir apps em nova aba" +"Use the global setting": "Usar configurações globais" +"Use my selection": "Usar minha seleção" +"Show and hide the list of applications": "Mostrar e ocultar a lista de aplicativos" +"Use the avatar instead of the logo": "Use o avatar ao invés da logo" +"You do not have permission to change the settings.": "Você não tem permissão para alterar as configurações." +"Force this configuration to users": "Forçar esta configuração para os usuários" +"Export the configuration": "Exportar a configuração" +"Purge the cache": "Limpar o cache" +"Show the link to settings": "Mostrar o link para configurações" +"The menu is enabled by default for users": "O menu é habilitado por padrão para os usuários" +"Except when the configuration is forced.": "Exceto quando a configuração é forçada." +"Apps that should not be displayed in the menu": "Apps que não devem ser mostrados no menu" +"This feature is only compatible with the big menu display.": "Este recurso só é compatível com a exibição do menu grande." +"The logo is a link to the default app": "A logo é um link para o app padrão" +"Others": "Outros" +"Categories": "Categorias" +"Customize sorting": "Personalizar classificação" +"Order by": "Ordenar por" +"Name": "Nome" +"Customed": "Personalizado" +"Show and hide the list of categories": "Mostrar e esconder a lista de categorias" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Estes parâmetros são usados quando o tema escuro ou o tema Dark Breeze está ativo." +"Dark mode colors": "Cores do modo escuro" +"With categories": "Com categorias" +"Custom categories": "Categorias personalizadas" +"Customize application categories": "Personalizar categorias de apps" +"Reset to default": "Restaurar padrão" +"Applications": "Aplicativos" +"Applications kept in the top menu": "Aplicativos mantidos no menu superior" +"Applications kept in the top menu but also shown in side menu": "Aplicativos mantidos no menu superior, mas também mostrados no menu lateral" +"These applications must be selected in the previous option.": "Estes aplicativos devem ser selecionados na opção anterior." +"Hide labels on mouse over": "Ocultar descrição ao passar o mouse" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/ru.yaml b/src/l10n/fixtures/ru.yaml new file mode 100644 index 0000000..660214f --- /dev/null +++ b/src/l10n/fixtures/ru.yaml @@ -0,0 +1,96 @@ +"Custom menu": "Custom menu" +"Enable the custom menu": "Включить пользовательское меню" +"No": "Нет" +"Yes": "Да" +"Menu": "Меню" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Используйте сочетание клавиш Ctrl+o, чтобы открыть или скрыть боковое меню. Используйте Tab для навигации.' +"Top menu": "Верхнее меню" +"Apps that not must be moved in the side menu": "Приложения не перемещаемые в боковое меню" +"If there is no selection then the global configuration is applied.": "Если тут ничего не отмечено, применяются глобальные настройки." +"Experimental": "Экспериментальный" +"Save": "Сохранить" +"You like this app and you want to support me?": "Вам нравится приложение или вы хотите поддержать меня?" +"Buy me a coffee ☕": "Купить мне чашку кофе ☕" +"Hidden": "Скрыто" +"Small": "Маленький" +"Normal": "Средний" +"Big": "Большой" +"Hidden icon": "Без иконки" +"Small icon": "Маленькая иконка" +"Normal icon": "Средняя иконка" +"Big icon": "Большая иконка" +"Hidden text": "Без текста" +"Small text": "Маленький текст" +"Normal text": "Средний текст" +"Big text": "Большой текст" +"Colors": "Цвета" +"Background color": "Цвет фона" +"Background color of current app": "Цвет фона выбранного приложения" +"Text color": "Цвет текста" +"Loader": "Загрузчик" +"Icon": "Иконка" +"Same color": "Такой же цвет" +"Opposite color": "Противоположный цвет" +"Transparent": "Прозрачный" +"Opaque": "Непрозрачный" +"Opener": "Открывалка" +"Default": "По умолчанию" +"Default (dark)": "По умолчанию (тёмный)" +"Hamburger": "Гамбургер" +"Hamburger (dark)": "Гамбургер (тёмный)" +"Hamburger 2": "Гамбургер 2" +"Hamburger 2 (dark)": "Гамбургер 2 (тёмный)" +"Before the logo": "Перед логотипом" +"After the logo": "После логотипа" +"Position": "Положение" +"Show only the opener (hidden logo)": "Показать только открывающую часть (скрытый логотип)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Не отображать боковое меню и открывалку, если нет приложения (например, публичные страницы)." +"Panel": "Панель" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Открывать меню при наведении мыши на экран (автоматически отключается на сенсорных экранах)" +"Display the big menu": "Отобразить большое меню" +"Display the logo": "Показать логотип" +"Icons and texts": "Иконки и текст" +"Loader enabled": "Загрузчик включен" +"Tips": "Советы" +"Always displayed": "Всегда отображается" +"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." +"Not compatible with touch screens.": "Не совместимо с сенсорными экранами." +"Big menu": "Большое меню" +"Live preview": "Live preview" +"Open apps in new tab": "Открывать приложения в новой вкладке" +"Use the global setting": "Использовать глобальные настройки" +"Use my selection": "Использовать мои настройки" +"Show and hide the list of applications": "Показать или скрыть список приложений" +"Use the avatar instead of the logo": "Использовать аватар вместо логотипа" +"You do not have permission to change the settings.": "У вас нет разрешения изменять настройки." +"Force this configuration to users": "Force this configuration to users" +"Export the configuration": "Экспортировать конфигурацию" +"Purge the cache": "Очистить кэш" +"Show the link to settings": "Показать ссылку на настройки" +"The menu is enabled by default for users": "Это меню включено по умолчанию для пользователей" +"Except when the configuration is forced.": "Except when the configuration is forced." +"Apps that should not be displayed in the menu": "Ппрограммы, скрытые из меню" +"This feature is only compatible with the big menu display.": "This feature is only compatible with the big menu display." +"The logo is a link to the default app": "Логотип открывает приложение по умолчанию" +"Others": "Прочие" +"Categories": "Категории" +"Customize sorting": "Настроить сортировку" +"Order by": "В порядке" +"Name": "Название" +"Customed": "Customed" +"Show and hide the list of categories": "Показать или скрыть список категорий" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Эти настройки используются темами Тёмная и Тёмная Breeze." +"Dark mode colors": "Цвета тёмной темы" +"With categories": "С категориями" +"Custom categories": "Пользовательские категории" +"Customize application categories": "Изменить категории приложений" +"Reset to default": "Сбросить к значениям по умолчанию" +"Applications": "Приложения" +"Applications kept in the top menu": "Applications kept in the top menu" +"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" +"These applications must be selected in the previous option.": "These applications must be selected in the previous option." +"Hide labels on mouse over": "Скрыть название при наведении мыши" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/sk.yaml b/src/l10n/fixtures/sk.yaml new file mode 100644 index 0000000..d386863 --- /dev/null +++ b/src/l10n/fixtures/sk.yaml @@ -0,0 +1,94 @@ +"Custom menu": "Custom menu" +"Enable the custom menu": "Enable the custom menu" +"No": "No" +"Yes": "Yes" +"Menu": "Menu" +? 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +: 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.' +"Top menu": "Top menu" +"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" +"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." +"Experimental": "Experimental" +"Save": "Save" +"You like this app and you want to support me?": "You like this app and you want to support me?" +"Buy me a coffee ☕": "Buy me a coffee ☕" +"Hidden": "Hidden" +"Small": "Small" +"Normal": "Normal" +"Big": "Big" +"Hidden icon": "Hidden icon" +"Small icon": "Small icon" +"Normal icon": "Normal icon" +"Big icon": "Big icon" +"Hidden text": "Hidden text" +"Small text": "Small text" +"Normal text": "Normal text" +"Big text": "Big text" +"Colors": "Colors" +"Background color": "Background color" +"Background color of current app": "Background color of current app" +"Text color": "Text color" +"Loader": "Loader" +"Icon": "Icon" +"Same color": "Same color" +"Opposite color": "Opposite color" +"Transparent": "Transparent" +"Opaque": "Opaque" +"Opener": "Opener" +"Default": "Default" +"Default (dark)": "Default (dark)" +"Hamburger": "Hamburger" +"Hamburger (dark)": "Hamburger (dark)" +"Hamburger 2": "Hamburger 2" +"Hamburger 2 (dark)": "Hamburger 2 (dark)" +"Before the logo": "Before the logo" +"After the logo": "After the logo" +"Position": "Position" +"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Do not display the side menu and the opener if there is no application (eg: public pages)." +"Panel": "Panel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)" +"Display the big menu": "Display the big menu" +"Display the logo": "Display the logo" +"Icons and texts": "Icons and texts" +"Loader enabled": "Loader enabled" +"Tips": "Tips" +"Always displayed": "Always displayed" +"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." +"Not compatible with touch screens.": "Not compatible with touch screens." +"Big menu": "Big menu" +"Live preview": "Live preview" +"Open apps in new tab": "Open apps in new tab" +"Use the global setting": "Use the global setting" +"Use my selection": "Use my selection" +"Show and hide the list of applications": "Show and hide the list of applications" +"Use the avatar instead of the logo": "Use the avatar instead of the logo" +"You do not have permission to change the settings.": "You do not have permission to change the settings." +"Force this configuration to users": "Force this configuration to users" +"Export the configuration": "Export the configuration" +"Purge the cache": "Purge the cache" +"Show the link to settings": "Show the link to settings" +"The menu is enabled by default for users": "The menu is enabled by default for users" +"Except when the configuration is forced.": "Except when the configuration is forced." +"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" +"This feature is only compatible with the big menu display.": "This feature is only compatible with the big menu display." +"The logo is a link to the default app": "The logo is a link to the default app" +"Others": "Others" +"Categories": "Categories" +"Customize sorting": "Customize sorting" +"Order by": "Order by" +"Name": "Name" +"Customed": "Customed" +"Show and hide the list of categories": "Show and hide the list of categories" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." +"Dark mode colors": "Dark mode colors" +"With categories": "With categories" +"Custom categories": "Custom categories" +"Customize application categories": "Customize application categories" +"Reset to default": "Reset to default" +"Applications": "Applications" +"Applications kept in the top menu": "Applications kept in the top menu" +"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" +"These applications must be selected in the previous option.": "These applications must be selected in the previous option." +"Hide labels on mouse over": "Hide labels on mouse over" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/tpl/base.yaml b/src/l10n/fixtures/tpl/base.yaml new file mode 100644 index 0000000..a2e635a --- /dev/null +++ b/src/l10n/fixtures/tpl/base.yaml @@ -0,0 +1,100 @@ +"Custom menu": "Custom menu" +"Enable the custom menu": "Enable the custom menu" +"No": "No" +"Yes": "Yes" +"Menu": "Menu" +? 'Use the shortcut Ctrl+o + to open and to hide the side menu. Use tab to + navigate.' +: 'Use the shortcut Ctrl+o + to open and to hide the side menu. Use tab to + navigate.' +"Top menu": "Top menu" +"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" +"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." +"Experimental": "Experimental" +"Save": "Save" +"You like this app and you want to support me?": "You like this app and you want to support me?" +"Buy me a coffee ☕": "Buy me a coffee ☕" +"Hidden": "Hidden" +"Small": "Small" +"Normal": "Normal" +"Big": "Big" +"Hidden icon": "Hidden icon" +"Small icon": "Small icon" +"Normal icon": "Normal icon" +"Big icon": "Big icon" +"Hidden text": "Hidden text" +"Small text": "Small text" +"Normal text": "Normal text" +"Big text": "Big text" +"Colors": "Colors" +"Background color": "Background color" +"Background color of current app": "Background color of current app" +"Text color": "Text color" +"Loader": "Loader" +"Icon": "Icon" +"Same color": "Same color" +"Opposite color": "Opposite color" +"Transparent": "Transparent" +"Opaque": "Opaque" +"Opener": "Opener" +"Default": "Default" +"Default (dark)": "Default (dark)" +"Hamburger": "Hamburger" +"Hamburger (dark)": "Hamburger (dark)" +"Hamburger 2": "Hamburger 2" +"Hamburger 2 (dark)": "Hamburger 2 (dark)" +"Before the logo": "Before the logo" +"After the logo": "After the logo" +"Position": "Position" +"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" +"Do not display the side menu and the opener if there is no application (eg: public pages).": "Do not display the side menu and the opener if there is no application (eg: public pages)." +"Panel": "Panel" +"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)" +"Display the big menu": "Display the big menu" +"Display the logo": "Display the logo" +"Icons and texts": "Icons and texts" +"Loader enabled": "Loader enabled" +"Tips": "Tips" +"Always displayed": "Always displayed" +"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." +"Not compatible with touch screens.": "Not compatible with touch screens." +"Big menu": "Big menu" +"Live preview": "Live preview" +"Open apps in new tab": "Open apps in new tab" +"Use the global setting": "Use the global setting" +"Use my selection": "Use my selection" +"Show and hide the list of applications": "Show and hide the list of applications" +"Use the avatar instead of the logo": "Use the avatar instead of the logo" +"You do not have permission to change the settings.": "You do not have permission to change the settings." +"Force this configuration to users": "Force this configuration to users" +"Export the configuration": "Export the configuration" +"Purge the cache": "Purge the cache" +"Show the link to settings": "Show the link to settings" +"The menu is enabled by default for users": "The menu is enabled by default for users" +"Except when the configuration is forced.": "Except when the configuration is forced." +"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" +"This feature is only compatible with the big menu display.": "This feature is only compatible with the big menu display." +"The logo is a link to the default app": "The logo is a link to the default app" +"Others": "Others" +"Categories": "Categories" +"Customize sorting": "Customize sorting" +"Order by": "Order by" +"Name": "Name" +"Customed": "Customed" +"Show and hide the list of categories": "Show and hide the list of categories" +"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." +"Dark mode colors": "Dark mode colors" +"With categories": "With categories" +"Custom categories": "Custom categories" +"Customize application categories": "Customize application categories" +"Reset to default": "Reset to default" +"Applications": "Applications" +"Applications kept in the top menu": "Applications kept in the top menu" +"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" +"These applications must be selected in the previous option.": "These applications must be selected in the previous option." +"Hide labels on mouse over": "Hide labels on mouse over" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle the menu": "Toggle the menu" diff --git a/src/l10n/fixtures/zh_CN.yaml b/src/l10n/fixtures/zh_CN.yaml index 4e8cb36..2f544b6 100644 --- a/src/l10n/fixtures/zh_CN.yaml +++ b/src/l10n/fixtures/zh_CN.yaml @@ -3,7 +3,8 @@ "No": "取消" "Yes": "确定" "Menu": "菜单" -"Use the shortcut Ctrl<\/span>+o<\/span> to open and to hide the side menu. Use tab<\/span> to navigate.": "使用快捷键 Ctrl<\/span>+o<\/span> 打开或隐藏侧边栏菜单。使用tab<\/span> 来导航。" +? "Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate." +: "使用快捷键 Ctrl+o 打开或隐藏侧边栏菜单。使用tab 来导航。" "Top menu": "顶部菜单" "Apps that not must be moved in the side menu": "禁止在侧边栏菜单移动的应用" "If there is no selection then the global configuration is applied.": "如不选择,将应用全局设定。" @@ -40,14 +41,11 @@ "Panel": "面板" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)" "Display the big menu": "显示大型菜单" -"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": "图标与文字" "Loader enabled": "菜单指示器已激活" "Tips": "技巧" "Always displayed": "一直显示" -"The logo will be hidden when the menu is always displayed.": "一直显示菜单时logo将被隐藏。" "This is the automatic behavior when the menu is always displayed.": "一直显示菜单时的自动动作。" "Not compatible with touch screens.": "与触屏不兼容。" "Big menu": "大型菜单" @@ -65,7 +63,7 @@ "The menu is enabled by default for users": "用户的默认菜单已激活" "Except when the configuration is forced.": "除非设置被强制使用。" "Apps that should not be displayed in the menu": "禁止在菜单中显示的应用" -"This feature is only compatible with the big menu<\/code> display.": "此功能只和大型菜单<\/code>兼容。" +"This feature is only compatible with the big menu display.": "此功能只和大型菜单兼容。" "The logo is a link to the default app": "logo链接到默认应用" "Others": "其他" "Categories": "类别" @@ -79,8 +77,6 @@ "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": "小图标" @@ -90,3 +86,11 @@ "Small text": "小文本" "Normal text": "普通文本" "Big text": "大文本" +"Applications": "Applications" +"Applications kept in the top menu": "Applications kept in the top menu" +"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" +"These applications must be selected in the previous option.": "These applications must be selected in the previous option." +"Hide labels on mouse over": "Hide labels on mouse over" +"Except the hovered app": "Except the hovered app" +"Search": "Search" +"Toggle menu": "Toggle menu" diff --git a/src/lib/createElement.js b/src/lib/createElement.js new file mode 100644 index 0000000..9be7ff4 --- /dev/null +++ b/src/lib/createElement.js @@ -0,0 +1,17 @@ +module.exports = (tagName, attributes) => { + const element = document.createElement(tagName) + + if (typeof attributes === 'object') { + for (let i in attributes) { + if (i === 'text') { + element.textContent = attributes[i] + } else if (i === 'html') { + element.innerHTML = attributes[i] + } else { + element.setAttribute(i, attributes[i]) + } + } + } + + return element +} diff --git a/templates/css/stylesheet.php b/templates/css/stylesheet.php index 379fb42..5638629 100644 --- a/templates/css/stylesheet.php +++ b/templates/css/stylesheet.php @@ -1,116 +1,132 @@ :root { - $value): ?> - - --side-menu-: url(''); - - --side-menu-: ; - - + $value): ?> + + --side-menu-: url(''); + + --side-menu-: ; + + } - #appmenu { - display: none; - } + #appmenu { + display: none; + } - #appmenu + nav { - display: none; - } + #appmenu + nav { + display: none; + } - .app-hidden { - opacity: 0; - } + .app-hidden { + opacity: 0; + } - #nextcloud { - display: none; - } + #nextcloud { + display: none; + } - .side-menu-logo { - display: none; + .side-menu-logo { + display: none; + } + + .side-menu-header { + height: 50px; + } + + .side-menu-apps-list { + height: calc(100vh - 49px); + top: 49px; + } + + #side-menu.hide-opener .side-menu-header .side-menu-opener.side-menu-closer { + visibility: hidden; + } + + #side-menu.hide-opener.side-menu-with-categories .side-menu-search { + float: none; + } + + + #side-menu, .side-menu-apps-list { + + width: 55px; + + width: 52px; + } - .side-menu-header { - height: 50px; + #side-menu .side-menu-opener { + + margin-left: 1px; + + margin-left: 0px; + } - - .side-menu-apps-list { - height: calc(100vh - 49px); - top: 49px; - } - - #side-menu.hide-opener .side-menu-header { - visibility: hidden; - } - - - #side-menu, .side-menu-apps-list { - - width: 55px; - - width: 52px; - - } - - #side-menu .side-menu-opener { - - margin-left: 1px; - - margin-left: 0px; - - } - + - .side-menu-app-icon { - display: none; - } + .side-menu-app-icon { + display: none; + } - .side-menu-app-icon svg { - width: 15px; - height: 15px; - } + .side-menu-app-icon svg { + width: 15px; + height: 15px; + } - img.side-menu-app-icon { - width: 15px; - height: 15px; - } + img.side-menu-app-icon { + width: 15px; + height: 15px; + } - .side-menu-app-icon svg { - width: 20px; - height: 20px; - } + .side-menu-app-icon svg { + width: 20px; + height: 20px; + } - img.side-menu-app-icon { - width: 20px; - height: 20px; - } + img.side-menu-app-icon { + width: 20px; + height: 20px; + } - .side-menu-app-icon svg { - width: 23px; - height: 23px; - } + .side-menu-app-icon svg { + width: 23px; + height: 23px; + } - img.side-menu-app-icon { - width: 23px; - height: 23px; - } + img.side-menu-app-icon { + width: 23px; + height: 23px; + } - .side-menu-app-text { - display: none; - } + .side-menu-app-text { + display: none; + } - .side-menu-app-text { - font-size: 12px; - } + .side-menu-app-text { + font-size: 12px; + } - .side-menu-app-text { - font-size: 16px; - } + .side-menu-app-text { + font-size: 16px; + } + + + + #content { + left: 53px; + width: calc(100% - (var(--body-container-margin) * 2) - 62px); + } + + #content-vue { + width: calc(100% - (var(--body-container-margin) * 2) - 60px); + margin-left: 11px; + } diff --git a/templates/js/_alwaysDisplayed.js b/templates/js/_alwaysDisplayed.js deleted file mode 100644 index 9e3727f..0000000 --- a/templates/js/_alwaysDisplayed.js +++ /dev/null @@ -1,86 +0,0 @@ -const alwaysDisplayed = function() { - const elements = querySelectorAll('*') - const fixedElements = [] - - for (let element of elements) { - if (typeof element !== 'object') { - continue - } - - const position = window.getComputedStyle(element, null).getPropertyValue('position') - - if (position !== 'fixed') { - continue - } - - const id = element.getAttribute('id') - - if (id === 'header' || id === 'side-menu' || id === 'side-menu-loader') { - continue - } - - if (element.classList.contains('oc-dialog')) { - continue - } - - 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 - } - - fixedElements.push(element) - } - - for (let i in fixedElements) { - const element = fixedElements[i] - const computedStyle = window.getComputedStyle(element, null) - const left = computedStyle.getPropertyValue('left') - const right = computedStyle.getPropertyValue('right') - - if (right !== '0px') { - const intValue = parseInt(left.replace('px', '')) + 50 - element.style.setProperty('transform', 'translateX(' + intValue.toString() + 'px)') - } - } -} - -const content = querySelector('#content') - -if (content && content.classList.contains('app-settings')) { - let loaded = false - const config = { - attributes: false, - childList: true, - subtree: true - } - const observer = new MutationObserver(() => { - if (loaded) { - return - } - - const element = content.querySelector('#app-category-your-apps') || content.querySelector('#app-navigation ul') - - if (element) { - loaded = true - - alwaysDisplayed() - } - }) - - observer.observe(content, config) -} else { - window.setTimeout(alwaysDisplayed, 200) -} diff --git a/templates/js/_loaderEnabled.js b/templates/js/_loaderEnabled.js deleted file mode 100644 index 1d5ff4e..0000000 --- a/templates/js/_loaderEnabled.js +++ /dev/null @@ -1,14 +0,0 @@ -let pageLoader = createElement('div', {id: 'side-menu-loader'}) -let pageLoaderBar = createElement('div', {id: 'side-menu-loader-bar'}) - -pageLoader.appendChild(pageLoaderBar) -querySelector('body').appendChild(pageLoader) - -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 deleted file mode 100644 index 455b09a..0000000 --- a/templates/js/_topMenuApps.js +++ /dev/null @@ -1,216 +0,0 @@ -let menuCache = null - -const breakpointMobileWidth = 1024 -const usePercentualAppMenuLimit = 0.8 -const minAppsDesktop = 8 - -const handleMenuClick = (e, icon) => { - let element = e.target - - 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 - } - - let navigationAppsHtml = '' - - for (let app of apps) { - const dataId = app.getAttribute('data-id') - - if (dataId === null) { - continue - } - - 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) - - navigationAppsHtml = navigationAppsHtml + app.outerHTML - } - - if (targetBlankApps.indexOf(dataId) !== -1) { - querySelector('a', app).setAttribute('target', '_blank') - } - } - - 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 - } else if (!isMobile && appCount < minAppsDesktop) { - appCount = minAppsDesktop - } - - menu.style.opacity = 1 - - if (appShown.length - 1 - appCount >= 1) { - appCount-- - } - - for (let item of querySelectorAll('a', moreApps)) { - item.classList.remove('active') - } - - let k = 0 - let notInHeader = 0 - - 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.classList.remove('hidden') - li.classList.add('in-header') - - lastShownApp = app - } else { - app.classList.add('hidden') - li.classList.remove('in-header') - - notInHeader++ - - const a = querySelector('a', app) - - if (appCount > 0 && a.classList.contains('active')) { - lastShownApp.classList.add('hidden') - app.classList.remove('hidden') - notInHeader++ - - li.classList.add('in-header') - } - } - - k++ - } - - // 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') - - 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) - }) - } - } - - for (let app of querySelectorAll('#apps li.app-external-site')) { - const appId = app.getAttribute('data-id') - - if (app.classList.contains('in-header')) { - for (let defs of querySelectorAll('svg defs', app)) { - defs.remove() - } - } else { - const svg = querySelector('svg', app) - - if (querySelectorAll('svg defs', app).length > 0) { - continue - } - - const defs = ` - - - - - ` - - svg.innerHTML = defs + svg.innerHTML - - for (let image of querySelectorAll('image', svg)) { - image.setAttribute('filter', `url(#invertMenuMore-${appId})`) - } - - svg.innerHTML = svg.innerHTML.replace(/fecolormatrix/g, 'feColorMatrix') - } - } - - if (notInHeader === 0) { - moreApps.style.display = 'none' - navigation.style.display = 'none' - } else { - moreApps.style.display = 'flex' - } - - menuCache = menu.innerHTML + menu.nextSibling.innerHTML -} - -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 f62b7da..f623d8f 100644 --- a/templates/js/script.php +++ b/templates/js/script.php @@ -1,5 +1,7 @@ +const SMcreateElement = (tagName, attributes) => { + const element = document.createElement(tagName) + + if (typeof attributes === 'object') { + for (let i in attributes) { + if (i === 'text') { + element.textContent = attributes[i] + } else if (i === 'html') { + element.innerHTML = attributes[i] + } else { + element.setAttribute(i, attributes[i]) + } + } + } + + return element +} + (function() { - const querySelector = function(selector, element) { - if (element) { - return element.querySelector(selector) - } + const sideMenuContainer = SMcreateElement('div', {id: 'side-menu-container'}) + const sideMenuOpener = SMcreateElement('button', { + 'class': 'side-menu-opener', + 'arial-label': t('side_menu', 'Toggle the menu'), + 'html': `${t('side_menu', 'Toggle the menu')}` + }) + const sideMenu = SMcreateElement('div', {id: 'side-menu'}) - return document.querySelector(selector) + const body = document.querySelector('body') + const html = document.querySelector('html') + const nextcloud = document.querySelector('#nextcloud') + const logo = document.querySelector('.header-left .logo') + + const isTouchDevice = window.matchMedia("(pointer: coarse)").matches + + window.targetBlankApps = + window.topMenuApps = + window.topSideMenuApps = + window.menuAppsOrder = + window.topMenuAppsMouseOverHiddenLabel = + + + sideMenu.setAttribute('data-bigmenu', '1') + + sideMenu.setAttribute('data-sidewithcategories', '1') + + + const sideMenuFocus = () => { + let a = document.querySelector('#side-menu .side-menu-app.active a') + || document.querySelector('#side-menu .side-menu-app a') + + if (a) { + a.focus() } + } - const querySelectorAll = function(selector, element) { - if (element) { - return element.querySelectorAll(selector) + document.querySelector('body').addEventListener('side-menu.apps', (e) => { + const apps = e.detail.apps; + + + const sideMenu = document.querySelector('#side-menu') + + if (apps.length === 0) { + sideMenu.classList.remove('open') + sideMenu.classList.add('hide') + sideMenuOpener.classList.add('hide') + } else { + sideMenu.classList.remove('hide') + sideMenuOpener.classList.remove('hide') + } + + + if (apps.length === 0) { + html.classList.remove('side-menu-always-displayed') + } else { + html.classList.add('side-menu-always-displayed') } - - 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]) - } + + + + if (apps.length === 0) { + html.classList.remove('side-menu-always-displayed') + } else { + html.classList.add('side-menu-always-displayed') } - - 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') + + }) - querySelector('body').addEventListener('side-menu.apps', (e) => { - const apps = e.detail.apps; + body.addEventListener('side-menu.ready', () => { + const sideMenu = document.querySelector('#side-menu') + const headerMenuOpener = document.querySelector('#header .side-menu-opener') + const sideMenuOpener = document.querySelectorAll('#side-menu .side-menu-opener') - - const sideMenu = querySelector('#side-menu') + if (!headerMenuOpener) { + return + } - if (apps.length === 0) { - sideMenu.classList.remove('open') - sideMenu.classList.add('hide') - sideMenuOpener.classList.add('hide') - } else { - sideMenu.classList.remove('hide') - sideMenuOpener.classList.remove('hide') - } + + const sideMenuMouseLeave = () => { + sideMenu.classList.remove('open') + sideMenu.removeEventListener('mouseleave', sideMenuMouseLeave) + } - - if (apps.length === 0) { - html.classList.remove('side-menu-always-displayed') - } else { - html.classList.add('side-menu-always-displayed') - } - - - - if (apps.length === 0) { - html.classList.remove('side-menu-always-displayed') - } else { - html.classList.add('side-menu-always-displayed') - } - - - }) + const sideMenuMouseEnter = () => { + sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) + } - 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') + const sideMenuOpenerMouseEnter = () => { + sideMenu.classList.add('open') + sideMenu.addEventListener('mouseenter', sideMenuMouseEnter) - sideMenuFocus = () => { - let a = querySelector('.side-menu-app.active a', sideMenu) - - if (!a) { - return - } - - if (a.length === 0) { - a = querySelector('.side-menu-app:first-child a', sideMenu) - } - - if (a.length > 0) { - a.focus() - } - } + sideMenuFocus() + } + if (!isTouchDevice) { - const sideMenuMouseLeave = () => { - sideMenu.classList.remove('open') - sideMenu.removeEventListener('mouseleave', sideMenuMouseLeave) - } + headerMenuOpener.addEventListener('mouseenter', sideMenuOpenerMouseEnter) - const sideMenuMouseEnter = () => { - sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) - } + sideMenu.classList.add('hide-opener') + - const sideMenuOpenerMouseEnter = () => { - sideMenu.classList.add('open') - sideMenu.addEventListener('mouseenter', sideMenuMouseEnter) + sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) + sideMenu.addEventListener('mouseenter', sideMenuOpenerMouseEnter) + } + - sideMenuFocus() - } - - if (!isTouchDevice) { - - headerMenuOpener.addEventListener('mouseenter', sideMenuOpenerMouseEnter) - - sideMenu.classList.add('hide-opener') - - - sideMenu.addEventListener('mouseleave', sideMenuMouseLeave) - sideMenu.addEventListener('mouseenter', sideMenuOpenerMouseEnter) - } - - - headerMenuOpener.addEventListener('click', () => { - sideMenu.classList.add('open') - - const a = querySelector('.side-menu-app.active a', sideMenu) - - if (a !== null) { - a.focus() - } - - headerMenuOpener.blur() - }) - - for (let opener of sideMenuOpener) { - opener.addEventListener('click', () => { - - sideMenu.classList.toggle('open') - - sideMenu.classList.remove('open') - - }) - } - - document.addEventListener('keydown', (e) => { - var key = e.key || e.keyCode - - if ((key === 'o' || key === 79) && e.ctrlKey === true) { - e.preventDefault() - - 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 - }) + headerMenuOpener.addEventListener('click', () => { + sideMenu.classList.add('open') + headerMenuOpener.blur() + sideMenuFocus() }) - body.appendChild(sideMenuContainer) - sideMenuContainer.appendChild(sideMenu) + for (let opener of sideMenuOpener) { + opener.addEventListener('click', () => { + + sideMenu.classList.toggle('open') + + sideMenu.classList.remove('open') + + }) + } - - - + document.addEventListener('keydown', (e) => { + var key = e.key || e.keyCode + + if ((key === 'o' || key === 79) && e.ctrlKey === true) { + e.preventDefault() + + 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.appendChild(sideMenuContainer) + sideMenuContainer.appendChild(sideMenu) + + + PageLoader() + + + if (nextcloud) { + if (logo && logo.parentNode !== nextcloud) { + nextcloud.appendChild(logo) + } - nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud) + nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud) - nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling) - - - - const topMenuApps = - const topSideMenuApps = - - - - - - + nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling) + } })(); diff --git a/templates/settings/admin-form.php b/templates/settings/admin-form.php index d2ffa93..1053b04 100644 --- a/templates/settings/admin-form.php +++ b/templates/settings/admin-form.php @@ -41,8 +41,108 @@ $choicesSizes = [ $labelShowHideApps = 'Show and hide the list of applications'; $labelReset = 'Reset to default'; +$labelDefault = 'Default'; +$labelWithCategories = 'With categories'; +$labelBigMenu = 'Big menu'; +$labelAlwaysDisplayed = 'Always displayed'; + ?>
+ +
+

+ 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'], + ]; + ?> + +
+ +
+

+ <?php p($l->t($labelDefault)); ?> +

+ +
+ +
+

+ <?php p($l->t($labelWithCategories)); ?> +

+ +
+ +
+

+ <?php p($l->t($labelBigMenu)); ?> +

+ +
+ +
+

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

+

+ <?php p($l->t($labelAlwaysDisplayed)); ?> +

+ + + + +
+

t('Colors')); ?> @@ -50,6 +150,13 @@ $labelReset = 'Reset to default'; t('Live preview')); ?>

+
+ t($labelDefault)); ?> + t($labelWithCategories)); ?> + t($labelBigMenu)); ?> + t($labelAlwaysDisplayed)); ?> +
+
@@ -206,7 +313,7 @@ $labelReset = 'Reset to default';
'side-menu-opener', + $labelDefault => 'side-menu-opener', 'Default (dark)' => 'side-menu-opener-dark', 'Hamburger' => 'side-menu-opener-hamburger', 'Hamburger (dark)' => 'side-menu-opener-hamburger-dark', @@ -234,6 +341,13 @@ $labelReset = 'Reset to default'; t('Dark mode colors')); ?> +
+ t($labelDefault)); ?> + t($labelWithCategories)); ?> + t($labelBigMenu)); ?> + t($labelAlwaysDisplayed)); ?> +
+

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

@@ -392,7 +506,7 @@ $labelReset = 'Reset to default';
'side-menu-opener', + $labelDefault => 'side-menu-opener', 'Default (dark)' => 'side-menu-opener-dark', 'Hamburger' => 'side-menu-opener-hamburger', 'Hamburger (dark)' => 'side-menu-opener-hamburger-dark', @@ -422,6 +536,13 @@ $labelReset = 'Reset to default'; t('Opener')); ?> +
+ t($labelDefault)); ?> + t($labelWithCategories)); ?> + t($labelBigMenu)); ?> + t($labelAlwaysDisplayed)); ?> +
+
-
-
-
-

- 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'], - ]; - ?> - -
- -
-

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

- -
- -
-

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

-

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

- -
- -
-

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')); ?> -

- - - - -
- - -
t('Open the menu when the mouse is hover the opener (automatically disabled on touch screens)')); ?> @@ -591,16 +612,18 @@ $labelReset = 'Reset to default';
+
+
+
+
t('Display the logo')); ?> -
- - t('This feature is not compatible with the big menu display.'); ?> -
- t('The logo will be hidden when the menu is always displayed.')); ?> -
+ +
+ t($labelDefault)); ?> +
@@ -631,6 +658,10 @@ $labelReset = 'Reset to default';
t('The logo is a link to the default app')); ?> + +
+ t($labelDefault)); ?> +
+ $value): ?> + + + +
+
+
+
+ +
+

+ t('Applications')); ?> +

+ +
+ t($labelDefault)); ?> + t($labelWithCategories)); ?> + t($labelBigMenu)); ?> + t($labelAlwaysDisplayed)); ?> +
+ +
+
+
+ t('Customize sorting')); ?> +
+
+ + 🖱️ t($labelShowHideApps)); ?> + + +
+ + + + ' name="apps-order" class="side-menu-setting" id="side-menu-apps-order"> +
+
@@ -840,6 +949,11 @@ $labelReset = 'Reset to default'; t('Categories')); ?> +
+ t($labelWithCategories)); ?> + t($labelBigMenu)); ?> +
+
-
+
t('Custom categories')); ?>
@@ -950,6 +1064,13 @@ $labelReset = 'Reset to default'; t('Tips')); ?> +
+ t($labelDefault)); ?> + t($labelWithCategories)); ?> + t($labelBigMenu)); ?> + t($labelAlwaysDisplayed)); ?> +
+

t('Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.'); ?>

@@ -994,19 +1115,19 @@ $labelReset = 'Reset to default';
- - - @@ -1018,7 +1139,7 @@ $labelReset = 'Reset to default'; t('You like this app and you want to support me?')); ?> - diff --git a/templates/settings/personal-form.php b/templates/settings/personal-form.php index 7794f23..793feec 100644 --- a/templates/settings/personal-form.php +++ b/templates/settings/personal-form.php @@ -27,6 +27,7 @@ $choicesYesNo = [ $labelShowHideApps = 'Show and hide the list of applications'; +$labelReset = 'Reset to default'; ?>
@@ -41,10 +42,6 @@ $labelShowHideApps = 'Show and hide the list of applications';
-

- t('Menu')); ?> -

-

t('Use the shortcut Ctrl+o to open and to hide the side menu. Use tab to navigate.'); ?>

@@ -123,7 +120,7 @@ $labelShowHideApps = 'Show and hide the list of applications';
- t('Apps only visible in the top menu')); ?> + t('Applications kept in the top menu')); ?>

t('If there is no selection then the global configuration is applied.')); ?> @@ -163,9 +160,10 @@ $labelShowHideApps = 'Show and hide the list of applications';

- t('Apps visible in the top and side menus')); ?> + t('Applications kept in the top menu but also shown in side menu')); ?>

+ t('These applications must be selected in the previous option.')); ?>
t('If there is no selection then the global configuration is applied.')); ?>

@@ -200,11 +198,49 @@ $labelShowHideApps = 'Show and hide the list of applications';
+
+

+ t('Applications')); ?> +

+ +
+
+
+ t('Customize sorting')); ?> +
+
+ + 🖱️ t($labelShowHideApps)); ?> + + +
+ + + + ' name="apps-order" class="side-menu-setting" id="side-menu-apps-order" data-personal> +
+
+
+
- @@ -219,7 +255,7 @@ $labelShowHideApps = 'Show and hide the list of applications'; t('You like this app and you want to support me?')); ?> - diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cd22dba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "@vue/tsconfig/tsconfig.json", + "include": ["./src/**/*.js"], + "compilerOptions": { + "types": ["node", "vue", "vue-router"], + "outDir": "./js/", + "target": "ESNext", + "module": "ESNext", + // Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + // Allow ts to import js files + "allowJs": true, + "allowSyntheticDefaultImports": true, + "declaration": false, + "noImplicitAny": false, + "resolveJsonModule": true, + "strict": true, + }, + "vueCompilerOptions": { + "target": 2.7 + }, + "ts-node": { + // these options are overrides used only by ts-node + // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable + "compilerOptions": { + "moduleResolution": "node", + "module": "commonjs", + "verbatimModuleSyntax": false + } + } +} diff --git a/webpack.js b/webpack.js index 6175934..9509a6a 100644 --- a/webpack.js +++ b/webpack.js @@ -1,53 +1,70 @@ const path = require('path') -const { VueLoaderPlugin } = require('vue-loader') -const StyleLintPlugin = require('stylelint-webpack-plugin') +const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except') +const { + VueLoaderPlugin +} = require('vue-loader') +// const StyleLintPlugin = require('stylelint-webpack-plugin') module.exports = { - entry: { - 'admin': path.join(__dirname, 'src', 'admin.js'), - 'sideMenu': path.join(__dirname, 'src', 'SideMenu.js'), - }, - output: { - path: path.resolve(__dirname, './js'), - publicPath: '/js', - filename: '[name].js?v=[hash]', - chunkFilename: 'chunks/[name]-[hash].js', - }, - module: { - rules: [ - { - test: /\.css$/, - use: ['vue-style-loader', 'css-loader'], - }, - { - test: /\.scss$/, - use: ['vue-style-loader', 'css-loader', 'sass-loader'], - }, - { - test: /\.vue$/, - loader: 'vue-loader', - }, - { - test: /\.js$/, - loader: 'babel-loader', - exclude: /node_modules/, - }, - { - test: /\.(png|jpg|gif|svg)$/, - loader: 'url-loader', - options: { - name: '[name].[ext]?[hash]', - limit: 8192, - }, + devtool: "source-map", + entry: { + 'admin': path.join(__dirname, 'src', 'admin.js'), + 'sideMenu': path.join(__dirname, 'src', 'SideMenu.js'), + }, + output: { + path: path.resolve(__dirname, './js'), + publicPath: '/js', + filename: '[name].js?v=[chunkhash]', + chunkFilename: 'chunks/[name]-[chunkhash].js', + }, + module: { + rules: [{ + test: /\.css$/, + use: ['vue-style-loader', 'css-loader'], + }, + { + test: /\.scss$/, + use: ['vue-style-loader', 'css-loader', 'sass-loader'], + }, + { + test: /\.vue$/, + loader: 'vue-loader', + }, + { + test: /\.tsx?$/, + use: [ + 'babel-loader', + { + // Fix TypeScript syntax errors in Vue + loader: 'ts-loader', + options: { + transpileOnly: true, }, + }, ], - }, - plugins: [ - new VueLoaderPlugin(), - new StyleLintPlugin(), + exclude: BabelLoaderExcludeNodeModulesExcept([]), + }, + { + test: /\.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: 'url-loader', + options: { + name: '[name].[ext]?[hash]', + limit: 8192, + }, + }, ], - resolve: { - extensions: ['*', '.js', '.vue'], - symlinks: false, - }, + }, + plugins: [ + new VueLoaderPlugin(), + // new StyleLintPlugin(), + ], + resolve: { + extensions: ['.*', '.js', '.vue'], + symlinks: false, + }, }