Compare commits

..

26 commits

Author SHA1 Message Date
5c218bff2a Merge pull request 'releave v3.11.2' (#300) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #300
2023-11-12 20:30:56 +01:00
b0385c172b Merge pull request 'release v3.11.1' (#297) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #297
2023-11-12 17:03:21 +01:00
04ac7e5ed1 Merge pull request 'update changelogs' (#293) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #293
2023-11-05 18:46:46 +01:00
67743485cb Merge pull request 'release v3.11.0' (#292) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #292
2023-11-05 18:45:25 +01:00
0ce318d6ec Merge pull request 'fix ci syntax' (#281) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #281
2023-09-29 16:48:36 +02:00
c26c5f26df Merge pull request 'release v3.10.3' (#276) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #276
2023-07-24 21:35:58 +02:00
6ce7d3223f Merge pull request 'release v3.10.2' (#272) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #272
2023-07-15 15:22:53 +02:00
6eb977dad5 Merge pull request 'release v3.10.1' (#271) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #271
2023-07-15 15:03:26 +02:00
5f307cd046 Merge pull request 'release v3.10.0' (#268) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #268
2023-07-13 22:30:27 +02:00
d2421f90bf Merge pull request 'release v3.9.1' (#266) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #266
2023-06-29 13:04:13 +02:00
b16da36507 Merge pull request 'release v3.9.0' (#263) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #263
2023-06-23 12:33:54 +02:00
29f0ead9cc Merge pull request 'release v3.8.0' (#255) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #255
2023-05-25 13:49:08 +02:00
e1567b0689 Merge pull request 'v3.7.4' (#249) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #249
2023-04-16 18:05:46 +02:00
aa139bc671 Merge pull request 'release v3.7.3' (#246) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #246
2023-04-14 21:13:10 +02:00
b133ba3a97 Merge pull request 'release of v3.7.2' (#238) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #238
2023-03-27 18:47:15 +02:00
aaf2bb55db Merge pull request 'release v3.7.1' (#232) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #232
2023-03-19 10:00:00 +01:00
749f25a231 Merge pull request 'add badge of downloads' (#228) from develop into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
Reviewed-on: #228
2023-03-11 00:30:39 +01:00
04026f5c75 Merge pull request 'release v3.7.0' (#227) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #227
2023-03-09 16:57:33 +01:00
48b2b30406 Merge pull request 'update issue template' (#223) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #223
2023-02-20 14:06:23 +01:00
22e5445330 Merge pull request 'release v3.6.0' (#221) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #221
2023-02-17 11:53:27 +01:00
0544fd3765 Merge pull request 'doc' (#218) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #218
2023-02-12 16:56:39 +01:00
2635dd89ca Merge pull request 'Doc' (#217) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #217
2023-02-12 00:34:56 +01:00
aa203bb406 Merge pull request 'release v3.5.2' (#213) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #213
2023-01-21 22:31:21 +01:00
368c59a2bd Merge pull request 'next release' (#200) from develop into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: #200
2023-01-07 10:40:14 +01:00
ab7b1e96fe Merge pull request 'add gitea ISSUE/FEATURE templates' (#202) from gitea into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #202
2022-12-29 22:45:50 +01:00
f4322a8d71 Merge pull request 'add gitea ISSUE/FEATURE templates' (#201) from gitea into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #201
2022-12-29 22:45:03 +01:00
115 changed files with 5700 additions and 15318 deletions

View file

@ -1,14 +1,5 @@
module.exports = { module.exports = {
env: { rules: {
node: true, 'no-console': 'off',
}, },
extends: [ };
"eslint:recommended",
"plugin:vue/vue3-recommended",
"prettier",
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}

View file

@ -26,7 +26,7 @@ body:
id: configuration id: configuration
attributes: attributes:
label: Configuration 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/)). description: Export the configuration using the admin page and copy/paste here ([documentation](https://deblan.gitnet.page/side_menu_doc/tips/#export-the-configuration)).
value: | value: |
``` ```
{ {

View file

@ -1,30 +0,0 @@
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

View file

@ -1,8 +0,0 @@
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

2
.gitignore vendored
View file

@ -2,4 +2,6 @@
/node_modules /node_modules
/l10n/* /l10n/*
/releases /releases
/package-lock.json
!/l10n/.gitkeep !/l10n/.gitkeep
/yarn*.log

View file

@ -1,8 +0,0 @@
{
"bracketSpacing": true,
"bracketSameLine": false,
"semi": false,
"singleQuote": true,
"singleAttributePerLine": true,
"printWidth": 160
}

95
.woodpecker.yml Normal file
View file

@ -0,0 +1,95 @@
steps:
dependencies:
image: node:16
pull: true
commands:
- npm i
when:
event: [tag, push, pull_request, manual]
branch: [master, develop, feature/*, fix/*, bugfix/*, translations]
osv-detector:
image: gitnet.fr/deblan/osv-detector:v0.10
commands:
- osv-detector package-lock.json
failure: ignore
build-js:
image: node:16
commands:
- npm run build
when:
event: [tag, push, pull_request, manual]
branch: [master, develop, feature/*, fix/*, bugfix/*, translations]
build-translations:
image: deblan/php:8.0
commands:
- php bin/generate_l10n.php
when:
event: [tag, push, pull_request, manual]
branch: [master, develop, feature/*, fix/*, bugfix/*, translations]
create-signature:
image: nextcloud:25
secrets: [app_certificate, app_public_certificate]
environment:
SQLITE_DATABASE: /var/www/data/data.db
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: admin
commands:
- 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/
when:
event: [tag]
# check-code-quality:
# image: sonarsource/sonar-scanner-cli
# secrets: [sonar_token, sonar_host, sonar_project]
# commands:
# - sonar-scanner
# -Dsonar.projectKey=$SONAR_PROJECT
# -Dsonar.sources=.
# -Dsonar.host.url=$SONAR_HOST
# -Dsonar.pullrequest.key=$CI_COMMIT_PULL_REQUEST
# -Dsonar.pullrequest.branch=$CI_COMMIT_SOURCE_BRANCH
# -Dsonar.pullrequest.base=$CI_COMMIT_TARGET_BRANCH
# failure: ignore
# when:
# event: [pull_request]
create-package:
image: deblan/php:8.0
volumes:
- /var/www/html/artifacts:/var/www/html/artifacts
secrets: [app_certificate]
commands:
- 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 "<version>" 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]
push-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]

View file

@ -1,22 +0,0 @@
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

View file

@ -1,66 +0,0 @@
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 "<version>$TAG</version>" 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 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 "<version>" 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//}/*

View file

@ -1,17 +0,0 @@
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

View file

@ -1,147 +1,5 @@
## [Unreleased] ## [Unreleased]
## 5.2.2
### Fixed
* fix #464: add Intl.Segmenter polyfill
### Added
* add new translations
## 5.2.1
### Added
* chore: set side_menu as package name
### Fixed
* fix(LangRepository): check orm capabilities to query entities
* fix(admin/\*SaveButton): cast settings to string
## 5.2.0
### Added
* add compatibility with NC33
### Fixed
* fix #468: force nextcloud logo display css rule (opener-only)
## 5.1.3
### Fixed
* fix #445: fix build by adding package-lock.json
## 5.1.2
### Added
* add new translations
### Fixed
* fix #441: Side bar not working with Nextcloud 32 (thanks to AndyXheli)
## 5.1.1
### Fixed
* fix(build): define appName to fix this error: "The `@nextcloud/vue` library was used without setting / replacing the `appName`"
* fix #349: add custom controller to retrieve core apps
## 5.1.0
### Added
* fix #425: allow to set a color using hex code
### Fixed
* #422: usage of `OC\AppFramework\Http\Request` instead of `$_SERVER`
## 5.0.3
### Fixed
* fix #422: undefined array key "HTTP_USER_AGENT"
## 5.0.2
### Fixed
* fix #413: add user-agent check for memories mobile app
* fix #418: allow non admin user to access their settings
## 5.0.1
### Fixed
* fix(StandardMenu): appLimit must return a value > 0
## 5.0.0
### Fixed
* fix apps's order in the standard menu
### Added
* add new translations
* add route `/apps/side_menu/user/config`
* add new UI for admin and personals settings
### Changed
* migrate to Vue 3 and so add/update or remove dependencies
* replace CSS with SCSS
* remove route `/apps/side_menu/js/script`
* remove generated Javascript using PHP
* rewrite the standard menu of Nextcloud
### Security
* fix CVE-2023-44270
* fix CVE-2024-9506
* fix CVE-2024-6783
## 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
* fully apply Nextcloud AppMenu.vue updates
### Fixed
* add accessibility to open and close buttons (#311)
* 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 ## 3.11.2
### Fixed ### Fixed
* add default translations for Slovak - fix #298 * add default translations for Slovak - fix #298
@ -226,7 +84,7 @@
## 3.5.1 ## 3.5.1
### Added ### Added
* add translations (thanks to p-bo and gallegonovato) * add translations (thanks to p-bo adn gallegonovato)
### Fixed ### Fixed
* fix #189: sorting not applied on mobile * fix #189: sorting not applied on mobile
@ -355,323 +213,323 @@
## 2.3.2 ## 2.3.2
### Fixed ### Fixed
* fix hidden menu - fix hidden menu
## 2.3.1 ## 2.3.1
### Fixed ### Fixed
* fix #88: does not work with default menu - fix #88: does not work with default menu
## 2.3.0 ## 2.3.0
### Added ### Added
* fix #82: add an option to keep visible an app in both menus - fix #82: add an option to keep visible an app in both menus
* fix #83: add custom categories - fix #83: add custom categories
* add auto-reload when settings are saved - add auto-reload when settings are saved
## 2.2.0 ## 2.2.0
### Added ### Added
* fix #84: update icons - fix #84: update icons
* fix #85: use Nextcloud colors by default - fix #85: use Nextcloud colors by default
### Fixed ### Fixed
* fix categories order in large menu - fix categories order in large menu
## 2.1.0 ## 2.1.0
### Added ### Added
* add compatibility with Nextcloud 23 - add compatibility with Nextcloud 23
## 2.0.1 ## 2.0.1
### Fixed ### Fixed
* fix #78: Top menu is broken - invisible apps are shown - fix #78: Top menu is broken - invisible apps are shown
* fix #77: Update personal settings - HTTP error 412 (Precondition Failed) - fix #77: Update personal settings - HTTP error 412 (Precondition Failed)
* fix js error on the personal settings page (undefined sortable) - fix js error on the personal settings page (undefined sortable)
## 2.0.0 ## 2.0.0
### Fixed ### Fixed
* fix #66: removing usage of setInterval - fix #66: removing usage of setInterval
* fix #73: icon background - fix #73: icon background
### Changed ### Changed
* fix #67: replace jQuery with Vanilla JS - fix #67: replace jQuery with Vanilla JS
### Removed ### Removed
* Nextcloud 18 is not supported anymore - Nextcloud 18 is not supported anymore
## 1.28.0 ## 1.28.0
### Added ### Added
* fix #63: add a new side menu with categories - fix #63: add a new side menu with categories
## 1.27.2 ## 1.27.2
### Fixed ### Fixed
* fix #62: hide app notification icon - fix #62: hide app notification icon
## 1.27.1 ## 1.27.1
### Fixed ### Fixed
* fix German translation render - fix German translation render
## 1.27.0 ## 1.27.0
### Added ### Added
* hide personal settings access when settings are forced by the administrator - hide personal settings access when settings are forced by the administrator
### Fixed ### Fixed
* improve German translations - improve German translations
## 1.26.0 ## 1.26.0
### Added ### Added
* add Czech translation - add Czech translation
## 1.25.2 ## 1.25.2
### Fixed ### Fixed
* fix CHANGELOG - fix CHANGELOG
## 1.25.1 ## 1.25.1
### Added ### Added
* add PHP version as dependency - add PHP version as dependency
* add chinese translation - add chinese translation
### Fixed ### Fixed
* fix CHANGELOG - fix CHANGELOG
## 1.25.0 ## 1.25.0
### Added ### Added
* add compatibility with Nextcloud 22 - add compatibility with Nextcloud 22
* add CHANGELOG.md (fix #59) - add CHANGELOG.md (fix #59)
* update app icon - update app icon
## 1.24.0 ## 1.24.0
### Added ### Added
* add option to define the background opacity (fix #53) - add option to define the background opacity (fix #53)
* add missing translations - add missing translations
## 1.23.1 ## 1.23.1
### Fixed ### Fixed
* fix but wih dark mode opener option - fix but wih dark mode opener option
## 1.23.0 ## 1.23.0
### Added ### Added
* add support of dark Theme and Breeze Dark - add support of dark Theme and Breeze Dark
* add support of Nextcloud 21 - add support of Nextcloud 21
## 1.22.2 ## 1.22.2
### Fixed ### Fixed
* fix regression: apps does not open in new tab (fix #55) - fix regression: apps does not open in new tab (fix #55)
## 1.22.1 ## 1.22.1
### Fixed ### Fixed
* fix regression: apps does not open in new tab - fix regression: apps does not open in new tab
## 1.22.0 ## 1.22.0
### Added ### Added
* Add option to sort categories (fix #53) - Add option to sort categories (fix #53)
* Update admin UI - Update admin UI
## 1.21.0 ## 1.21.0
### Added ### Added
* [FEATURE] Logo in the menu links to main page of installation (#51) - [FEATURE] Logo in the menu links to main page of installation (#51)
## 1.20.1 ## 1.20.1
### Fixed ### Fixed
* Fix translations - Fix translations
## 1.20.0 ## 1.20.0
### Added ### Added
* [FEATURE] Ability to remove apps from the Big Menu (#49) - [FEATURE] Ability to remove apps from the Big Menu (#49)
## 1.19.1 ## 1.19.1
### Fixed ### Fixed
* fix #47: setting for list/grid view in files app flashes and dissapears - fix #47: setting for list/grid view in files app flashes and dissapears
## 1.19.0 ## 1.19.0
### Added ### Added
* add option: the menu is enabled by default for users (fix #46) - add option: the menu is enabled by default for users (fix #46)
## 1.18.0 ## 1.18.0
### Added ### Added
* add option to show link to settings (fix #44) - add option to show link to settings (fix #44)
* refactor menus using several components - refactor menus using several components
## 1.17.0 ## 1.17.0
### Added ### Added
* compliance with the app checker - compliance with the app checker
* add an action to export the configuration - add an action to export the configuration
* add an action to purge the cache - add an action to purge the cache
## 1.16.3 ## 1.16.3
### Fixed ### Fixed
* fix links that must be opened in new window (https://help.nextcloud.com/t/external-petit-probleme-concernant-lapplication-external-sites-ou-sites-externes/94884/11) - fix links that must be opened in new window (https://help.nextcloud.com/t/external-petit-probleme-concernant-lapplication-external-sites-ou-sites-externes/94884/11)
## 1.16.2 ## 1.16.2
### Fixed ### Fixed
* fix issue with personal settings when global settings are forced - fix issue with personal settings when global settings are forced
## 1.16.1 ## 1.16.1
### Fixed ### Fixed
* fix #42: add cache to manage failures to access apps.nextcloud.com - fix #42: add cache to manage failures to access apps.nextcloud.com
* fix #41: side menu was hover apps list - fix #41: side menu was hover apps list
## 1.16.0 ## 1.16.0
### Added ### Added
* add an option to force settings to users (fix #38) - add an option to force settings to users (fix #38)
## 1.15.0 ## 1.15.0
### Changed ### Changed
* New name - New name
### Fixed ### Fixed
* Fix #36: always displayed is not expanding - Fix #36: always displayed is not expanding
## 1.14.0 ## 1.14.0
### Added ### Added
* add an api accessed by components - add an api accessed by components
* add a config proxy in controllers - add a config proxy in controllers
### Fixed ### Fixed
* fix translations - fix translations
## 1.13.0 ## 1.13.0
### Added ### Added
* add an option to display the avatar instead of the logo (fix #34) - add an option to display the avatar instead of the logo (fix #34)
## 1.12.3 ## 1.12.3
### Added ### Added
* add a delay before moving elements (fix #33) - add a delay before moving elements (fix #33)
## 1.12.2 ## 1.12.2
### Fixed ### Fixed
* fix #30: `Always displayed` menu can not be close using touchscreens - fix #30: `Always displayed` menu can not be close using touchscreens
## 1.12.1 ## 1.12.1
### Fixed ### Fixed
* fix typo - fix typo
* fix translations - fix translations
## 1.12.0 ## 1.12.0
### Fixed ### Fixed
* fix #30: administrators and users can select what apps must be opened in new tab - fix #30: administrators and users can select what apps must be opened in new tab
* fix typo - fix typo
## 1.11.0 ## 1.11.0
### Added ### Added
* add the option for opening apps in new tab (fix #29) - add the option for opening apps in new tab (fix #29)
### Fixed ### Fixed
* fix issue with the header of the always displayed menu - fix issue with the header of the always displayed menu
## 1.10.0 ## 1.10.0
### Added ### Added
* add images to select the display of the menu - add images to select the display of the menu
* add live preview - add live preview
* update translations - update translations
* update app info - update app info
## 1.9.3 ## 1.9.3
### Fixed ### Fixed
* fix regression with logo display - fix regression with logo display
## 1.9.2 ## 1.9.2
### Added ### Added
* add the option “always displayed” which fixes the position of the menu to the left and always displays the application icons (fix #21, fix #2) - add the option “always displayed” which fixes the position of the menu to the left and always displays the application icons (fix #21, fix #2)
## 1.8.6 ## 1.8.6
### Added ### Added
* add translations: `fr` and `de` - add translations: `fr` and `de`
* improve and publish the `Makefile` - improve and publish the `Makefile`
* update documentation - update documentation
## 1.8.5 ## 1.8.5
### Fixed ### Fixed
* fix #28: menu items invisible after 1.8.4 - fix #28: menu items invisible after 1.8.4
## 1.8.4 ## 1.8.4
### Fixed ### Fixed
* fix #27: disable side menu on public pages - broken in 1.8.3 - fix #27: disable side menu on public pages - broken in 1.8.3
## 1.8.3 ## 1.8.3
### Fixed ### Fixed
* fix alphabetic order of apps (#26) - fix alphabetic order of apps (#26)
## 1.8.2 ## 1.8.2
### Added ### Added
* add icon for closing the big menu (fix #25) - add icon for closing the big menu (fix #25)
### Fixed ### Fixed
* fix hidden icons in the top menu (fix #23) - fix hidden icons in the top menu (fix #23)
* fix missing apps (fix #24) - fix missing apps (fix #24)
## 1.8.1 ## 1.8.1
### Fixed ### Fixed
* fix issue with links - fix issue with links
* fix missing l10n files - fix missing l10n files
## 1.8.0 ## 1.8.0
### Added ### Added
* add a `big menu` display (fix #22) - add a `big menu` display (fix #22)
* add the possibility to choose what apps are displayed in the top menu (fix #22) - add the possibility to choose what apps are displayed in the top menu (fix #22)
* add icon color filter - add icon color filter
* add icon opacity filter - add icon opacity filter
## 1.8.0-rc2 ## 1.8.0-rc2
## 1.7.0 ## 1.7.0
### Added ### Added
* add a loader when the page is unloading - add a loader when the page is unloading
* add compatibility with Nextcloud 19 - add compatibility with Nextcloud 19
## 1.6.3 ## 1.6.3
### Fixed ### Fixed
* fix #20: add a shortcut to open and to hide the menu - fix #20: add a shortcut to open and to hide the menu
## 1.6.2 ## 1.6.2
### Fixed ### Fixed
* fix #19: add a cache to limit flashes - fix #19: add a cache to limit flashes
## 1.6.1 ## 1.6.1
### Fixed ### Fixed
* fix #19: add a hack to show external sites in the top menu with navigation - fix #19: add a hack to show external sites in the top menu with navigation
## 1.6.0 ## 1.6.0
### Added ### Added
* Add a page of personal settings - Add a page of personal settings
* Add an option to disable the side menu as user - Add an option to disable the side menu as user
* Refactoring of javascripts - Refactoring of javascripts
## 1.5.0 ## 1.5.0
### Added ### Added
* add option to force light icons instead of dark icons - add option to force light icons instead of dark icons
### Fixed ### Fixed
* fix #19: add option to keep external sites in the top menu - fix #19: add option to keep external sites in the top menu
* fix #16 #17: add dark icons and handle the svg filters - fix #16 #17: add dark icons and handle the svg filters
## 1.4.1 ## 1.4.1
### Added ### Added
* Rendering harmonization with browsers - Rendering harmonization with browsers
### Fixed ### Fixed
* FIX #15: make menu start after icon - FIX #15: make menu start after icon
## 1.4.0 ## 1.4.0
### Fixed ### Fixed
* Fix #12: add an option to hide the opener and the panel when there is no application - Fix #12: add an option to hide the opener and the panel when there is no application
## 1.3.4 ## 1.3.4
### Fixed ### Fixed
* FIX #14: add option to show only the opener - FIX #14: add option to show only the opener
* FIX #13: add alternate hamburger icon - FIX #13: add alternate hamburger icon
* FIX #11: remove error in the console - FIX #11: remove error in the console
## 1.3.3 ## 1.3.3
### Added ### Added
* Add a second background color to create a background gradiant - Add a second background color to create a background gradiant
* Update documentation - Update documentation
### Fixed ### Fixed
* FIX #10: add options to select the size of the icons and the text, or hide them - FIX #10: add options to select the size of the icons and the text, or hide them
## 1.3.2 ## 1.3.2
### Fixed ### Fixed
* FIX #9: opener icon not visible with multiple apps_paths - FIX #9: opener icon not visible with multiple apps_paths
## 1.3.1 ## 1.3.1
### Fixed ### Fixed
* FIX #3: Add an opition the choose the position of the opener (after or before the logo) - FIX #3: Add an opition the choose the position of the opener (after or before the logo)
* FIX #8: Remove extension of dynamic asset's routes (js, css) - FIX #8: Remove extension of dynamic asset's routes (js, css)
## 1.3.0 ## 1.3.0
### Added ### Added
* `main.js` is replaced by a controller and a template - `main.js` is replaced by a controller and a template
### Fixed ### Fixed
* FIX #2: add option to open the menu by hovering over opener - FIX #2: add option to open the menu by hovering over opener
## 1.2.4 ## 1.2.4
### Fixed ### Fixed
* FIX #7: Opener icon not visible in Safari - FIX #7: Opener icon not visible in Safari

View file

@ -6,6 +6,7 @@ watch: dep
dep: dep:
npm i npm i
npm link @nextcloud/vue || sudo npm link @nextcloud/vue
.ONESHELL: .ONESHELL:
release: release:
@ -20,7 +21,7 @@ release:
test -d $$RELEASE_DIRECTORY/$$VERSION && rm -fr $$RELEASE_DIRECTORY/$$VERSION test -d $$RELEASE_DIRECTORY/$$VERSION && rm -fr $$RELEASE_DIRECTORY/$$VERSION
mkdir -p $$RELEASE_DIRECTORY/$$VERSION/side_menu mkdir -p $$RELEASE_DIRECTORY/$$VERSION/side_menu
cp -r README.md CHANGELOG.md appinfo lib img l10n js src templates screenshots vendor $$RELEASE_DIRECTORY/$$VERSION/side_menu cp -r README.md CHANGELOG.md appinfo css lib img l10n js src templates screenshots vendor $$RELEASE_DIRECTORY/$$VERSION/side_menu
cd $$RELEASE_DIRECTORY/$$VERSION cd $$RELEASE_DIRECTORY/$$VERSION
zip -r side_menu_v$$VERSION.zip side_menu zip -r side_menu_v$$VERSION.zip side_menu
tar cvzf side_menu_v$$VERSION.tar.gz side_menu tar cvzf side_menu_v$$VERSION.tar.gz side_menu

View file

@ -24,7 +24,8 @@ You like this app and you want to support me? ☕ [Buy me a coffee](https://www.
Requirements Requirements
------------ ------------
* PHP >= 8.1 * PHP >= 8.0
* App `theming` enabled
Installation and upgrade Installation and upgrade
------------------------ ------------------------
@ -40,7 +41,7 @@ If you want to install it from source, go to https://gitnet.fr/deblan/side_menu/
``` ```
$ cd /path/to/nextcloud/apps $ cd /path/to/nextcloud/apps
$ VERSION=x.y.z; curl -sS "https://gitnet.fr/deblan/side_menu/releases/download/${VERSION}/side_menu_v${VERSION}.tar.gz" | tar xvfz - $ curl -sS https://gitnet.fr/attachments/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | tar xvfz -
``` ```
Administrators can edit many settings using the administration page. Administrators can edit many settings using the administration page.

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> <info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>side_menu</id> <id>side_menu</id>
<name>Custom menu</name> <name>Custom menu</name>
<summary>Modify the display of the menu.</summary> <summary>Modify the display of the menu.</summary>
@ -10,13 +11,14 @@ 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. 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. You can customize colors depending of the theme (Dark theme and Breeze Dark).
To report a bug or request a feature, please open an issue. You can report a bug or request a feature by opening an issue.
Requirements: Requirements:
* PHP >= 8.1 * PHP >= 8.0
* App `theming` enabled
If you like this application and if you want to support the development: If you like this application and if you want to support the development:
@ -30,9 +32,9 @@ Notice
Because I believe in a free and decentralized Internet, [Gitnet](https://gitnet.fr) is **self-hosted at home**. 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/). In case of downtime, you can download **Custom Menu** from [here](https://kim.deblan.fr/~side_menu/).
]]></description> ]]></description>
<version>5.2.2</version> <version>3.11.2</version>
<licence>agpl</licence> <licence>agpl</licence>
<author mail="contact@deblan.fr" homepage="https://www.deblan.fr/">Simon Vieille</author> <author mail="contact@deblan.fr" homepage="https://www.deblan.io/">Simon Vieille</author>
<namespace>SideMenu</namespace> <namespace>SideMenu</namespace>
<documentation> <documentation>
<admin>https://deblan.gitnet.page/side_menu_doc/</admin> <admin>https://deblan.gitnet.page/side_menu_doc/</admin>
@ -40,20 +42,20 @@ In case of downtime, you can download **Custom Menu** from [here](https://kim.de
</documentation> </documentation>
<category>customization</category> <category>customization</category>
<website>https://gitnet.fr/deblan/side_menu</website> <website>https://gitnet.fr/deblan/side_menu</website>
<discussion><![CDATA[https://matrix.to/#/!TFPucDATKODpHNVAtu:neutralnetwork.org?via=neutralnetwork.org]]></discussion> <discussion>https://matrix.to/#/!TFPucDATKODpHNVAtu:neutralnetwork.org?via=neutralnetwork.org</discussion>
<bugs>https://gitnet.fr/deblan/side_menu/issues</bugs> <bugs>https://gitnet.fr/deblan/side_menu/issues</bugs>
<repository type="git">https://gitnet.fr/deblan/side_menu</repository> <repository type="git">https://gitnet.fr/deblan/side_menu</repository>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc19_default_menu.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc19_default_menu.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/admin_settings.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/admin_settings.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/n19_big_menu.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/n19_big_menu.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc18_menu_always_displayed.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc18_menu_always_displayed.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc20_big_menu_responsive.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc20_big_menu_responsive.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/personal_settings.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/personal_settings.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_big_menu.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_big_menu.png</screenshot>
<screenshot><![CDATA[https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_default_menu.png]]></screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_default_menu.png</screenshot>
<dependencies> <dependencies>
<php min-version="8.1" max-version="8.4" /> <nextcloud min-version="25" max-version="28"/>
<nextcloud min-version="31" max-version="33"/> <php min-version="7.4"/>
</dependencies> </dependencies>
<settings> <settings>
<admin>OCA\SideMenu\Settings\Admin</admin> <admin>OCA\SideMenu\Settings\Admin</admin>

View file

@ -1,3 +1,5 @@
<?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -15,18 +17,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { loadState } from '@nextcloud/initial-state' return [
'routes' => [
const getActiveAppId = () => { ['name' => 'App#index', 'url' => '/', 'verb' => 'GET'],
const apps = loadState('core', 'apps', {}) ['name' => 'Css#stylesheet', 'url' => '/css/stylesheet', 'verb' => 'GET'],
['name' => 'Js#script', 'url' => '/js/script', 'verb' => 'GET'],
for (let id in apps) { ['name' => 'Js#config', 'url' => '/js/config', 'verb' => 'GET'],
if (apps[id].active) { ['name' => 'Nav#items', 'url' => '/nav/items', 'verb' => 'GET'],
return apps[id].id ['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'],
],
return null ];
}
export { getActiveAppId }

View file

@ -31,7 +31,6 @@ function generateJsonContent($translations)
chdir(__DIR__.'/../'); chdir(__DIR__.'/../');
foreach (glob('src/l10n/fixtures/*.yaml') as $file) { foreach (glob('src/l10n/fixtures/*.yaml') as $file) {
echo "$file\n";
$lang = str_replace('.yaml', '', basename($file)); $lang = str_replace('.yaml', '', basename($file));
$translations = yaml_parse(file_get_contents($file)); $translations = yaml_parse(file_get_contents($file));

View file

@ -1,75 +1,21 @@
<?php <?php
/**
* Imports a json configuration into a sqlite database.
*
* Usage:
* php bin/import_config.php /path/to/config.json /path/to/owncloud.db
*/
function showUsageAndExit(int $code) $configFile = $argv[1];
{ $databaseFile = $argv[2];
global $argv;
echo "${argv[0]} [--help] --config /path/to/config/config.php --file /path/to/config.json\n"; $content = file_get_contents($configFile);
$config = json_decode($content, true);
exit($code); $pdo = new \Pdo(sprintf('sqlite:%s', $databaseFile));
} $stmt = $pdo->prepare('UPDATE oc_appconfig SET configvalue=:value WHERE configkey=:key and appid=:appId');
function value(string $shortName, string $longName, array $options, bool $required = true): ?string foreach ($config as $key => $value) {
{
$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([ $stmt->execute([
'appId' => 'side_menu', 'appId' => 'side_menu',
'key' => $key, 'key' => $key,

220
css/admin.css Normal file
View file

@ -0,0 +1,220 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#side-menu-section input[type="color"] {
width: 100px;
margin: 10px 0 10px 0;
padding: 0;
border-radius: 0;
}
#-dropside-menu-section input[type="checkbox"] {
vertical-align: middle;
}
#side-menu-section input[type="range"] {
vertical-align: middle;
}
#side-menu-section select {
margin: 10px 0 10px 0;
}
.keyboard-key {
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;
}
.side-menu-display.is-active {
border: 2px solid #91cb7f;
}
.info {
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;
}
.side-menu-toggler {
cursor: pointer;
}
.side-menu-setting-list {
margin: 10px 4px 4px 0px;
border: 2px solid var(--color-border-dark);
border-radius: 15px;
}
.side-menu-setting-list-item {
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;
}
.side-menu-setting.arrow {
color: #ccc;
padding-right: 5px;
}
.side-menu-setting-list-item input {
margin-top: 0;
height: 21px !important;
min-height: auto !important;
}
#apps-categories-custom-list select {
width: 100%;
}
.side-menu-setting-table {
display: table;
width: 100%;
}
.side-menu-setting-row {
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: 430px;
padding-right: 20px;
}
.side-menu-setting-label--top {
vertical-align: top;
}
.side-menu-setting-form {
display: table-cell;
min-width: 300px;
}
.side-menu-setting-label-short {
width: 300px;
}
.side-menu-setting-form-long {
width: 400px;
}
#side-menu-save-progress {
display: inline-block;
width: 0;
height: 15px;
background: #fff;
}
.btn-reset {
display: inline-block;
cursor: pointer;
position: 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;
}

370
css/sideMenu.css Normal file
View file

@ -0,0 +1,370 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#side-menu {
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;
}
#side-menu.open {
display: block;
}
#header .side-menu-opener {
margin-left: 0px;
margin-top: -1px;
}
.side-menu-settings {
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;
}
.side-menu-settings:hover a, .side-menu-settings a:active, .side-menu-settings a:focus {
background: var(--side-menu-current-app-background-color, #444);
}
.side-menu-settings img {
vertical-align: bottom;
margin-left: 3px;
width: 32px;
height: 32px;
}
#side-menu.open .side-menu-settings {
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;
}
.side-menu-opener:active, .side-menu-opener:focus {
background-color: var(--side-menu-current-app-background-color, #444) !important;
}
.side-menu-closer {
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;
}
.side-menu-apps-list {
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: 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;
}
.side-menu-app a:hover, .side-menu-app.active, .side-menu-app a:focus {
background: var(--side-menu-current-app-background-color, #444);
}
.side-menu-logo {
text-align: center;
}
.side-menu-logo img {
max-width: 60%;
max-height: 100px;
}
.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;
}
#side-menu.hide-opener .side-menu-logo {
margin-top: 10px;
}
#side-menu-loader {
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;
}
#side-menu.side-menu-big, #side-menu.side-menu-with-categories {
max-width: 100%;
height: auto;
}
.side-menu-big .side-menu-header, .side-menu-with-categories .side-menu-header {
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;
}
.side-menu-big .side-menu-app a, .side-menu-with-categories .side-menu-app a {
padding: 7px 0 7px 7px;
}
.side-menu-categories-wrapper {
padding-bottom: 70px;
}
.side-menu-categories {
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;
}
.side-menu-category-title {
padding-left: 10px;
color: var(--side-menu-text-color, #fff);
}
.side-menu-loader {
text-align: center;
}
.side-menu-loader svg {
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 body {
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;
}
.side-menu-always-displayed .side-menu-apps-list {
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;
}
.side-menu-always-displayed #side-menu,
.side-menu-always-displayed .side-menu-header,
.side-menu-always-displayed .side-menu-apps-list {
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;
}
.side-menu-always-displayed #side-menu .side-menu-header {
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%;
}
.side-menu-always-displayed #side-menu.open .side-menu-app-text {
display: inline;
}
.side-menu-always-displayed .app-navigation--close {
transform: translateX(calc(-100% + 50px)) !important;
}
#side-menu.side-menu-with-categories {
max-width: 290px;
height: 100vh;
}
.side-menu-with-categories .side-menu-categories {
display: block;
padding: 0;
width: 100%;
}
.side-menu-with-categories .side-menu-category {
padding: 10px 0;
}
.side-menu-always-displayed #body-settings, #body-settings.body-settings-side-menu {
overflow-x: visible;
}
.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.hide-opener.side-menu-big .side-menu-search {
float: none;
}
.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-big .side-menu-header {
max-width: 100%;
}
}

View file

@ -2,25 +2,17 @@
namespace OCA\SideMenu\AppInfo; namespace OCA\SideMenu\AppInfo;
use OC\AllConfig; use OC;
use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\AppFramework\Http\Request;
use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OC\User\User; use OC\User\User;
use OCA\SideMenu\Service\AppRepository; use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\CategoryRepository; use OCA\SideMenu\Service\CategoryRepository;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\App; use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\INavigationManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Util; use OCP\Util;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -32,75 +24,33 @@ use Psr\Container\ContainerInterface;
class Application extends App implements IBootstrap class Application extends App implements IBootstrap
{ {
public const APP_ID = 'side_menu'; public const APP_ID = 'side_menu';
public const APP_NAME = 'Custom menu'; public const APP_NAME = 'Custom menu';
/**
* @var OC\AllConfig
*/
protected $config;
protected AllConfig $config; /**
protected ContentSecurityPolicyNonceManager $cspnm; * @var ContentSecurityPolicyNonceManager
protected Request $request; */
protected ?User $user = null; protected $cspnm;
/**
* @var User
*/
protected $user;
/**
* {@inheritdoc}
*/
public function __construct(array $urlParams = []) public function __construct(array $urlParams = [])
{ {
parent::__construct(self::APP_ID, $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),
);
});
$context->registerService(Color::class, function (ContainerInterface $c) {
return new Color(
$c->get(ThemingDefaults::class),
);
});
}
public function boot(IBootContext $context): void
{
$this->config = \OC::$server->getConfig();
$this->cspnm = \OC::$server->getContentSecurityPolicyNonceManager();
$this->user = \OC::$server[IUserSession::class]->getUser();
$this->request = \OC::$server->getRequest();
if (!$this->isEnabled()) {
return;
}
$this->addAssets();
}
protected function isEnabled(): bool protected function isEnabled(): bool
{ {
if (isset($this->request->server['HTTP_USER_AGENT']) && preg_match('/MemoriesNative/', $this->request->server['HTTP_USER_AGENT'])) {
return false;
}
$enabled = true; $enabled = true;
$isForced = (bool) $this->config->getAppValue(self::APP_ID, 'force', '0'); $isForced = (bool) $this->config->getAppValue(self::APP_ID, 'force', '0');
@ -122,7 +72,8 @@ class Application extends App implements IBootstrap
protected function addAssets() protected function addAssets()
{ {
Util::addScript(self::APP_ID, 'side_menu-menu'); Util::addScript(self::APP_ID, 'sideMenu');
Util::addStyle(self::APP_ID, 'sideMenu');
$assets = [ $assets = [
'stylesheet' => [ 'stylesheet' => [
@ -133,19 +84,51 @@ class Application extends App implements IBootstrap
'rel' => 'stylesheet', 'rel' => 'stylesheet',
], ],
], ],
'script' => [
'route' => 'side_menu.Js.script',
'type' => 'script',
'route_attr' => 'src',
'attr' => [
'nonce' => $this->cspnm->getNonce(),
],
],
]; ];
$cache = $this->config->getAppValue(self::APP_ID, 'cache', '0'); $cache = $this->config->getAppValue(self::APP_ID, 'cache', '0');
foreach ($assets as $value) { foreach ($assets as $value) {
$route = \OC::$server->getURLGenerator()->linkToRoute( $route = OC::$server->getURLGenerator()->linkToRoute($value['route'], ['v' => $cache]);
$value['route'],
['v' => $cache]
);
$value['attr'][$value['route_attr']] = $route; $value['attr'][$value['route_attr']] = $route;
Util::addHeader($value['type'], $value['attr'], ''); 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();
}
} }

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -20,36 +19,40 @@
namespace OCA\SideMenu\Controller; namespace OCA\SideMenu\Controller;
use OCA\SideMenu\AppInfo\Application; use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy;
use OCA\SideMenu\Service\LangRepository;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\IConfig; use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
use OCP\IURLGenerator; use OCP\IURLGenerator;
class AdminSettingController extends Controller class AdminSettingController extends Controller
{ {
public function __construct( /**
$appName, * @var IConfig
IRequest $request, */
protected IConfig $config, protected $config;
protected ConfigProxy $configProxy,
protected IURLGenerator $urlGenerator, /**
protected Color $color, * @var IURLGenerator
protected LangRepository $langRepository, */
) { protected $urlGenerator;
public function __construct($appName, IRequest $request, IConfig $config, IURLGenerator $urlGenerator)
{
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->config = $config;
$this->urlGenerator = $urlGenerator;
} }
#[NoCSRFRequired] /**
#[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')] * @NoCSRFRequired
public function removeCache(): RedirectResponse *
* @return RedirectResponse
*/
public function removeCache()
{ {
$this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]'); $this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]');
@ -58,16 +61,18 @@ class AdminSettingController extends Controller
]).'#more'); ]).'#more');
} }
#[NoCSRFRequired] /**
#[FrontpageRoute(verb: 'GET', url: '/admin/config/export')] * @NoCSRFRequired
public function exportConfiguration(): DataDownloadResponse *
* @return Response
*/
public function exportConfiguration()
{ {
$keys = $this->config->getAppKeys(Application::APP_ID); $keys = $this->config->getAppKeys(Application::APP_ID);
$appConfig = []; $appConfig = [];
$excludedKeys = [ $excludedKeys = [
'cache', 'cache',
'cache-categories', 'cache-categories',
'langs',
]; ];
foreach ($keys as $key) { foreach ($keys as $key) {
@ -84,135 +89,4 @@ class AdminSettingController extends Controller
'text/json' 'text/json'
); );
} }
#[NoCSRFRequired]
#[FrontpageRoute(verb: 'GET', url: '/admin/config')]
public function configuration(): JSONResponse
{
$keys = $this->config->getAppKeys(Application::APP_ID);
$booleans = [
'opener-only',
'opener-hover',
'display-logo',
'use-avatar',
'add-logo-link',
'show-settings',
'loader-enabled',
'always-displayed',
'enabled',
'force',
'big-menu',
'external-sites-in-top-menu',
'force-light-icon',
'side-with-categories',
'default-enabled',
];
$arrays = [
'apps-categories-custom',
'big-menu-hidden-apps',
'apps-order',
'categories-custom',
'categories-order',
'target-blank-apps',
'top-menu-apps',
'top-side-menu-apps',
];
$integers = [
'background-color-opacity',
'dark-mode-background-color-opacity',
'dark-mode-icon-invert-filter',
'dark-mode-icon-opacity',
'icon-invert-filter',
'icon-opacity',
'target-blank-mode',
'top-menu-mouse-over-hidden-label',
];
$defaults = [
'opener-only' => '0',
'opener-hover' => '0',
'display-logo' => '1',
'use-avatar' => '0',
'add-logo-link' => '1',
'show-settings' => '0',
'loader-enabled' => '1',
'always-displayed' => '0',
'enabled' => '1',
'force' => '0',
'big-menu' => '0',
'external-sites-in-top-menu' => '0',
'force-light-icon' => '0',
'side-with-categories' => '0',
'cache' => '1',
'default-enabled' => '1',
'apps-categories-custom' => '[]',
'big-menu-hidden-apps' => '[]',
'apps-order' => '[]',
'categories-custom' => '[]',
'categories-order' => '[]',
'target-blank-apps' => '[]',
'top-menu-apps' => '[]',
'top-side-menu-apps' => '[]',
'cache-categories' => '[]',
'background-color-opacity' => '100',
'dark-mode-background-color-opacity' => '100',
'dark-mode-icon-invert-filter' => '0',
'dark-mode-icon-opacity' => '100',
'icon-invert-filter' => '0',
'icon-opacity' => '100',
'top-menu-mouse-over-hidden-label' => '0',
'opener' => 'side-menu-opener',
'dark-mode-opener' => 'side-menu-opener',
'size-icon' => 'normal',
'size-text' => 'normal',
'opener-position' => 'before',
'background-color' => $this->color->getPrimaryColor(),
'background-color-to' => $this->color->getLightenPrimaryColor(),
'current-app-background-color' => $this->color->getDarkenPrimaryColor(),
'text-color' => $this->color->getTextColorPrimary(),
'loader-color' => $this->color->getLightenPrimaryColor(),
'dark-mode-background-color' => $this->color->getDarkenPrimaryColor(),
'dark-mode-background-color-to' => $this->color->getDarkenPrimaryColor(),
'dark-mode-current-app-background-color' => $this->color->getDarkenPrimaryColor2(),
'dark-mode-text-color' => $this->color->getTextColorPrimary(),
'dark-mode-loader-color' => $this->color->getLightenPrimaryColor(),
'categories-order-type' => 'default',
];
$config = [];
foreach ($keys as $key) {
if (!isset($defaults[$key])) {
continue;
}
if (in_array($key, $booleans)) {
$config[$key] = $this->configProxy->getAppValueBool($key, $defaults[$key]);
} elseif (in_array($key, $arrays)) {
$config[$key] = $this->configProxy->getAppValueArray($key, $defaults[$key]);
} elseif (in_array($key, $integers)) {
$config[$key] = $this->configProxy->getAppValueInt($key, $defaults[$key]);
} else {
$config[$key] = $this->configProxy->getAppValue($key, $defaults[$key]);
}
}
foreach ($defaults as $key => $default) {
if (!array_key_exists($key, $config)) {
$config[$key] = $default;
}
}
$config['langs'] = $this->langRepository->getUsedLangs();
return new JSONResponse($config);
}
} }

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -19,12 +18,10 @@
namespace OCA\SideMenu\Controller; namespace OCA\SideMenu\Controller;
use OC;
use OCA\SideMenu\Service\AppRepository; use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCP\AppFramework\Controller; 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\AppFramework\Http\RedirectResponse;
use OCP\IRequest; use OCP\IRequest;
use OCP\IURLGenerator; use OCP\IURLGenerator;
@ -32,22 +29,37 @@ use OCP\IUserSession;
class AppController extends Controller class AppController extends Controller
{ {
/**
* @var ConfigProxy
*/
protected $config;
/**
* @var AppRepository
*/
protected $appRepository;
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
protected AppRepository $appRepository, AppRepository $appRepository,
protected IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
protected ConfigProxy $config, ConfigProxy $config
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->appRepository = $appRepository;
$this->urlGenerator = $urlGenerator;
$this->config = $config;
} }
#[NoCSRFRequired] /**
#[NoAdminRequired] * @NoAdminRequired
#[FrontpageRoute(verb: 'GET', url: '/')] * @NoCSRFRequired
*/
public function index(): RedirectResponse public function index(): RedirectResponse
{ {
$user = \OC::$server[IUserSession::class]->getUser(); $user = OC::$server[IUserSession::class]->getUser();
$topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]');
$hiddenApps = $this->config->getAppValueArray('big-menu-hidden-apps', '[]'); $hiddenApps = $this->config->getAppValueArray('big-menu-hidden-apps', '[]');
$isForced = $this->config->getAppValueBool('force', '0'); $isForced = $this->config->getAppValueBool('force', '0');
@ -75,7 +87,7 @@ class AppController extends Controller
protected function redirectToApp($app, bool $isHref = false): RedirectResponse protected function redirectToApp($app, bool $isHref = false): RedirectResponse
{ {
if (!$isHref) { if (!$isHref) {
$isIgnoreFrontController = true === \OC::$server->getConfig()->getSystemValue( $isIgnoreFrontController = true === OC::$server->getConfig()->getSystemValue(
'htaccess.IgnoreFrontController', 'htaccess.IgnoreFrontController',
false false
); );

View file

@ -1,74 +0,0 @@
<?php
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\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\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
class CoreController extends Controller
{
public function __construct(
string $appName,
IRequest $request,
protected ConfigProxy $config,
protected AppRepository $appRepository,
) {
parent::__construct($appName, $request);
}
#[NoCSRFRequired]
#[NoAdminRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'GET', url: '/core/apps')]
public function items(): JSONResponse
{
$user = \OC::$server[IUserSession::class]->getUser();
$items = [];
if (!$user) {
return new JSONResponse([
'items' => $items,
]);
}
$apps = $this->appRepository->getOrderedApps($user);
$keys = ['id', 'name', 'category', 'href', 'icon'];
foreach ($apps as &$app) {
foreach ($app as $key => $value) {
if (!in_array($key, $keys)) {
unset($app[$key]);
}
}
}
return new JSONResponse([
'items' => $apps,
]);
}
}

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -19,40 +18,63 @@
namespace OCA\SideMenu\Controller; namespace OCA\SideMenu\Controller;
use OC;
use OC\User\User; use OC\User\User;
use OCA\SideMenu\AppInfo\Application; use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\Color; use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCA\Theming\ThemingDefaults; use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserSession; use OCP\IUserSession;
class CssController extends Controller class CssController extends Controller
{ {
protected ?User $user; /**
* @var ConfigProxy
*/
protected $config;
/**
* @var User
*/
protected $user;
/**
* @var ThemingDefaults
*/
protected $theming;
/**
* @var Color
*/
protected $color;
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
protected ConfigProxy $config, ConfigProxy $config,
protected ThemingDefaults $theming, ThemingDefaults $theming,
protected Color $color, Color $color
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->user = \OC::$server[IUserSession::class]->getUser(); $this->user = OC::$server[IUserSession::class]->getUser();
$this->config = $config;
$this->theming = $theming;
$this->color = $color;
} }
#[NoCSRFRequired] /**
#[NoAdminRequired] * @NoAdminRequired
#[PublicPage] * @NoCSRFRequired
#[FrontpageRoute(verb: 'GET', url: '/css/stylesheet')] * @PublicPage
public function stylesheet(): TemplateResponse *
* @return Response
*/
public function stylesheet()
{ {
$response = new TemplateResponse(Application::APP_ID, 'css/stylesheet', $this->getConfig(), 'blank'); $response = new TemplateResponse(Application::APP_ID, 'css/stylesheet', $this->getConfig(), 'blank');
$response->addHeader('Content-Type', 'text/css'); $response->addHeader('Content-Type', 'text/css');
@ -66,6 +88,10 @@ class CssController extends Controller
$topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]');
$topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]'); $topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]');
$isAccessibilityAppEnabled = $this->config->getAppValueBool('enabled', '0', 'accessibility');
$isBreezeDarkAppEnabled = $this->config->getAppValueBool('enabled', '0', 'breezedark');
$isBreezeDarkGlobalEnabled = $this->config->getAppValueBool('theme_enabled', '0', 'breezedark');
if ($this->user) { if ($this->user) {
$userTopMenuApps = $this->config->getUserValueArray($this->user, 'top-menu-apps', '[]'); $userTopMenuApps = $this->config->getUserValueArray($this->user, 'top-menu-apps', '[]');
$userTopSideMenuApps = $this->config->getUserValueArray($this->user, 'top-side-menu-apps', '[]'); $userTopSideMenuApps = $this->config->getUserValueArray($this->user, 'top-side-menu-apps', '[]');
@ -77,62 +103,81 @@ class CssController extends Controller
if (!empty($userTopSideMenuApps) && !$isForced) { if (!empty($userTopSideMenuApps) && !$isForced) {
$topSideMenuApps = $userTopSideMenuApps; $topSideMenuApps = $userTopSideMenuApps;
} }
$isDarkThemeUserEnabled = 'dark' === $this->config->getUserValue($this->user, 'theme', '', 'accessibility');
$isBreezeDarkUserEnabled = $this->config->getUserValue($this->user, 'theme_enabled', '', 'breezedark');
$isBreezeDarkUserEnabled = '1' === $isBreezeDarkUserEnabled ||
($isBreezeDarkGlobalEnabled && '' === $isBreezeDarkUserEnabled);
} else {
$isDarkThemeUserEnabled = false;
$isBreezeDarkUserEnabled = false;
} }
$lightenPrimaryColor = $this->color->getLightenPrimaryColor(); $isDarkMode = ($isAccessibilityAppEnabled && $isDarkThemeUserEnabled) ||
$darkenPrimaryColor = $this->color->getDarkenPrimaryColor(); ($isBreezeDarkAppEnabled && $isBreezeDarkUserEnabled);
$darkenPrimaryColor2 = $this->color->getDarkenPrimaryColor2();
$textColor = $this->color->getTextColorPrimary();
$backgroundColor = $this->config->getAppValue('background-color', $darkenPrimaryColor); $primaryColor = $this->theming->getColorPrimary();
$backgroundColorTo = $this->config->getAppValue('background-color-to', $darkenPrimaryColor); $lightenPrimaryColor = $this->color->adjustBrightness($primaryColor, 0.2);
$opacity = $this->config->getAppValueInt('background-color-opacity', '100'); $darkenPrimaryColor = $this->color->adjustBrightness($primaryColor, -0.2);
$backgroundOpacity = dechex($opacity * 255 / 100); $darkenPrimaryColor2 = $this->color->adjustBrightness($primaryColor, -0.3);
$textColor = $this->theming->getTextColorPrimary();
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
);
$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');
$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
);
$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');
$opacity = $this->config->getAppValueInt('background-color-opacity', '100');
$backgroundOpacity = dechex($opacity * 255 / 100);
}
$backgroundColor .= $backgroundOpacity; $backgroundColor .= $backgroundOpacity;
$backgroundColorTo .= $backgroundOpacity; $backgroundColorTo .= $backgroundOpacity;
$darkBackgroundColor = $this->config->getAppValue('dark-mode-background-color', $darkenPrimaryColor);
$darkBackgroundColorTo = $this->config->getAppValue('dark-mode-background-color-to', $darkenPrimaryColor);
$darkOpacity = $this->config->getAppValueInt('dark-mode-background-color-opacity', '100');
$darkBackgroundOpacity = dechex($opacity * 255 / 100);
$darkBackgroundColor .= $darkBackgroundOpacity;
$darkBackgroundColorTo .= $darkBackgroundOpacity;
return [ return [
'vars' => [ 'vars' => [
'light' => [ 'background-color' => $backgroundColor,
'background-color' => $backgroundColor, 'background-color-to' => $backgroundColorTo,
'background-color-to' => $backgroundColorTo, 'current-app-background-color' => $currentAppBackgroundColor,
'current-app-background-color' => $this->config->getAppValue( 'loader-color' => $loaderColor,
'current-app-background-color', 'text-color' => $textColor,
$darkenPrimaryColor2 'opener' => $opener,
), 'icon-invert-filter' => $iconInvertFilter,
'loader-color' => $this->config->getAppValue('loader-color', $lightenPrimaryColor), 'icon-opacity' => $iconOpacity,
'text-color' => $this->config->getAppValue('text-color', $textColor),
'opener' => $this->config->getAppValue('opener', 'side-menu-opener'),
'icon-invert-filter' => abs($this->config->getAppValueInt('icon-invert-filter', '0')).'%',
'icon-opacity' => abs($this->config->getAppValueInt('icon-opacity', '100') / 100),
],
'dark' => [
'background-color' => $darkBackgroundColor,
'background-color-to' => $darkBackgroundColorTo,
'current-app-background-color' => $this->config->getAppValue(
'dark-mode-current-app-background-color',
$darkenPrimaryColor2
),
'loader-color' => $this->config->getAppValue('dark-mode-loader-color', $lightenPrimaryColor),
'text-color' => $this->config->getAppValue('dark-mode-text-color', $textColor),
'opener' => $this->config->getAppValue('dark-mode-opener', 'side-menu-opener'),
'icon-invert-filter' => abs($this->config->getAppValueInt('dark-mode-icon-invert-filter', '0')).'%',
'icon-opacity' => abs($this->config->getAppValueInt('dark-mode-icon-opacity', '100') / 100),
]
], ],
'display-logo' => $this->config->getAppValueBool('display-logo', '1'),
'opener-only' => $this->config->getAppValueBool('opener-only', '0'), 'opener-only' => $this->config->getAppValueBool('opener-only', '0'),
'external-sites-in-top-menu' => $this->config->getAppValueBool('external-sites-in-top-menu', '0'),
'size-icon' => $this->config->getAppValue('size-icon', 'normal'), 'size-icon' => $this->config->getAppValue('size-icon', 'normal'),
'size-text' => $this->config->getAppValue('size-text', 'normal'), 'size-text' => $this->config->getAppValue('size-text', 'normal'),
'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'), 'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'),
'big-menu' => $this->config->getAppValueBool('big-menu', '0'),
'top-menu-apps' => $topMenuApps,
'top-side-menu-apps' => $topSideMenuApps,
]; ];
} }
} }

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -19,45 +18,74 @@
namespace OCA\SideMenu\Controller; namespace OCA\SideMenu\Controller;
use OC;
use OC\User\User; use OC\User\User;
use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCA\Theming\ThemingDefaults; use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Controller; 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\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use OCP\IAvatarManager;
use OCP\INavigationManager;
use OCP\IURLGenerator;
class JsController extends Controller class JsController extends Controller
{ {
protected ?User $user; /**
* @var ConfigProxy
*/
protected $config;
/**
* @var User
*/
protected $user;
/**
* @var ThemingDefaults
*/
protected $themingDefaults;
/**
* @var IFactory
*/
protected $l10nFactory;
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
protected ConfigProxy $config, ConfigProxy $config,
protected ThemingDefaults $themingDefaults, ThemingDefaults $themingDefaults,
protected IFactory $l10nFactory, IFactory $l10nFactory
protected IAvatarManager $avatarManager,
protected IUserSession $userSession,
protected INavigationManager $navigationManager,
protected IURLGenerator $urlGenerator,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->user = $this->userSession->getUser();
$this->themingDefaults = $themingDefaults;
$this->user = OC::$server[IUserSession::class]->getUser();
$this->config = $config;
$this->l10nFactory = $l10nFactory;
} }
#[NoCSRFRequired] /**
#[NoAdminRequired] * @NoAdminRequired
#[PublicPage] * @NoCSRFRequired
#[FrontpageRoute(verb: 'GET', url: '/js/config')] * @PublicPage
*/
public function script(): TemplateResponse
{
$response = new TemplateResponse(Application::APP_ID, 'js/script', $this->getConfig(), 'blank');
$response->addHeader('Content-Type', 'text/javascript');
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*/
public function config(): JSONResponse public function config(): JSONResponse
{ {
return new JSONResponse($this->getConfig()); return new JSONResponse($this->getConfig());
@ -99,25 +127,25 @@ class JsController extends Controller
$targetBlankApps = $userTargetBlankApps; $targetBlankApps = $userTargetBlankApps;
} }
$isAvatarSet = $this->avatarManager->getAvatar($this->user->getUID())->exists(); $isAvatarSet = OC::$server->getAvatarManager()->getAvatar($this->user->getUid())->exists();
if ($useAvatar && $isAvatarSet) { if ($useAvatar && $isAvatarSet) {
$avatar = $this->urlGenerator->linkToRoute('core.avatar.getAvatar', [ $avatar = OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUID(), 'userId' => $this->user->getUid(),
'size' => 128, 'size' => 128,
'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0), 'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0),
]); ]);
} }
if ($this->config->getAppValueBool('show-settings', '0')) { if ($this->config->getAppValueBool('show-settings', '0')) {
$settingsNav = $this->navigationManager->getAll('settings'); $settingsNav = OC::$server->getNavigationManager()->getAll('settings');
if (isset($settingsNav['settings'])) { if (isset($settingsNav['settings'])) {
$settings = [ $settings = [
'href' => $settingsNav['settings']['href'], 'href' => $settingsNav['settings']['href'],
'name' => $settingsNav['settings']['name'], 'name' => $settingsNav['settings']['name'],
'avatar' => $this->urlGenerator->linkToRoute('core.avatar.getAvatar', [ 'avatar' => OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUID(), 'userId' => $this->user->getUid(),
'size' => 32, 'size' => 32,
'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0), 'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0),
]), ]),
@ -126,15 +154,13 @@ class JsController extends Controller
} }
} }
$indexUrl = $this->urlGenerator->linkTo('', 'index.php'); $indexUrl = OC::$server->getURLGenerator()->linkTo('', 'index.php');
return [ return [
'opener-position' => $this->config->getAppValue('opener-position', 'before'), 'opener-position' => $this->config->getAppValue('opener-position', 'before'),
'opener-hover' => $this->config->getAppValueBool('opener-hover', '0'), 'opener-hover' => $this->config->getAppValueBool('opener-hover', '0'),
'external-sites-in-top-menu' => $this->config->getAppValueBool('external-sites-in-top-menu', '0'), 'external-sites-in-top-menu' => $this->config->getAppValueBool('external-sites-in-top-menu', '0'),
'force-light-icon' => $this->config->getAppValueBool('force-light-icon', '0'), 'force-light-icon' => $this->config->getAppValueBool('force-light-icon', '0'),
'display-logo' => $this->config->getAppValueBool('display-logo', '1'),
'use-avatar' => $this->config->getAppValueBool('use-avatar', '0'),
'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'), 'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'),
'loader-enabled' => $this->config->getAppValueBool('loader-enabled', '1'), 'loader-enabled' => $this->config->getAppValueBool('loader-enabled', '1'),
'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'), 'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'),

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -19,15 +18,13 @@
namespace OCA\SideMenu\Controller; namespace OCA\SideMenu\Controller;
use OC;
use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\URLGenerator; use OC\URLGenerator;
use OCA\SideMenu\Service\AppRepository; use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\CategoryRepository; use OCA\SideMenu\Service\CategoryRepository;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCP\AppFramework\Controller; 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\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserSession; use OCP\IUserSession;
@ -35,25 +32,59 @@ use OCP\L10N\IFactory;
class NavController extends Controller class NavController extends Controller
{ {
/**
* @var ConfigProxy
*/
protected $config;
/**
* @var AppRepository
*/
protected $appRepository;
/**
* @var IFactory
*/
protected $l10nFactory;
/**
* @var CategoryFetcher
*/
protected $categoryFetcher;
/**
* @var URLGenerator
*/
protected $router;
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
protected ConfigProxy $config, ConfigProxy $config,
protected AppRepository $appRepository, AppRepository $appRepository,
protected CategoryRepository $categoryRepository, CategoryRepository $categoryRepository,
protected URLGenerator $router, URLGenerator $router,
protected IFactory $l10nFactory, IFactory $l10nFactory
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->config = $config;
$this->appRepository = $appRepository;
$this->categoryRepository = $categoryRepository;
$this->l10nFactory = $l10nFactory;
$this->router = $router;
} }
#[NoCSRFRequired] /**
#[NoAdminRequired] * @NoAdminRequired
#[PublicPage] * @NoCSRFRequired
#[FrontpageRoute(verb: 'GET', url: '/nav/items')] * @PublicPage
public function items(): JSONResponse *
* @return JSONResponse
*/
public function items()
{ {
$user = \OC::$server[IUserSession::class]->getUser(); $user = OC::$server[IUserSession::class]->getUser();
$items = []; $items = [];
if (!$user) { if (!$user) {
@ -115,7 +146,6 @@ class NavController extends Controller
$appsCategories[$app['id']][] = $category; $appsCategories[$app['id']][] = $category;
$items[$category]['apps'][$app['id']] = [ $items[$category]['apps'][$app['id']] = [
'id' => $app['id'],
'name' => $app['name'], 'name' => $app['name'],
'href' => $app['href'], 'href' => $app['href'],
'icon' => $app['icon'], 'icon' => $app['icon'],
@ -159,11 +189,11 @@ class NavController extends Controller
usort($items, function ($a, $b) use ($categoriesLabels) { usort($items, function ($a, $b) use ($categoriesLabels) {
foreach ($categoriesLabels as $key => $value) { foreach ($categoriesLabels as $key => $value) {
if ('other' === $a['categoryId']) { if ($a['categoryId'] === 'other') {
return -1; return -1;
} }
if ('other' === $b['categoryId']) { if ($b['categoryId'] === 'other') {
return 1; return 1;
} }

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -22,30 +21,52 @@ namespace OCA\SideMenu\Controller;
use OCA\SideMenu\AppInfo\Application; use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig; use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserSession; use OCP\IUserSession;
class PersonalSettingController extends Controller class PersonalSettingController extends Controller
{ {
/**
* @var IConfig
*/
protected $config;
/**
* @var ConfigProxy
*/
protected $configProxy;
/**
* @var IUserSession
*/
protected $userSession;
public function __construct( public function __construct(
$appName, $appName,
IRequest $request, IRequest $request,
protected IConfig $config, IConfig $config,
protected ConfigProxy $configProxy, ConfigProxy $configProxy,
protected IUserSession $userSession, IUserSession $userSession
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->config = $config;
$this->configProxy = $configProxy;
$this->userSession = $userSession;
} }
#[NoCSRFRequired] /**
#[NoAdminRequired] * @NoAdminRequired
#[FrontpageRoute(verb: 'POST', url: '/user/valueSet')] * @NoCSRFRequired
public function valueSet($name, $value): array *
* @param mixed $name
* @param mixed $value
*
* @return Response
*/
public function valueSet($name, $value)
{ {
$doSave = false; $doSave = false;
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
@ -66,7 +87,22 @@ class PersonalSettingController extends Controller
} }
} }
if (in_array($name, ['target-blank-apps', 'top-menu-apps', 'top-side-menu-apps', 'apps-order'])) { if ('target-blank-apps' === $name) {
$doSave = true;
$data = json_decode($value, true);
if (!is_array($data)) {
$doSave = false;
} else {
foreach ($data as $v) {
if (!is_string($v)) {
$doSave = false;
}
}
}
}
if (in_array($name, ['top-menu-apps', 'top-side-menu-apps', 'apps-order'])) {
$doSave = true; $doSave = true;
$data = json_decode($value, true); $data = json_decode($value, true);
@ -96,63 +132,4 @@ class PersonalSettingController extends Controller
return []; return [];
} }
#[NoCSRFRequired]
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/user/config')]
public function configuration(): JSONResponse
{
$user = $this->userSession->getUser();
$keys = $this->config->getUserKeys($user->getUid(), Application::APP_ID);
$booleans = [
'enabled',
];
$arrays = [
'apps-order',
'target-blank-apps',
'top-menu-apps',
'top-side-menu-apps',
];
$integers = [
'target-blank-mode',
];
$defaults = [
'enabled' => '1',
'target-blank-mode' => '1',
'apps-order' => '[]',
'target-blank-apps' => '[]',
'top-menu-apps' => '[]',
'top-side-menu-apps' => '[]',
];
$config = [];
foreach ($keys as $key) {
if (!isset($defaults[$key])) {
continue;
}
if (in_array($key, $booleans)) {
$config[$key] = $this->configProxy->getUserValueBool($user, $key, $defaults[$key]);
} elseif (in_array($key, $arrays)) {
$config[$key] = $this->configProxy->getUserValueArray($user, $key, $defaults[$key]);
} elseif (in_array($key, $integers)) {
$config[$key] = $this->configProxy->getUserValueInt($user, $key, $defaults[$key]);
} else {
$config[$key] = $this->configProxy->getUserValue($user, $key, $defaults[$key]);
}
}
foreach ($defaults as $key => $default) {
if (!array_key_exists($key, $config)) {
$config[$key] = $default;
}
}
return new JSONResponse($config);
}
} }

View file

@ -3,12 +3,7 @@
namespace OCA\SideMenu\Service; namespace OCA\SideMenu\Service;
use OC\User\User; 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\INavigationManager;
use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
/** /**
@ -18,25 +13,51 @@ use OCP\L10N\IFactory;
*/ */
class AppRepository class AppRepository
{ {
/**
* @var \OC_App
*/
protected $ocApp;
/**
* @var IFactory
*/
protected $l10nFactory;
/**
* @var ConfigProxy
*/
protected $config;
/**
* @var CategoryRepository
*/
protected $categoryRepository;
/**
* @var INavigationManager
*/
protected $navigationManager;
public function __construct( public function __construct(
protected \OC_App $ocApp, \OC_App $ocApp,
protected INavigationManager $navigationManager, INavigationManager $navigationManager,
protected IFactory $l10nFactory, IFactory $l10nFactory,
protected ConfigProxy $config, ConfigProxy $config,
protected CategoryRepository $categoryRepository, CategoryRepository $categoryRepository
protected IEventDispatcher $dispatcher,
protected IUserSession $userSession,
) { ) {
$this->dispatcher->dispatchTyped(new BeforeTemplateRenderedEvent( $this->ocApp = $ocApp;
$this->userSession->isLoggedIn(), $this->l10nFactory = $l10nFactory;
new TemplateResponse(Application::APP_NAME, '') $this->config = $config;
)); $this->navigationManager = $navigationManager;
$this->categoryRepository = $categoryRepository;
} }
/** /**
* Retrieves visibles apps. * Retrieves visibles apps.
*
* @return array
*/ */
public function getVisibleApps(): array public function getVisibleApps()
{ {
$navigation = $this->navigationManager->getAll(); $navigation = $this->navigationManager->getAll();
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]'); $appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
@ -69,14 +90,6 @@ class AppRepository
'external_links', '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']) { } elseif ('files' === $app['id']) {
$visibleApps[$app['id']] = [ $visibleApps[$app['id']] = [
'id' => $app['id'], 'id' => $app['id'],
@ -97,7 +110,7 @@ class AppRepository
return $visibleApps; return $visibleApps;
} }
public function getAppName($app): string public function getAppName($app)
{ {
return $this->config->getAppValue( return $this->config->getAppValue(
'app.navigation.name', 'app.navigation.name',
@ -106,7 +119,7 @@ class AppRepository
); );
} }
public function getOrderedApps(?User $user = null): array public function getOrderedApps(?User $user = null)
{ {
$apps = $this->getVisibleApps(); $apps = $this->getVisibleApps();
$orders = $this->config->getAppValueArray('apps-order', '[]'); $orders = $this->config->getAppValueArray('apps-order', '[]');

View file

@ -15,19 +15,51 @@ use OCP\L10N\IFactory;
*/ */
class CategoryRepository 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( public function __construct(
protected CategoryFetcher $categoryFetcher, CategoryFetcher $categoryFetcher,
protected ConfigProxy $config, ConfigProxy $config,
protected IConfig $iConfig, IConfig $iConfig,
protected IFactory $l10nFactory, IFactory $l10nFactory,
protected IUserSession $userSession, IUserSession $userSession
) { ) {
$this->categoryFetcher = $categoryFetcher;
$this->l10nFactory = $l10nFactory;
$this->config = $config;
$this->iConfig = $iConfig;
$this->userSession = $userSession;
} }
/** /**
* Retrieves categories. * Retrieves categories.
*
* @return array
*/ */
public function getOrderedCategories(): array public function getOrderedCategories()
{ {
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
$type = $this->config->getAppValue('categories-order-type', 'default'); $type = $this->config->getAppValue('categories-order-type', 'default');

View file

@ -2,8 +2,6 @@
namespace OCA\SideMenu\Service; namespace OCA\SideMenu\Service;
use OCA\Theming\ThemingDefaults;
/** /**
* class Color. * class Color.
* *
@ -11,10 +9,6 @@ use OCA\Theming\ThemingDefaults;
*/ */
class Color class Color
{ {
public function __construct(protected ThemingDefaults $theming)
{
}
/** /**
* @thanks https://stackoverflow.com/posts/54393956/revision * @thanks https://stackoverflow.com/posts/54393956/revision
*/ */
@ -37,29 +31,4 @@ class Color
return '#'.implode($hexCode); return '#'.implode($hexCode);
} }
public function getPrimaryColor()
{
return $this->theming->getColorPrimary();
}
public function getLightenPrimaryColor()
{
return $this->adjustBrightness($this->getPrimaryColor(), 0.2);
}
public function getDarkenPrimaryColor()
{
return $this->adjustBrightness($this->getPrimaryColor(), -0.2);
}
public function getDarkenPrimaryColor2()
{
return $this->adjustBrightness($this->getPrimaryColor(), -0.3);
}
public function getTextColorPrimary()
{
return $this->theming->getTextColorPrimary();
}
} }

View file

@ -13,7 +13,12 @@ use OCP\IConfig;
*/ */
class ConfigProxy class ConfigProxy
{ {
public function __construct(protected IConfig $config) /**
* @var IConfig
*/
protected $config;
public function __construct(IConfig $config)
{ {
$this->config = $config; $this->config = $config;
} }

View file

@ -11,7 +11,12 @@ use OCP\IDBConnection;
*/ */
class LangRepository class LangRepository
{ {
public function __construct(protected IDBConnection $db) /**
* @var IDBConnection
*/
protected $db;
public function __construct(IDBConnection $db)
{ {
$this->db = $db; $this->db = $db;
} }
@ -30,12 +35,7 @@ class LangRepository
->from('preferences') ->from('preferences')
; ;
// Nextcloud >=33+ $stmt = $qb->execute();
if (method_exists($qb, 'executeQuery')) {
$stmt = $qb->executeQuery();
} else {
$stmt = $qb->execute();
}
$langs = ['en']; $langs = ['en'];

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -22,25 +21,75 @@ namespace OCA\SideMenu\Settings;
use OCA\SideMenu\AppInfo\Application; use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\AppRepository; use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\CategoryRepository; use OCA\SideMenu\Service\CategoryRepository;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCA\SideMenu\Service\LangRepository;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N; use OCP\IL10N;
use OCP\ILogger;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
use OCA\Theming\ThemingDefaults;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\LangRepository;
class Admin implements ISettings class Admin implements ISettings
{ {
/**
* @var IL10N
*/
private $l;
/**
* @var ILogger
*/
private $logger;
/**
* @var ConfigProxy
*/
private $config;
/**
* @var AppRepository
*/
private $appRepository;
/**
* @var CategoryRepository
*/
private $categoryRepository;
/**
* @var ThemingDefaults
*/
protected $theming;
/**
* @var Color
*/
protected $color;
/**
* @var LangRepository
*/
protected $langRepository;
public function __construct( public function __construct(
protected IL10N $l, IL10N $l,
protected ConfigProxy $config, ILogger $logger,
protected AppRepository $appRepository, ConfigProxy $config,
protected CategoryRepository $categoryRepository, AppRepository $appRepository,
protected ThemingDefaults $theming, CategoryRepository $categoryRepository,
protected Color $color, ThemingDefaults $theming,
protected LangRepository $langRepository, Color $color,
LangRepository $langRepository
) { ) {
$this->l = $l;
$this->logger = $logger;
$this->config = $config;
$this->appRepository = $appRepository;
$this->categoryRepository = $categoryRepository;
$this->theming = $theming;
$this->color = $color;
$this->langRepository = $langRepository;
} }
/** /**

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -26,27 +25,59 @@ use OCP\Settings\IIconSection;
class AdminSection implements IIconSection class AdminSection implements IIconSection
{ {
public function __construct( /**
protected IURLGenerator $url, * @var IL10N
protected IL10N $l, */
) { private $l;
/**
* @var IURLGenerator
*/
private $url;
public function __construct(IURLGenerator $url, IL10N $l)
{
$this->url = $url;
$this->l = $l;
} }
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'.
*
* @returns string
*/
public function getID() public function getID()
{ {
return Application::APP_ID; 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() public function getName()
{ {
return $this->l->t(Application::APP_NAME); 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() public function getPriority()
{ {
return 70; return 70;
} }
/**
* {@inheritdoc}
*/
public function getIcon() public function getIcon()
{ {
return $this->url->imagePath(Application::APP_ID, 'icon.svg'); return $this->url->imagePath(Application::APP_ID, 'icon.svg');

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -24,17 +23,49 @@ use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\ConfigProxy; use OCA\SideMenu\Service\ConfigProxy;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N; use OCP\IL10N;
use OCP\ILogger;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
class Personal implements ISettings class Personal implements ISettings
{ {
/**
* @var IL10N
*/
private $l;
/**
* @var ILogger
*/
private $logger;
/**
* @var ConfigProxy
*/
private $config;
/**
* @var IUserSession
*/
private $userSession;
/**
* @var AppRepository
*/
private $appRepository;
public function __construct( public function __construct(
protected IL10N $l, IL10N $l,
protected ConfigProxy $config, ILogger $logger,
protected IUserSession $userSession, ConfigProxy $config,
protected AppRepository $appRepository, IUserSession $userSession,
AppRepository $appRepository
) { ) {
$this->l = $l;
$this->logger = $logger;
$this->config = $config;
$this->userSession = $userSession;
$this->appRepository = $appRepository;
} }
/** /**

View file

@ -1,5 +1,4 @@
<?php <?php
/** /**
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -27,13 +26,34 @@ use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection class PersonalSection implements IIconSection
{ {
public function __construct( /**
protected IURLGenerator $url, * @var IL10N
protected IL10N $l, */
protected ConfigProxy $configProxy, private $l;
) {
/**
* @var IURLGenerator
*/
private $url;
/**
* @var ConfigProxy
*/
private $configProxy;
public function __construct(IURLGenerator $url, IL10N $l, ConfigProxy $configProxy)
{
$this->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() public function getID()
{ {
if ($this->configProxy->getAppValueBool('force', '0')) { if ($this->configProxy->getAppValueBool('force', '0')) {
@ -43,6 +63,12 @@ class PersonalSection implements IIconSection
return Application::APP_ID; 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() public function getName()
{ {
if ($this->configProxy->getAppValueBool('force', '0')) { if ($this->configProxy->getAppValueBool('force', '0')) {
@ -52,6 +78,13 @@ class PersonalSection implements IIconSection
return $this->l->t(Application::APP_NAME); 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() public function getPriority()
{ {
if ($this->configProxy->getAppValueBool('force', '0')) { if ($this->configProxy->getAppValueBool('force', '0')) {
@ -61,6 +94,9 @@ class PersonalSection implements IIconSection
return 70; return 70;
} }
/**
* {@inheritdoc}
*/
public function getIcon() public function getIcon()
{ {
return $this->url->imagePath(Application::APP_ID, 'icon.svg'); return $this->url->imagePath(Application::APP_ID, 'icon.svg');

8188
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,63 @@
{ {
"license": "agpl", "license": "agpl",
"private": true, "private": true,
"module": true, "scripts": {
"scripts": { "build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.js",
"build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.config.js", "dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.js",
"dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.config.js", "watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.config.js", "lint": "./node_modules/.bin/eslint --ext .js,.vue src",
"lint": "ESLINT_USE_FLAT_CONFIG=false ./node_modules/.bin/eslint --ext .js,.vue --ignore-path .gitignore --fix src", "lint:fix": "./node_modules/.bin/eslint --ext .js,.vue src --fix",
"format": "./node_modules/.bin/prettier src --write" "stylelint": "./node_modules/.bin/stylelint src",
}, "stylelint:fix": "./node_modules/.bin/stylelint src --fix"
"dependencies": { },
"@babel/core": ">=7.12.0 <8.0.0", "dependencies": {
"@formatjs/intl-segmenter": "^12.0.8", "axios": "^0.24.0",
"@nextcloud/router": "^3.0.1", "trim": "^1.0.1",
"@nextcloud/vue": "^9.0.0-alpha.8", "vue": "^2.6.11"
"node-polyfill-webpack-plugin": "^4.1.0", },
"pinia": "^3.0.1", "browserslist": [
"postcss": "^7.0.0 || ^8.0.1", "extends @nextcloud/browserslist-config"
"vue": "^3.5.13", ],
"vuedraggable": "^4.1.0" "engines": {
}, "node": ">=16.0.0"
"browserslist": [ },
"extends @nextcloud/browserslist-config" "devDependencies": {
], "@babel/core": "^7.9.0",
"engines": { "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"node": ">=16.0.0" "@babel/preset-env": "^7.9.0",
}, "@nextcloud/axios": "^2.3.0",
"devDependencies": { "@nextcloud/browserslist-config": "^2.3.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@nextcloud/eslint-config": "^8.1.2",
"@nextcloud/axios": "^2.5.1", "@nextcloud/initial-state": "^2.0.0",
"@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/l10n": "^2.1.0",
"@nextcloud/event-bus": "^3.3.1", "@nextcloud/vue": "^7.12.1",
"@nextcloud/initial-state": "^2.2.0", "babel-eslint": "^10.1.0",
"@nextcloud/l10n": "^3.2.0", "babel-loader": "^8.1.0",
"babel-loader": "^9.1.3", "css-loader": "^3.4.2",
"css-loader": "^7.1.2", "eslint": "^8.0.0",
"eslint": "^9.19.0", "eslint-config-standard": "^17.0.0",
"eslint-config-prettier": "^10.0.1", "eslint-import-resolver-webpack": "^0.12.1",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-import": "^2.20.0",
"file-loader": "^6.2.0", "eslint-plugin-nextcloud": "^0.3.0",
"mini-css-extract-plugin": "^2.9.1", "eslint-plugin-node": "^10.0.0",
"postcss-loader": "^8.1.1", "eslint-plugin-promise": "^6.0.0",
"prettier": "3.4.2", "eslint-plugin-standard": "^4.0.1",
"sass": "^1.78.0", "eslint-plugin-vue": "^9.0.0",
"sass-loader": "^16.0.1", "eslint-webpack-plugin": "^3.0.0",
"source-map-loader": "^5.0.0", "file-loader": "^6.0.0",
"style-loader": "^4.0.0", "sass": "^1.49.9",
"vue-loader": "^17.4.2", "sass-loader": "^13.0.2",
"vue-router": "^4.4.5", "stylelint": "^14.0.0",
"webpack": "^5.94.0", "stylelint-config-recommended-scss": "^7.0.0",
"webpack-cli": "^5.1.4", "stylelint-scss": "^4.0.0",
"webpack-notifier": "^1.15.0" "stylelint-webpack-plugin": "^3.3.0",
} "url-loader": "^4.0.0",
"vue-loader": "^15",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.7.13",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"webpack-merge": "^4.2.2",
"webpack-node-externals": "^1.7.2"
}
} }

View file

@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before After
Before After

View file

@ -0,0 +1,181 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div>
<ul class="side-menu-setting-list" :class="{hide: values.length === 0}">
<li v-for="item in values" class="side-menu-setting-list-item" v-on:click="showEditForm(item)">
<span v-text="item.en"></span>
</li>
</ul>
<NcActions>
<NcActionButton @click="showAddForm" icon="icon-add"></NcActionButton>
</NcActions>
<NcModal v-if="addForm" @close="hideAddForm">
<div class="modal__content">
<div v-for="lang in langs">
<span class="lang" v-text="lang"></span>
<input type="text" v-model="newValue[lang]" required style="width: calc(100% - 100px)">
</div>
<NcActions>
<NcActionButton @click="saveAdd" icon="icon-checkmark"></NcActionButton>
</NcActions>
</div>
</NcModal>
<NcModal v-if="editForm" @close="hideEditForm">
<div class="modal__content">
<div v-for="lang in langs">
<span class="lang" v-text="lang"></span>
<input type="text" v-model="editValue[lang]" required style="width: calc(100% - 100px)">
</div>
<div class="pull-right">
<NcActions>
<NcActionButton @click="removeEdit" icon="icon-delete"></NcActionButton>
</NcActions>
</div>
<NcActions>
<NcActionButton @click="saveEdit" icon="icon-checkmark"></NcActionButton>
</NcActions>
</div>
</NcModal>
</div>
</template>
<style>
.modal__content {
padding: 10px;
}
.modal__content .lang {
width: 60px;
display: inline-block;
padding: 4px;
box-sizing: border-box;
}
.pull-right {
float: right;
}
</style>
<style scoped>
.hide {
display: none;
}
</style>
<script>
import NcModal from '@nextcloud/vue/dist/Components/NcModal'
import NcActions from '@nextcloud/vue/dist/Components/NcActions'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
export default {
name: 'AdminCategoriesCustom',
components: {
NcModal,
NcActions,
NcActionButton,
},
data() {
return {
input: null,
values: [],
langs: [],
addForm: false,
editForm: false,
newValue: {},
editValue: {},
}
},
methods: {
init() {
this.values = JSON.parse(this.input.value)
this.langs = JSON.parse(this.input.getAttribute('data-langs'))
},
update() {
this.input.value = JSON.stringify(this.values)
},
showAddForm() {
this.newValue = {id: 'cat' + Math.random().toString().replace('0.', '')}
this.addForm = true
},
showEditForm(value) {
this.editValue = {id: value.id}
for (let i of this.langs) {
this.editValue[i] = typeof value[i] !== 'undefined' ? value[i] : ''
}
this.editForm = true
},
saveAdd() {
for (let i of this.langs) {
if (!this.newValue[i] || /^\s*$/.test(this.newValue[i])) {
return
}
}
this.values.push(this.newValue)
this.update()
this.hideAddForm()
this.newValue = {}
},
saveEdit() {
for (let i of this.langs) {
if (!this.editValue[i] || /^\s*$/.test(this.editValue[i])) {
return
}
}
for (let i in this.values) {
if (this.values[i].id === this.editValue.id) {
this.values[i] = this.editValue
}
}
this.update()
this.hideEditForm()
},
removeEdit() {
for (let i in this.values) {
if (this.values[i].id === this.editValue.id) {
this.values.splice(i, 1);
}
}
this.update()
this.hideEditForm()
},
hideAddForm() {
this.addForm = false
},
hideEditForm() {
this.editForm = false
},
},
mounted() {
this.input = document.querySelector('input[name="categories-custom"]')
this.init()
}
}
</script>

346
src/AppMenu.vue Normal file
View file

@ -0,0 +1,346 @@
<!--
- @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<nav class="app-menu show">
<ul
class="app-menu-main"
:class="{ 'app-menu-main__hidden-label': hiddenLabels === 1, 'app-menu-main__show-hovered': hiddenLabels === 2 }"
v-if="apps !== null"
>
<li v-for="app in mainAppList()"
:key="app.id"
:data-app-id="app.id"
class="app-menu-entry"
:class="{ 'app-menu-entry__active': app.active, 'app-menu-entry__hidden-label': hiddenLabels === 1, 'app-menu-main__show-hovered': hiddenLabels === 2 }"
:style="makeStyle(app)"
>
<a :href="app.href"
:class="{ 'has-unread': app.unread > 0 }"
:aria-label="appLabel(app)"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.active ? 'page' : false">
<img :src="app.icon" alt="">
<div class="app-menu-entry--label">
{{ app.name }}
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
</div>
</a>
</li>
</ul>
<NcActions class="app-menu-more" :aria-label="t('core', 'More apps')" v-if="apps !== null">
<NcActionLink v-for="app in popoverAppList()"
:key="app.id"
:aria-label="appLabel(app)"
:aria-current="app.active ? 'page' : false"
:href="app.href"
:style="makeStyle(app)"
class="app-menu-popover-entry">
<template #icon>
<div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
<img :src="app.icon" alt="">
</div>
</template>
{{ app.name }}
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
</NcActionLink>
</NcActions>
</nav>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
export default {
name: 'AppMenu',
components: {
NcActions, NcActionLink,
},
data() {
return {
apps: null,
appLimit: 0,
observer: null,
targetBlankApps: [],
hiddenLabels: true
}
},
mounted() {
const ncApps = loadState('core', 'apps', {})
this.apps = {}
let orders = {}
window.menuAppsOrder.forEach((app, order) => {
orders[app] = order + 1
})
Array.from(window.topMenuApps).forEach((id) => {
if (ncApps.hasOwnProperty(id)) {
this.apps[id] = ncApps[id]
this.apps[id].order = orders[id] || null
}
})
this.targetBlankApps = window.targetBlankApps
this.hiddenLabels = window.topMenuAppsMouseOverHiddenLabel
this.observer = new ResizeObserver(this.resize)
this.observer.observe(this.$el)
this.resize()
},
beforeDestroy() {
this.observer.disconnect()
},
methods: {
appLabel(app) {
return app.name
+ (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
+ (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
},
appList() {
let items = Object.values(this.apps)
items.sort((a, b) => {
return a.order < b.order ? -1 : 1;
})
return items
},
mainAppList() {
return this.appList().slice(0, this.appLimit)
},
popoverAppList() {
return this.appList().slice(this.appLimit)
},
setNavigationCounter(id, counter) {
this.$set(this.apps[id], 'unread', counter)
},
resize() {
const availableWidth = this.$el.offsetWidth
let appCount = Math.floor(availableWidth / 50) - 1
const popoverAppCount = this.appList.length - appCount
if (popoverAppCount === 1) {
appCount--
}
if (appCount < 1) {
appCount = 0
}
this.appLimit = appCount
},
makeStyle(app) {
if (app.order !== null) {
return `order: ${app.order}`
}
}
},
}
</script>
<style lang="scss" scoped>
$header-icon-size: 20px;
.app-menu {
width: 100%;
display: flex;
flex-shrink: 1;
flex-wrap: wrap;
}
.app-menu-main {
display: flex;
flex-wrap: nowrap;
.app-menu-entry {
width: 50px;
height: 50px;
position: relative;
display: flex;
opacity: .7;
&.app-menu-entry__active {
opacity: 1;
&::before {
content: " ";
position: absolute;
pointer-events: none;
border-bottom-color: var(--color-main-background);
transform: translateX(-50%);
width: 12px;
height: 5px;
border-radius: 3px;
background-color: var(--color-primary-text);
left: 50%;
bottom: 6px;
display: block;
transition: all 0.1s ease-in-out;
opacity: 1;
}
.app-menu-entry--label {
font-weight: bold;
}
}
a {
width: calc(100% - 4px);
height: calc(100% - 4px);
margin: 2px;
color: var(--color-primary-text);
position: relative;
}
img {
transition: margin 0.1s ease-in-out;
width: $header-icon-size;
height: $header-icon-size;
padding: calc((100% - $header-icon-size) / 2);
filter: var(--primary-invert-if-bright);
}
.app-menu-entry--label {
opacity: 0;
position: absolute;
font-size: 12px;
color: var(--color-primary-text);
text-align: center;
bottom: -5px;
left: 50%;
display: block;
min-width: 100%;
transform: translateX(-50%);
transition: all 0.1s ease-in-out;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
&:not(.app-menu-entry__hidden-label):not(.app-menu-entry__show-hovered):hover,
&:not(.app-menu-entry__hidden-label):not(.app-menu-entry__show-hovered):focus-within {
opacity: 1;
.app-menu-entry--label {
opacity: 1;
font-weight: bold;
font-size: 14px;
bottom: 0;
width: auto;
overflow: visible;
}
}
}
// Show labels
&:hover,
&:focus-within,
.app-menu-entry:hover,
.app-menu-entry:focus {
opacity: 1;
}
&:not(.app-menu-main__hidden-label):not(.app-menu-main__show-hovered):hover,
&:not(.app-menu-main__hidden-label):not(.app-menu-main__show-hovered):focus-within,
.app-menu-entry:not(.app-menu-entry__hidden-label):hover,
.app-menu-entry:not(.app-menu-entry__hidden-label):focus {
img {
margin-top: -6px;
}
.app-menu-entry--label {
opacity: 1;
bottom: 0;
}
&::before, .app-menu-entry::before {
opacity: 0;
}
}
&.app-menu-main__show-hovered .app-menu-entry:hover,
&.app-menu-main__show-hovered .app-menu-entry:focus {
img {
margin-top: -6px;
}
.app-menu-entry--label {
opacity: 1;
bottom: 0;
}
&::before, .app-menu-entry::before {
opacity: 0;
}
}
}
::v-deep .app-menu-more .button-vue--vue-tertiary {
color: var(--color-primary-text);
opacity: .7;
margin: 3px;
&:hover {
opacity: 1;
background-color: transparent !important;
}
&:focus-visible {
opacity: 1;
background-color: transparent !important;
border-radius: var(--border-radius);
outline: none;
box-shadow: 0 0 0 2px var(--color-primary-text);
}
}
.app-menu-popover-entry {
.app-icon {
position: relative;
height: 44px;
&.has-unread::after {
background-color: var(--color-main-text);
}
img {
filter: var(--background-invert-if-bright);
width: $header-icon-size;
height: $header-icon-size;
padding: calc((50px - $header-icon-size) / 2);
}
}
}
.has-unread::after {
content: "";
width: 8px;
height: 8px;
background-color: var(--color-primary-text);
border-radius: 50%;
position: absolute;
display: block;
top: 10px;
right: 10px;
}
.unread-counter {
display: none;
}
</style>

View file

@ -15,20 +15,18 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div <div class="side-menu-search">
class="side-menu-setting-form" <input type="text" :value="value" :placeholder="t('side_menu', 'Search')" @input="$emit('input', $event.target.value)">
:class="{ 'side-menu-setting-form-long': long }"
>
<slot></slot>
</div> </div>
</template> </template>
<script setup> <script>
const { long } = defineProps({ export default {
long: { name: 'AppSearch',
type: Boolean, props: {
required: false, value: {
default: false, required: true
},
}, },
}) }
</script> </script>

View file

@ -15,15 +15,11 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<button <button class="side-menu-opener side-menu-closer"></button>
class="cm-opener cm-closer"
:arial-label="t('side_menu', 'Close the menu')"
@click="$emit('click')"
>
<span>{{ t('side_menu', 'Close the menu') }}</span>
</button>
</template> </template>
<script setup> <script>
defineEmits(['click']) export default {
name: 'CloserButton',
}
</script> </script>

View file

@ -15,35 +15,28 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div class="cm-loader"> <div class="side-menu-loader">
<div <svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
class="cm-loader-bar" <g fill="none" fill-rule="evenodd">
:style="createStyle(width)" <g transform="translate(1 1)" stroke-width="2">
></div> <circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>
</div> </div>
</template> </template>
<script setup> <script>
import { onMounted, ref } from 'vue' export default {
name: 'Loader',
const width = ref(0)
const createStyle = (size) => {
return {
width: `${size}%`,
}
} }
let interval = null
onMounted(() => {
window.addEventListener('beforeunload', () => {
interval = setInterval(() => {
width.value = Math.min(width.value + 0.2, 100)
if (width.value === 100) {
clearInterval(interval)
window.setTimeout(() => (width.value = 0), 2000)
}
}, 25)
})
})
</script> </script>

View file

@ -15,26 +15,30 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div <div v-bind:class="classes">
class="cm-settings-item" <a v-if="link !== null" v-bind:href="link">
:class="{ 'cm-settings-item--disabled': disabled }" <img v-bind:src="image" alt="Logo">
> </a>
<slot></slot> <img v-else v-bind:src="image" alt="Logo">
</div> </div>
</template> </template>
<script setup> <script>
const { disabled } = defineProps({ export default {
disabled: { name: 'Logo',
type: Boolean, props: {
required: false, image: {
default: false, type: String,
required: true
},
link: {
type: String,
required: false
},
classes: {
type: Object,
required: true
},
}, },
})
</script>
<style scoped>
.disabled {
display: block;
} }
</style> </script>

View file

@ -15,15 +15,11 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<button <button class="side-menu-opener"></button>
class="cm-opener"
:arial-label="label"
@click="$emit('click')"
>
<span>{{ t('side_menu', 'Toggle the menu') }}</span>
</button>
</template> </template>
<script setup> <script>
defineEmits(['click']) export default {
name: 'OpenerButton',
}
</script> </script>

20
src/PageLoader.js Normal file
View file

@ -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

View file

@ -15,35 +15,35 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div class="cm-setting"> <div class="side-menu-settings">
<a :href="href"> <a v-bind:href="href">
<!-- <!--
{{ label }} {{ label }}
--> -->
<span class="avatardiv avatardiv-shown"> <span class="avatardiv avatardiv-shown">
<img <img v-bind:src="avatar" :alt="label">
:src="avatar"
:alt="label"
/>
</span> </span>
</a> </a>
</div> </div>
</template> </template>
<script setup> <script>
const { label, href, avatar } = defineProps({ export default {
label: { name: 'SettingsButton',
type: String, props: {
required: true, label: {
type: String,
required: true
},
href: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
}, },
href: { }
type: String,
required: true,
},
avatar: {
type: String,
required: true,
},
})
</script> </script>

71
src/SideMenu.js Normal file
View file

@ -0,0 +1,71 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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'
import SMcreateElement from './lib/createElement'
Vue.prototype.OC = OC
Vue.prototype.t = OC.L10N.translate
window.SMcreateElement = SMcreateElement
window.PageLoader = PageLoader
const mountSideMenuComponent = () => {
const container = document.querySelector('#side-menu')
if (!container) {
return window.setTimeout(mountSideMenuComponent, 50)
}
const component = (() => {
if (container.getAttribute('data-bigmenu')) {
return SideMenuBig
} else if(container.getAttribute('data-sidewithcategories')) {
return SideMenuWithCategories
} else {
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()

171
src/SideMenu.vue Normal file
View file

@ -0,0 +1,171 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div id="side-menu">
<div class="side-menu-header" v-if="settings || !openerHover || (!avatar && !alwaysDisplayed && logo) || avatar">
<SettingsButton
v-if="settings"
v-bind:href="settings.href"
v-bind:label="settings.name"
v-bind:avatar="settings.avatar" />
<AppSearch v-model:search="search" />
<OpenerButton />
<Logo
v-if="!avatar && !alwaysDisplayed && logo" v-bind:classes="{'side-menu-logo': true, 'avatardiv': false}"
v-bind:image="logo"
v-bind:link="logoLink"
/>
<Logo
v-if="avatar" v-bind:classes="{'side-menu-logo': true, 'avatardiv': true}"
v-bind:image="avatar"
v-bind:link="logoLink"
/>
</div>
<ul class="side-menu-apps-list" :class="{'side-menu-apps-list--with-settings': !!settings}">
<SideMenuApp
v-for="(app, key) in apps"
v-if="!hiddenApps.includes(app.id) && searchMatch(app.name)"
v-bind:classes="{'side-menu-app': true, 'active': app.active}"
v-bind:key="key"
v-bind:icon="app.icon"
v-bind:label="app.name"
v-bind:href="app.href"
v-bind:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
/>
</ul>
</div>
</template>
<script>
import axios from 'axios'
import OpenerButton from './OpenerButton'
import SettingsButton from './SettingsButton'
import SideMenuApp from './SideMenuApp'
import AppSearch from './AppSearch'
import Logo from './Logo'
import { loadState } from '@nextcloud/initial-state'
export default {
name: 'SideMenu',
components: {
SettingsButton,
OpenerButton,
SideMenuApp,
Logo,
AppSearch,
},
data() {
return {
apps: [],
logo: null,
logoLink: null,
avatar: null,
forceLightIcon: false,
targetBlankApps: [],
hiddenApps: [],
settings: null,
openerHover: false,
alwaysDisplayed: false,
search: '',
}
},
methods: {
retrieveApps() {
const ncApps = loadState('core', 'apps', {})
let orders = {}
let finalApps = []
window.menuAppsOrder.forEach((app, order) => {
orders[app] = order + 1
})
for (let id in ncApps) {
if (window.topMenuApps.includes(id) && !window.topSideMenuApps.includes(id)) {
continue
}
if (this.hiddenApps.includes(id)) {
continue
}
let app = ncApps[id]
app.order = orders[id] || null
finalApps.push(app)
}
finalApps.sort((a, b) => {
if (a.order === null || b.order === null) {
return a.name < b.name ? -1 : 1
}
return a.order < b.order ? -1 : 1
})
this.apps = finalApps
document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', {
detail: {apps: this.apps},
}))
},
retrieveConfig() {
},
hasSearchMatch(apps) {
if (this.search.trim() === '') {
return true
}
for (let key in apps) {
if (this.searchMatch(apps[key].name)) {
return true
}
}
return false
},
searchMatch(name) {
if (this.search.trim() === '') {
return true
}
return name.toLowerCase().includes(this.search.toLowerCase())
},
},
mounted() {
axios
.get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => {
const config = response.data
this.targetBlankApps = config['target-blank-apps']
this.forceLightIcon = config['force-light-icon']
this.avatar = config['avatar']
this.logo = config['logo']
this.logoLink = config['logo-link']
this.settings = config['settings']
this.openerHover = config['opener-hover']
this.alwaysDisplayed = config['always-displayed']
this.hiddenApps = config['big-menu-hidden-apps']
this.retrieveApps()
})
}
}
</script>

View file

@ -15,44 +15,38 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<li :class="classes"> <li v-bind:class="classes">
<a <a v-bind:href="href" :target="target" v-bind:title="label">
:href="href" <img class="side-menu-app-icon" v-bind:src="icon" v-bind:alt="label" />
:target="target" <span class="side-menu-app-text" v-text="label"></span>
:title="label"
>
<img
class="cm-app-icon"
:src="icon"
:alt="label"
/>
<span class="cm-app-text">{{ label }}</span>
</a> </a>
</li> </li>
</template> </template>
<script setup> <script>
const { label, icon, href, classes, target } = defineProps({ export default {
label: { name: 'SideMenuApp',
type: String, props: {
required: true, label: {
type: String,
required: true
},
icon: {
type: String,
required: true
},
href: {
type: String,
required: true
},
classes: {
type: Object,
required: true
},
target: {
type: String,
required: false
},
}, },
icon: { }
type: String,
required: true,
},
href: {
type: String,
required: true,
},
classes: {
type: Object,
required: true,
},
target: {
type: String,
required: false,
default: null,
},
})
</script> </script>

155
src/SideMenuBig.vue Normal file
View file

@ -0,0 +1,155 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div id="side-menu" class="side-menu-big">
<div class="side-menu-header">
<CloserButton />
<SettingsButton
v-if="settings"
v-bind:href="settings.href"
v-bind:label="settings.name"
v-bind:avatar="settings.avatar"
/>
<AppSearch v-model:search="search" />
<OpenerButton />
</div>
<div class="side-menu-categories-wrapper">
<div class="side-menu-categories">
<Loader v-if="!items.length" />
<div class="side-menu-category" v-for="(category, key) in items" v-if="hasSearchMatch(category.apps)" v-bind:key="key">
<h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2>
<ul class="side-menu-apps-list">
<SideMenuBigApp
v-for="(app, appId) in category.apps"
v-if="searchMatch(app.name)"
v-bind:key="appId"
v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}"
v-bind:icon="app.icon"
v-bind:label="app.name"
v-bind:href="app.href"
v-bind:target="targetBlankApps.indexOf(appId) !== -1 ? '_blank' : undefined"
/>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import OpenerButton from './OpenerButton'
import CloserButton from './CloserButton'
import SettingsButton from './SettingsButton'
import Loader from './Loader'
import AppSearch from './AppSearch'
import SideMenuBigApp from './SideMenuBigApp'
import { loadState } from '@nextcloud/initial-state'
export default {
name: 'SideMenuBig',
components: {
SettingsButton,
OpenerButton,
CloserButton,
Loader,
SideMenuBigApp,
AppSearch,
},
data() {
return {
items: [],
activeApp: null,
targetBlank: false,
targetBlankApps: [],
settings: null,
search: '',
}
},
methods: {
retrieveApps() {
axios
.get(OC.generateUrl('/apps/side_menu/nav/items'))
.then((response) => {
this.items = response.data.items
let apps = []
for (let category of this.items) {
for (let a in category.apps) {
apps.push(category.apps[a])
}
}
document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', {
detail: {apps: apps},
}))
})
},
retrieveActiveApp() {
const ncApps = loadState('core', 'apps', {})
for (let id in ncApps) {
if (ncApps[id].active) {
this.activeApp = id
}
}
},
retrieveConfig() {
axios
.get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => {
const config = response.data
this.targetBlankApps = config['target-blank-apps']
this.settings = config['settings']
})
},
hasSearchMatch(apps) {
if (this.search.trim() === '') {
return true
}
for (let key in apps) {
if (this.searchMatch(apps[key].name)) {
return true
}
}
return false
},
searchMatch(name) {
if (this.search.trim() === '') {
return true
}
return name.toLowerCase().includes(this.search.toLowerCase())
},
},
mounted() {
this.retrieveConfig()
this.retrieveApps()
this.retrieveActiveApp()
}
}
</script>

View file

@ -15,44 +15,38 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<template> <template>
<li :class="classes"> <li v-bind:class="classes">
<a <a v-bind:href="href" :target="target" v-bind:title="label">
:href="href" <img class="side-menu-app-icon" v-bind:src="icon" v-bind:alt="label" />
:target="target" <span class="side-menu-app-text" v-text="label"></span>
:title="label"
>
<img
class="cm-app-icon"
:src="icon"
:alt="label"
/>
<span class="cm-app-text">{{ label }}</span>
</a> </a>
</li> </li>
</template> </template>
<script setup> <script>
const { label, icon, href, classes, target } = defineProps({ export default {
label: { name: 'SideMenuBigApp',
type: String, props: {
required: true, label: {
type: String,
required: true
},
icon: {
type: String,
required: true
},
href: {
type: String,
required: true
},
classes: {
type: Object,
required: true
},
target: {
type: String,
required: false
},
}, },
icon: { }
type: String,
required: true,
},
href: {
type: String,
required: true,
},
classes: {
type: Object,
required: true,
},
target: {
type: String,
required: false,
default: null,
},
})
</script> </script>

View file

@ -0,0 +1,152 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div id="side-menu" class="side-menu-with-categories">
<div class="side-menu-header">
<SettingsButton
v-if="settings"
v-bind:href="settings.href"
v-bind:label="settings.name"
v-bind:avatar="settings.avatar"
/>
<AppSearch v-model:search="search" />
<OpenerButton />
</div>
<div class="side-menu-categories-wrapper">
<div class="side-menu-categories">
<Loader v-if="!items.length" />
<div class="side-menu-category" v-for="(category, key) in items" v-if="hasSearchMatch(category.apps)" v-bind:key="key">
<h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2>
<ul class="side-menu-apps-list">
<SideMenuBigApp
v-for="(app, appId) in category.apps"
v-if="searchMatch(app.name)"
v-bind:key="appId"
v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}"
v-bind:icon="app.icon"
v-bind:label="app.name"
v-bind:href="app.href"
v-bind:target="targetBlankApps.indexOf(appId) !== -1 ? '_blank' : undefined"
/>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import OpenerButton from './OpenerButton'
import SettingsButton from './SettingsButton'
import Loader from './Loader'
import AppSearch from './AppSearch'
import SideMenuBigApp from './SideMenuBigApp'
import { loadState } from '@nextcloud/initial-state'
export default {
name: 'SideMenuWithCategories',
components: {
SettingsButton,
OpenerButton,
Loader,
SideMenuBigApp,
AppSearch,
},
data() {
return {
items: [],
activeApp: null,
targetBlank: false,
targetBlankApps: [],
settings: null,
search: '',
}
},
methods: {
retrieveApps() {
axios
.get(OC.generateUrl('/apps/side_menu/nav/items'))
.then((response) => {
this.items = response.data.items
let apps = []
for (let category of this.items) {
for (let a in category.apps) {
apps.push(category.apps[a])
}
}
document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', {
detail: {apps: apps},
}))
})
},
retrieveActiveApp() {
const ncApps = loadState('core', 'apps', {})
for (let id in ncApps) {
if (ncApps[id].active) {
this.activeApp = id
}
}
},
retrieveConfig() {
axios
.get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => {
const config = response.data
this.targetBlankApps = config['target-blank-apps']
this.settings = config['settings']
})
},
hasSearchMatch(apps) {
if (this.search.trim() === '') {
return true
}
for (let key in apps) {
if (this.searchMatch(apps[key].name)) {
return true
}
}
return false
},
searchMatch(name) {
if (this.search.trim() === '') {
return true
}
return name.toLowerCase().includes(this.search.toLowerCase())
},
},
mounted() {
this.retrieveConfig()
this.retrieveApps()
this.retrieveActiveApp()
}
}
</script>

View file

@ -8,27 +8,275 @@
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import './scss/admin.scss' import AdminCategoriesCustom from './AdminCategoriesCustom.vue'
import Vue from 'vue'
import '@formatjs/intl-segmenter/polyfill.js' Vue.prototype.OC = window.OC
Vue.prototype.OCA = window.OCA
import { createApp } from 'vue' let elements = []
import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
import AdminSettings from './pages/AdminSettings' const selector = '#side-menu-message'
waitContainer('#side-menu-admin-settings').then((selector) => { const userConfig = (name, value, callbacks) => {
const pinia = createPinia() const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet')
const app = createApp(AdminSettings) const formData = []
app.use(pinia)
app.mixin({ methods: { t, n } }) formData.push('name=' + encodeURIComponent(name))
app.mount(selector) formData.push('value=' + encodeURIComponent(value))
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.join('&')
})
.then(callbacks.success)
.catch(callbacks.error)
}
const appConfig = (name, value, callbacks) => {
OCP.AppConfig.setValue('side_menu', name, value, callbacks)
}
const saveSettings = (key) => {
const element = elements[key]
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)
}
value = JSON.stringify(value)
} else {
name = element.getAttribute('name')
value = element.value
}
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)
}
}
const elementToggler = (element) => {
let display = 'none'
if (window.getComputedStyle(element).display === 'none') {
display = 'block'
}
element.style.display = display
}
const updateAppsCategoriesCustom = () => {
let values = {}
for (let item of document.querySelectorAll('.apps-categories-custom')) {
let app = item.getAttribute('data-app')
let value = item.value
if (value) {
values[app] = value
}
}
document.querySelector('#apps-categories-custom').value = JSON.stringify(values)
}
document.addEventListener('DOMContentLoaded', () => {
$('*[data-toggle="tooltip"]').tooltip();
if (document.querySelector('#side-menu-categories-custom')) {
const View = Vue.extend(AdminCategoriesCustom)
const adminCategoriesCustom = new View({})
adminCategoriesCustom.$mount('#side-menu-categories-custom')
}
elements = document.querySelectorAll('.side-menu-setting')
document.querySelector('#side-menu-save').addEventListener('click', (event) => {
event.preventDefault()
OC.msg.startSaving(selector)
saveSettings(0)
})
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 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'
})
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) {
}
}) })

View file

@ -1,29 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-search">
<input
v-model="model"
type="text"
:placeholder="t('side_menu', 'Search')"
/>
</div>
</template>
<script setup>
const model = defineModel({ type: String })
</script>

View file

@ -1,52 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div :class="classes">
<a
v-if="link !== null"
:href="link"
>
<img
:src="image"
alt="Logo"
/>
</a>
<img
v-else
:src="image"
alt="Logo"
/>
</div>
</template>
<script setup>
const { image, link, classes } = defineProps({
image: {
type: String,
required: true,
},
link: {
type: String,
required: false,
default: null,
},
classes: {
type: Object,
required: true,
},
})
</script>

View file

@ -1,103 +0,0 @@
<template>
<div class="cm-settings-btn cm-settings-btn--save">
<NcButton
variant="success"
@click="save"
>
<template v-if="!loading">
{{ t('side_menu', 'Save') }}
</template>
<NcLoadingIcon v-else />
</NcButton>
<div
v-if="error"
id="error"
>
{{ error }}
</div>
</div>
</template>
<script setup>
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
import { ref } from 'vue'
import { waitPasswordConfirmation } from '../../lib/setting.js'
const loading = ref(false)
const error = ref(null)
const { config } = defineProps({
config: {
type: Object,
required: true,
},
})
const filterConfig = (value) => {
const result = {}
for (let key in value) {
if (['cache-categories', 'cache', 'langs', 'enabled'].includes(key) === false) {
result[key] = value[key]
}
}
return result
}
const save = async () => {
const data = filterConfig(config)
const size = Object.keys(data).length
let counter = 0
loading.value = true
error.value = null
const update = () => {
++counter
if (counter === size) {
loading.value = false
if (!error.value) {
location.reload()
}
}
}
waitPasswordConfirmation()
.then(() => {
for (let key in data) {
let value = data[key]
if (Array.isArray(value) || typeof value === 'object') {
value = JSON.stringify(value)
} else if (typeof value === 'boolean') {
value = value ? '1' : '0'
}
OCP.AppConfig.setValue('side_menu', key, value.toString(), {
success() {
update()
},
error() {
error.value = `Error while saving ${key}`
update()
},
})
}
})
.catch(() => {
counter = 0
loading.value = false
error.value = null
})
}
</script>
<style scoped>
#error {
padding-top: 10px;
color: red;
}
</style>

View file

@ -1,18 +0,0 @@
<template>
<a
:href="href"
rel="noopener"
target="_blank"
>
<slot></slot>
</a>
</template>
<script setup>
defineProps({
href: {
type: String,
required: true,
},
})
</script>

View file

@ -1,35 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<h2>{{ t('side_menu', label) }}</h2>
</template>
<script setup>
const { label } = defineProps({
label: {
type: String,
required: true,
},
})
</script>
<style scoped>
h2 {
font-size: 1.3em;
margin: 0 0 12px 0;
}
</style>

View file

@ -1,71 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div
class="cm-settings-item-label"
:class="{
'cm-settings-item-label--short': short,
'cm-settings-item-label--top': top,
'cm-settings-item-label--middle': middle,
}"
>
{{ t('side_menu', label) }}
<template v-if="help">
<br />
<em>{{ t('side_menu', help) }}</em>
</template>
<template v-if="help2">
<br />
<em>{{ t('side_menu', help2) }}</em>
</template>
</div>
</template>
<script setup>
const { short, label } = defineProps({
short: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: true,
},
middle: {
type: Boolean,
required: false,
default: false,
},
top: {
type: Boolean,
required: false,
default: true,
},
help: {
type: [String, null],
required: false,
default: null,
},
help2: {
type: [String, null],
required: false,
default: null,
},
})
</script>

View file

@ -1,34 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div
class="cm-settings-section"
:class="{ 'cm-settings-section--hidden': hidden }"
>
<slot></slot>
</div>
</template>
<script setup>
defineProps({
hidden: {
type: Boolean,
required: false,
default: false,
},
})
</script>

View file

@ -1,99 +0,0 @@
<template>
<div class="cm-settings-btn cm-settings-btn--save">
<NcButton
variant="success"
@click="save"
>
<template v-if="!loading">
{{ t('side_menu', 'Save') }}
</template>
<NcLoadingIcon v-else />
</NcButton>
<div
v-if="error"
id="error"
></div>
</div>
</template>
<script setup>
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
import { ref } from 'vue'
const loading = ref(false)
const error = ref(null)
const { config } = defineProps({
config: {
type: Object,
required: true,
},
})
const filterConfig = (value) => {
const result = {}
for (let key in value) {
result[key] = value[key]
}
return result
}
const save = async () => {
const data = filterConfig(config)
const size = Object.keys(data).length
const url = OC.generateUrl('/apps/side_menu/user/valueSet')
let counter = 0
loading.value = true
error.value = null
const update = () => {
++counter
if (counter === size) {
loading.value = false
if (!error.value) {
location.reload()
}
}
}
for (let key in data) {
let value = data[key]
let formData = []
if (Array.isArray(value) || typeof value === 'object') {
value = JSON.stringify(value)
} else if (typeof value === 'boolean') {
value = value ? '1' : '0'
}
formData.push('name=' + encodeURIComponent(key))
formData.push('value=' + encodeURIComponent(value.toString()))
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.join('&'),
})
.then(update)
.catch(() => {
error.value = `Error while saving ${key}`
update()
})
}
}
</script>
<style scoped>
#error {
padding-top: 10px;
color: red;
}
</style>

View file

@ -1,292 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-appcategory">
<NcButton
aria-label="t('side_menu', 'Customize')"
variant="primary"
@click="openModal"
>
{{ t('side_menu', 'Customize') }}
</NcButton>
<NcModal
v-if="modal"
class="cm-settings-form-appcategory-modal"
@close="closeModal"
>
<div class="modal__content">
<div class="menu">
<NcButton
aria-label="t('side_menu', 'Categories')"
:variant="section === 'cats' ? 'primary' : 'secondary'"
@click="setSection('cats')"
>
{{ t('side_menu', 'Categories') }}
</NcButton>
<NcButton
aria-label="t('side_menu', 'Applications')"
:variant="section === 'apps' ? 'primary' : 'secondary'"
@click="setSection('apps')"
>
{{ t('side_menu', 'Applications') }}
</NcButton>
</div>
<div v-if="section === 'cats'">
<table
v-if="!newCustomCategory && editCustomCategoryKey === null"
width="100%"
>
<tbody>
<tr
v-for="(item, key) in categoriesCustom"
:key="key"
>
<td>{{ item[langs[0]] }}</td>
<td width="50px">
<NcActions>
<NcActionButton
icon="icon-edit"
@click="editCustomCategory(key)"
></NcActionButton>
<NcActionButton
icon="icon-delete"
@click="removeCustomCategory(key)"
></NcActionButton>
</NcActions>
</td>
</tr>
</tbody>
</table>
<div
v-else
class="form"
>
<template v-if="newCustomCategory">
<NcTextField
v-for="lang in langs"
:key="lang"
v-model="newCustomCategory[lang]"
:label="lang"
/>
</template>
<template v-if="editCustomCategoryKey !== null">
<NcTextField
v-for="lang in langs"
:key="lang"
v-model="categoriesCustom[editCustomCategoryKey][lang]"
:label="lang"
/>
</template>
</div>
</div>
<div v-if="section === 'apps'">
<table width="100%">
<tbody>
<tr
v-for="item in apps"
:key="item.id"
>
<td>
<img
:src="item.icon"
:alt="item.name"
/>
{{ item.name }}
</td>
<td width="50%">
<FormSelect
v-model="appsCategoriesCustom[item.id]"
:options="getOptions(categoriesCustom)"
:required="false"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal__footer">
<template v-if="section === 'cats'">
<template v-if="newCustomCategory">
<NcActions>
<NcActionButton
icon="icon-close"
@click="cancelCustomCategory"
></NcActionButton>
</NcActions>
<NcActions>
<NcActionButton
icon="icon-checkmark"
@click="saveCustomCategory"
></NcActionButton>
</NcActions>
</template>
<template v-if="editCustomCategoryKey !== null">
<NcActions>
<NcActionButton
icon="icon-close"
@click="cancelCustomCategory"
></NcActionButton>
</NcActions>
</template>
<template v-else>
<NcActions>
<NcActionButton
v-if="!newCustomCategory && editCustomCategoryKey === null"
icon="icon-add"
@click="addCustomCategory"
></NcActionButton>
<NcActionButton
v-if="editCustomCategoryKey !== null"
icon="icon-checkmark"
@click="saveCustomCategory"
></NcActionButton>
</NcActions>
</template>
</template>
<NcButton
variant="primary"
class="btn-close"
@click="closeModal"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</div>
</template>
<script setup>
import { NcButton, NcModal, NcActions, NcActionButton, NcTextField } from '@nextcloud/vue'
import { useNavStore } from '../../../store/nav.js'
import { ref, onMounted, watch } from 'vue'
import FormSelect from './FormSelect'
const emit = defineEmits(['update:categoriesCustom', 'update:appsCategoriesCustom'])
const { categoriesCustom, appsCategoriesCustom, langs } = defineProps({
categoriesCustom: {
type: Array,
required: true,
},
appsCategoriesCustom: {
type: [Object, Array],
required: true,
},
langs: {
type: Array,
required: true,
},
})
const navStore = useNavStore()
const modal = ref(false)
const apps = ref([])
const categories = ref([])
const section = ref('apps')
const newCustomCategory = ref(null)
const editCustomCategoryKey = ref(null)
const openModal = () => {
modal.value = true
}
const closeModal = () => {
modal.value = false
}
const setSection = (value) => {
section.value = value
}
const addCustomCategory = () => {
let data = {
id: 'cat' + Math.random().toString().replace('0.', ''),
}
langs.forEach((lang) => {
data[lang] = ''
})
newCustomCategory.value = data
}
const cancelCustomCategory = () => {
newCustomCategory.value = null
editCustomCategoryKey.value = null
}
const saveCustomCategory = () => {
const data = categoriesCustom
if (editCustomCategoryKey.value === null) {
data.push({ ...newCustomCategory.value })
}
emit('update:categoriesCustom', data)
newCustomCategory.value = null
editCustomCategoryKey.value = null
}
const removeCustomCategory = (key) => {
const data = categoriesCustom
delete data[key]
emit('update:categoriesCustom', Object.values(data))
}
const editCustomCategory = (key) => {
editCustomCategoryKey.value = key
}
const getOptions = (custom) => {
const data = []
custom.forEach((item) => {
data.push({ id: item.id, label: item[langs[0]] })
})
categories.value.forEach((item) => {
data.push({ id: item.categoryId, label: item.name !== '' ? item.name : t('side_menu', 'Other') })
})
data.sort((a, b) => (a.label < b.label ? -1 : 1))
return data
}
onMounted(async () => {
apps.value = await navStore.getApps()
categories.value = await navStore.getCategories()
let value = {}
apps.value.forEach((app) => {
if (!appsCategoriesCustom[app.id]) {
value[app.id] = null
} else {
value[app.id] = appsCategoriesCustom[app.id]
}
})
emit('update:appsCategoriesCustom', value)
})
</script>

View file

@ -1,82 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-apppicker">
<NcButton
aria-label="t('side_menu', 'Select apps')"
variant="primary"
@click="openModal"
>
{{ t('side_menu', 'Select apps') }} ({{ model.length }})
</NcButton>
<NcModal
v-if="modal"
size="small"
class="cm-settings-form-apppicker-modal"
@close="closeModal"
>
<div class="modal__content">
<NcCheckboxRadioSwitch
v-for="(item, key) in apps"
:key="key"
v-model="model"
name="value"
:value="item.id"
>
<img
:src="item.icon"
:alt="item.name"
/>
{{ item.name }}
</NcCheckboxRadioSwitch>
<div class="modal__footer">
<NcButton
variant="primary"
@click="closeModal"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</div>
</template>
<script setup>
import { NcButton, NcModal, NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { useNavStore } from '../../../store/nav.js'
import { ref, onMounted } from 'vue'
const model = defineModel({ type: Array })
const navStore = useNavStore()
const modal = ref(false)
const apps = ref([])
const openModal = () => {
modal.value = true
}
const closeModal = () => {
modal.value = false
}
onMounted(async () => {
apps.value = await navStore.getCoreApps()
})
</script>

View file

@ -1,116 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-appsort">
<NcButton
aria-label="t('side_menu', 'Sort')"
variant="primary"
@click="openModal"
>
{{ t('side_menu', 'Sort') }}
</NcButton>
<NcModal
v-if="modal"
size="small"
class="cm-settings-form-appsort-modal"
@close="closeModal"
>
<div class="modal__content">
<draggable
v-model="apps"
item-key="id"
@end="update"
>
<template #item="{ element }">
<div class="cm-settings-form-draggable">
<span class="cm-settings-form-arrow"></span>
{{ element.name }}
</div>
</template>
</draggable>
<div class="modal__footer">
<NcButton
variant="primary"
@click="closeModal"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</div>
</template>
<script setup>
import { NcButton, NcModal } from '@nextcloud/vue'
import { useNavStore } from '../../../store/nav.js'
import { ref, onMounted } from 'vue'
import draggable from 'vuedraggable'
const model = defineModel({ type: Array })
const emit = defineEmits(['update:modelValue'])
const navStore = useNavStore()
const modal = ref(false)
const apps = ref([])
const openModal = () => {
modal.value = true
}
const closeModal = () => {
modal.value = false
}
const setApps = (items) => {
apps.value = []
model.value.forEach((id) => {
items.forEach((app) => {
if (app.id === id) {
apps.value.push(app)
}
})
})
items.forEach((app) => {
if (!apps.value.find((element) => element.id === app.id)) {
apps.value.push(app)
}
})
}
const update = () => {
const value = []
apps.value.forEach((app) => {
value.push(app.id)
})
emit('update:modelValue', value)
}
onMounted(async () => {
const items = await navStore.getCoreApps()
window.setTimeout(() => {
setApps(items)
}, 500)
})
</script>

View file

@ -1,119 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-catsort">
<NcButton
aria-label="t('side_menu', 'Sort')"
variant="primary"
@click="openModal"
>
{{ t('side_menu', 'Sort') }}
</NcButton>
<NcModal
v-if="modal"
size="small"
class="cm-settings-form-catsort-modal"
@close="closeModal"
>
<div class="modal__content">
<draggable
v-model="apps"
item-key="categoryId"
@end="update"
>
<template #item="{ element }">
<div
v-if="element.name !== ''"
class="cm-settings-form-draggable"
>
<span class="cm-settings-form-arrow"></span>
{{ element.name }}
</div>
</template>
</draggable>
<div class="modal__footer">
<NcButton
variant="primary"
@click="closeModal"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</div>
</template>
<script setup>
import { NcButton, NcModal } from '@nextcloud/vue'
import { useNavStore } from '../../../store/nav.js'
import { ref, onMounted } from 'vue'
import draggable from 'vuedraggable'
const model = defineModel({ type: Array })
const emit = defineEmits(['update:modelValue'])
const navStore = useNavStore()
const modal = ref(false)
const apps = ref([])
const openModal = () => {
modal.value = true
}
const closeModal = () => {
modal.value = false
}
const setApps = (items) => {
apps.value = []
model.value.forEach((id) => {
items.forEach((app) => {
if (app.categoryId === id) {
apps.value.push(app)
}
})
})
items.forEach((app) => {
if (!apps.value.find((element) => element.categoryId === app.categoryId)) {
apps.value.push(app)
}
})
}
const update = () => {
const value = []
apps.value.forEach((app) => {
value.push(app.categoryId)
})
emit('update:modelValue', value)
}
onMounted(async () => {
const items = await navStore.getCategories()
window.setTimeout(() => {
setApps(items)
}, 500)
})
</script>

View file

@ -1,34 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<NcColorPicker
v-model="model"
:advancedFields="true"
class="cm-settings-form-colorpicker"
>
<div
:style="{ 'background-color': model }"
class="cm-settings-form-colorpicker-value"
/>
</NcColorPicker>
</template>
<script setup>
import { NcColorPicker } from '@nextcloud/vue'
const model = defineModel({ type: String })
</script>

View file

@ -1,85 +0,0 @@
<template>
<div class="cm-settings-form-displaypicker">
<div class="cm-settings-button-inline">
<NcButton
:variant="is(false, false, false) ? 'primary' : 'seconday'"
@click="update(false, false, false)"
>
{{ t('side_menu', 'Default') }}
</NcButton>
<NcButton
:variant="is(true, false, false) ? 'primary' : 'seconday'"
@click="update(true, false, false)"
>
{{ t('side_menu', 'Always displayed') }}
</NcButton>
<NcButton
:variant="is(false, true, false) ? 'primary' : 'seconday'"
@click="update(false, true, false)"
>
{{ t('side_menu', 'Big menu') }}
</NcButton>
<NcButton
:variant="is(false, false, true) ? 'primary' : 'seconday'"
@click="update(false, false, true)"
>
{{ t('side_menu', 'With categories') }}
</NcButton>
</div>
<p>
<img
v-if="is(false, false, false)"
:src="DefaultImg"
/>
<img
v-if="is(true, false, false)"
:src="AlwaysDisplayedImg"
/>
<img
v-if="is(false, true, false)"
class="side-menu-display"
:src="TopWideImg"
/>
<img
v-if="is(false, false, true)"
:src="SideMenuWithCategoriesImg"
/>
</p>
</div>
</template>
<script setup>
import { NcButton } from '@nextcloud/vue'
import AlwaysDisplayedImg from '../../../../img/admin/layout-always-displayed.svg'
import TopWideImg from '../../../../img/admin/layout-big-menu.svg'
import SideMenuWithCategoriesImg from '../../../../img/admin/layout-side-menu-with-categories.svg'
import DefaultImg from '../../../../img/admin/layout-default.svg'
const emit = defineEmits(['update:alwaysDisplayed', 'update:topWideMenu', 'update:sideMenuWithCategories'])
const { alwaysDisplayed, topWideMenu, sideMenuWithCategories } = defineProps({
alwaysDisplayed: {
type: Boolean,
required: true,
},
topWideMenu: {
type: Boolean,
required: true,
},
sideMenuWithCategories: {
type: Boolean,
required: true,
},
})
const update = (isAlwayDisplayed, isTopWideMenu, isSideMenuWithCategories) => {
emit('update:alwaysDisplayed', isAlwayDisplayed)
emit('update:topWideMenu', isTopWideMenu)
emit('update:sideMenuWithCategories', isSideMenuWithCategories)
}
const is = (isAlwayDisplayed, isTopWideMenu, isSideMenuWithCategories) => {
return isAlwayDisplayed === alwaysDisplayed && isTopWideMenu === topWideMenu && isSideMenuWithCategories === sideMenuWithCategories
}
</script>

View file

@ -1,37 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<FormSelect
v-model="model"
class="cm-settings-form-opener"
:options="options"
/>
</template>
<script setup>
import FormSelect from './FormSelect'
const model = defineModel({ type: String })
const options = [
{ id: 'side-menu-opener', label: 'Default' },
{ id: 'side-menu-opener-dark', label: 'Default (dark)' },
{ id: 'side-menu-opener-hamburger', label: 'Hamburger' },
{ id: 'side-menu-opener-hamburger-dark', label: 'Hamburger (dark)' },
{ id: 'side-menu-opener-hamburger-2', label: 'Hamburger 2' },
{ id: 'side-menu-opener-hamburger-2-dark', label: 'Hamburger 2 (dark)' },
]
</script>

View file

@ -1,65 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-range">
<em
v-if="prepend"
class="cm-settings-form-range-prepend"
>{{ t('side_menu', prepend) }}</em
>
<input
v-model="model"
type="range"
:min="min"
:max="max"
/>
<em
v-if="append"
class="cm-settings-form-range-append"
>{{ t('side_menu', append) }}</em
>
</div>
</template>
<script setup>
const model = defineModel({ type: Number })
defineProps({
prepend: {
type: [String, null],
required: false,
default: null,
},
append: {
type: [String, null],
required: false,
default: null,
},
min: {
type: Number,
required: false,
default: 0,
},
max: {
type: Number,
required: false,
default: 100,
},
})
</script>

View file

@ -1,78 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="cm-settings-form-select">
<template v-if="!expanded">
<select
v-if="!expanded"
v-model="model"
:multiple="multiple"
>
<option
v-if="!required"
:value="null"
></option>
<option
v-for="option in options"
:key="option.id"
:value="option.id"
>
{{ t('side_menu', option.label) }}
</option>
</select>
</template>
<template v-else>
<NcCheckboxRadioSwitch
v-for="option in options"
:key="option.id"
v-model="model"
:value="option.id"
:type="multiple ? 'checkbox' : 'radio'"
name="value"
>
{{ t('side_menu', option.label) }}
</NcCheckboxRadioSwitch>
</template>
</div>
</template>
<script setup>
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
const model = defineModel({ type: [Number, String, Array, null] })
const { options, expanded } = defineProps({
options: {
type: Array,
required: true,
},
required: {
type: Boolean,
required: false,
default: true,
},
expanded: {
type: Boolean,
required: false,
default: false,
},
multiple: {
type: Boolean,
required: false,
default: false,
},
})
</script>

View file

@ -1,35 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<FormSelect
v-model="model"
class="cm-settings-form-size"
:options="options"
/>
</template>
<script setup>
import FormSelect from './FormSelect'
const model = defineModel({ type: String })
const options = [
{ id: 'hidden', label: 'Hidden' },
{ id: 'small', label: 'Small' },
{ id: 'normal', label: 'Normal' },
{ id: 'big', label: 'Big' },
]
</script>

View file

@ -1,29 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<NcCheckboxRadioSwitch
v-model="model"
class="cm-settings-form-yesno"
type="switch"
/>
</template>
<script setup>
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
const model = defineModel({ type: Boolean })
</script>

View file

@ -1,13 +0,0 @@
import FormRange from './FormRange'
import FormColorPicker from './FormColorPicker'
import FormOpener from './FormOpener'
import FormSelect from './FormSelect'
import FormYesNo from './FormYesNo'
import FormSize from './FormSize'
import FormAppPicker from './FormAppPicker'
import FormAppSort from './FormAppSort'
import FormCatSort from './FormCatSort'
import FormDisplayPicker from './FormDisplayPicker'
import FormAppCategory from './FormAppCategory'
export { FormRange, FormColorPicker, FormOpener, FormSelect, FormYesNo, FormSize, FormAppPicker, FormAppSort, FormCatSort, FormDisplayPicker, FormAppCategory }

View file

@ -1,10 +0,0 @@
import SettingsSection from './SettingsSection'
import SettingItem from './SettingItem'
import SettingLabel from './SettingLabel'
import SettingValue from './SettingValue'
import SectionTitle from './SectionTitle'
import ExternalLink from './ExternalLink'
import AdminSaveButton from './AdminSaveButton'
import UserSaveButton from './UserSaveButton'
export { SettingsSection, SettingItem, SettingLabel, SettingValue, SectionTitle, ExternalLink, AdminSaveButton, UserSaveButton }

View file

@ -1,111 +1,95 @@
'Custom menu': 'Uživatelsky určená nabídka' "Custom menu": "Uživatelsky určená nabídka"
'Enable the custom menu': 'Zapnout uživatelsky určenou nabídku' "Enable the custom menu": "Zapnout uživatelsky určenou nabídku"
'No': 'Ne' "No": "Ne"
'Yes': 'Ano' "Yes": "Ano"
'Menu': 'Nabídka' "Menu": "Nabídka"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key 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 key.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Horní nabídka' : 'Pro otevření/skrytí postranní nabídky použijte zkratku <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">O</span> („O“ jako otevřít). Pro pohyb po použijte klávesu <span class="keyboard-key">Tab</span>.'
'Apps that not must be moved in the side menu': 'Aplikace, které nepřesouvat do postranní nabídky' "Top menu": "Horní nabídka"
'If there is no selection then the global configuration is applied.': 'Pokud neexistuje žádný výběr, je uplatněno globální nastavení.' "Apps that not must be moved in the side menu": "Aplikace, které nepřesouvat do postranní nabídky"
'Experimental': 'Experimentální' "If there is no selection then the global configuration is applied.": "Pokud neexistuje žádný výběr, je uplatněno globální nastavení."
'Save': 'Uložit' "Experimental": "Experimentální"
'You like this app and you want to support me?': 'Líbí se vám tato aplikace a chcete podpořit její vývoj?' "Save": "Uložit"
'Buy me a coffee ☕': 'Kupte mi kafe ☕' "You like this app and you want to support me?": "Líbí se vám tato aplikace a chcete podpořit její vývoj?"
'Hidden': 'Skryté' "Buy me a coffee ☕": "Kupte mi kafe ☕"
'Small': 'Malé' "Hidden": "Skryté"
'Normal': 'Normální' "Small": "Malé"
'Big': 'Velké' "Normal": "Normální"
'Colors': 'Barvy' "Big": "Velké"
'Background color': 'Barva pozadí' "Colors": "Barvy"
'Background color of current app': 'Barva pozadí stávající aplikace' "Background color": "Barva pozadí"
'Text color': 'Barva textu' "Background color of current app": "Barva pozadí stávající aplikace"
'Loader': 'Nástroj pro načítání' "Text color": "Barva textu"
'Icon': 'Ikona' "Loader": "Nástroj pro načítání"
'Same color': 'Stejná barva' "Icon": "Ikona"
'Opposite color': 'Doplňková barva' "Same color": "Stejná barva"
'Transparent': 'Průhledné' "Opposite color": "Doplňková barva"
'Opaque': 'Neprůhledné' "Transparent": "Průhledné"
'Opener': 'Tlačítko pro otevření' "Opaque": "Neprůhledné"
'Default': 'Výchozí' "Opener": "Tlačítko pro otevření"
'Default (dark)': 'Výchozí (tmavé)' "Default": "Výchozí"
'Hamburger': 'Hamburger' "Default (dark)": "Výchozí (tmavé)"
'Hamburger (dark)': 'Hamburger (tmavé)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (tmavé)"
'Hamburger 2 (dark)': 'Hamburger 2 (tmavé)' "Hamburger 2": "Hamburger 2"
'Before the logo': 'Před logem' "Hamburger 2 (dark)": "Hamburger 2 (tmavé)"
'After the logo': 'Za logem' "Before the logo": "Před logem"
'Position': 'Pozice' "After the logo": "Za logem"
'Show only the opener (hidden logo)': 'Zobrazovat pouze otevírací tlačítko (logo skryto)' "Position": "Pozice"
'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).' "Show only the opener (hidden logo)": "Zobrazovat pouze otevírací tlačítko (logo skryto)"
'Panel': 'Panel' "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)."
'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)' "Panel": "Panel"
'Display the big menu': 'Zobrazit velkou nabídku' "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 logo': 'Zobrazit logo' "Display the big menu": "Zobrazit velkou nabídku"
'Icons and texts': 'Ikony a texty' "Display the logo": "Zobrazit logo"
'Loader enabled': 'Načítání zapnuto' "Icons and texts": "Ikony a texty"
'Tips': 'Tipy' "Loader enabled": "Načítání zapnuto"
'Always displayed': 'Vždy zobrazeno' "Tips": "Tipy"
'This is the automatic behavior when the menu is always displayed.': 'Toto je automatické chování, kdy je nabídka vždy zobrazena.' "Always displayed": "Vždy zobrazeno"
'Not compatible with touch screens.': 'Nekompatibilní s dotykovými obrazovkami.' "This is the automatic behavior when the menu is always displayed.": "Toto je automatické chování, kdy je nabídka vždy zobrazena."
'Big menu': 'Velká nabídka' "Not compatible with touch screens.": "Nekompatibilní s dotykovými obrazovkami."
'Live preview': 'Živý náhled' "Big menu": "Velká nabídka"
'Open apps in new tab': 'Otevírat aplikace v novém panelu' "Live preview": "Živý náhled"
'Use the global setting': 'Použít globální nastavení' "Open apps in new tab": "Otevírat aplikace v novém panelu"
'Use my selection': 'Použít můj výběr' "Use the global setting": "Použít globální nastavení"
'Show and hide the list of applications': 'Zobrazit/skrýt seznam aplikací' "Use my selection": "Použít můj výběr"
'Use the avatar instead of the logo': 'Použít namísto loga profilový obrázek uživatele' "Show and hide the list of applications": "Zobrazit/skrýt seznam aplikací"
'You do not have permission to change the settings.': 'Nemáte oprávnění měnit nastavení.' "Use the avatar instead of the logo": "Použít namísto loga profilový obrázek uživatele"
'Force this configuration to users': 'Vynutit uplatnění těchto nastavení uživatelům' "You do not have permission to change the settings.": "Nemáte oprávnění měnit nastavení."
'Export the configuration': 'Exportovat nastavení' "Force this configuration to users": "Vynutit uplatnění těchto nastavení uživatelům"
'Purge the cache': 'Vyprázdnit mezipaměť' "Export the configuration": "Exportovat nastavení"
'Show the link to settings': 'Zobrazit odkaz na nastavení' "Purge the cache": "Vyprázdnit mezipaměť"
'The menu is enabled by default for users': 'Nabídka je ve výchozím stavu pro uživatele zapnutá' "Show the link to settings": "Zobrazit odkaz na nastavení"
'Except when the configuration is forced.': 'S výjimkou, kdy je nastavení vynuceno.' "The menu is enabled by default for users": "Nabídka je ve výchozím stavu pro uživatele zapnutá"
'Apps that should not be displayed in the menu': 'Aplikace, které by neměly být v nabídce zobrazeny' "Except when the configuration is forced.": "S výjimkou, kdy je nastavení vynuceno."
'This feature is only compatible with the <code>big menu</code> display.': 'Tato funkce je kompatibilní pouze s <code>velkou nabídkou</code>.' "Apps that should not be displayed in the menu": "Aplikace, které by neměly být v nabídce zobrazeny"
'The logo is a link to the default app': 'Logo je odkaz na výchozí aplikaci' "This feature is only compatible with the <code>big menu</code> display.": "Tato funkce je kompatibilní pouze s <code>velkou nabídkou</code>."
'Others': 'Ostatní' "The logo is a link to the default app": "Logo je odkaz na výchozí aplikaci"
'Categories': 'Kategorie' "Others": "Ostatní"
'Customize sorting': 'Přizpůsobit si řazení' "Categories": "Kategorie"
'Order by': 'Řadit podle' "Customize sorting": "Přizpůsobit si řazení"
'Name': 'Název' "Order by": "Řadit podle"
'Customed': 'Přizpůsobeno' "Name": "Název"
'Show and hide the list of categories': 'Zobrazit/skrýt seznam kategorií' "Customed": "Přizpůsobeno"
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Tyto parametry jsou použity v případě, že je zapnutý (Breeze) tmavý motiv vzhledu.' "Show and hide the list of categories": "Zobrazit/skrýt seznam kategorií"
'Dark mode colors': 'Barvy tmavého režimu' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Tyto parametry jsou použity v případě, že je zapnutý (Breeze) tmavý motiv vzhledu."
'With categories': 'S kategoriemi' "Dark mode colors": "Barvy tmavého režimu"
'Custom categories': 'Vlastní kategorie' "With categories": "S kategoriemi"
'Customize application categories': 'Přizpůsobte kategorie aplikací' "Custom categories": "Vlastní kategorie"
'Reset to default': 'Vrátit zpět na výchozí hodnoty' "Customize application categories": "Přizpůsobte kategorie aplikací"
'Hidden icon': 'Skrytá ikona' "Reset to default": "Vrátit zpět na výchozí hodnoty"
'Small icon': 'Malá ikona' "Hidden icon": "Skrytá ikona"
'Normal icon': 'Normální ikona' "Small icon": "Malá ikona"
'Big icon': 'Velká ikona' "Normal icon": "Normální ikona"
'Hidden text': 'Skrytý text' "Big icon": "Velká ikona"
'Small text': 'Malý text' "Hidden text": "Skrytý text"
'Normal text': 'Normální text' "Small text": "Malý text"
'Big text': 'Velký text' "Normal text": "Normální text"
'Applications': 'Aplikace' "Big text": "Velký text"
'Applications kept in the top menu': 'Aplikace ponechané v horní nabídce' "Applications": "Aplikace"
'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í' "Applications kept in the top menu": "Aplikace ponechané v horní nabídce"
'These applications must be selected in the previous option.': 'Tyto aplikace je třeba vybrat v předchozí volbě.' "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í"
'Hide labels on mouse over': 'Skrýt popisky při najetím ukazatele myši' "These applications must be selected in the previous option.": "Tyto aplikace je třeba vybrat v předchozí volbě."
'Except the hovered app': 'S výjimkou nadnášené aplikace' "Hide labels on mouse over": "Skrýt popisky při najetím ukazatele myši"
'Search': 'Hledat' "Except the hovered app": "Except the hovered app"
'Toggle the menu': 'Vyp/zap nabídku' "Search": "Search"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Zeptejte se vývojáře'
'New request': 'Nový požadavek'
'Report a bug': 'Nahlásit chybu'
'Show the configuration': 'Zobrazit nastavení'
'Configuration:': 'Configuration:'
'Done!': 'Hotovo!'
'Copy': 'Zkopírovat'
'Need help': 'Potřebuji pomoc'
'I would like a new feature': 'Rád bych novou funkci v aplikaci'
'Something went wrong': 'Něco se pokazilo'
'Select apps': 'Vyberte aplikace'
'Sort': 'Seřadit'
'Customize': 'Přizpůsobit'
'Custom': 'Custom'
'Close': 'Zavřít'

View file

@ -1,111 +1,95 @@
'Custom menu': 'Benutzerdefiniertes Menü' "Custom menu": "Benutzerdefiniertes Menü"
'Enable the custom menu': 'Benutzerdefiniertes Menü aktivieren' "Enable the custom menu": "Benutzerdefiniertes Menü aktivieren"
'No': 'Nein' "No": "Nein"
'Yes': 'Ja' "Yes": "Ja"
'Menu': 'Menü' "Menu": "Menü"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Verwende die Tastenkombination <span class="keyboard-key">Strg</span>+o, um das Seitenmenü ein- und auszublenden. Verwende tab key zum Navigieren.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Obere Navigationsleiste' : 'Verwende die Tastenkombination <span class="keyboard-key">Strg</span>+<span class="keyboard-key">o</span>, um das Seitenmenü ein- und auszublenden. Verwende <span class="keyboard-key">tab</span> zum Navigieren.'
'Apps that not must be moved in the side menu': 'Apps, die nicht ins Seitenmenü verschoben werden sollen' "Top menu": "Obere Navigationsleiste"
'If there is no selection then the global configuration is applied.': 'Wenn keine Auswahl vorhanden ist, wird die globale Konfiguration angewendet.' "Apps that not must be moved in the side menu": "Apps, die nicht ins Seitenmenü verschoben werden sollen"
'Experimental': 'Experimentell' "If there is no selection then the global configuration is applied.": "Wenn keine Auswahl vorhanden ist, wird die globale Konfiguration angewendet."
'Save': 'Speichern' "Experimental": "Experimentell"
'You like this app and you want to support me?': 'Du magst diese App und möchtest mich unterstützen?' "Save": "Speichern"
'Buy me a coffee ☕': 'Gib mir einen Kaffee aus ☕' "You like this app and you want to support me?": "Du magst diese App und möchtest mich unterstützen?"
'Hidden': 'Ausblenden' "Buy me a coffee ☕": "Gib mir einen Kaffee aus ☕"
'Small': 'Klein' "Hidden": "Ausblenden"
'Normal': 'Normal' "Small": "Klein"
'Big': 'Groß' "Normal": "Normal"
'Colors': 'Farben' "Big": "Groß"
'Background color': 'Hintergrundfarbe' "Colors": "Farben"
'Background color of current app': 'Hintergrundfarbe der aktuellen App' "Background color": "Hintergrundfarbe"
'Text color': 'Textfarbe' "Background color of current app": "Hintergrundfarbe der aktuellen App"
'Loader': 'Fortschrittsbalken' "Text color": "Textfarbe"
'Icon': 'Symbol' "Loader": "Fortschrittsbalken"
'Same color': 'Selbe Farbe' "Icon": "Symbol"
'Opposite color': 'Gegenfarbe' "Same color": "Selbe Farbe"
'Transparent': 'Transparent' "Opposite color": "Gegenfarbe"
'Opaque': 'Nicht transparent' "Transparent": "Transparent"
'Opener': 'Menü-Symbol' "Opaque": "Nicht transparent"
'Default': 'Standard' "Opener": "Menü-Symbol"
'Default (dark)': 'Standard (dunkel)' "Default": "Standard"
'Hamburger': 'Hamburger' "Default (dark)": "Standard (dunkel)"
'Hamburger (dark)': 'Hamburger (dunkel)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (dunkel)"
'Hamburger 2 (dark)': 'Hamburger 2 (dunkel)' "Hamburger 2": "Hamburger 2"
'Before the logo': 'Vor dem Logo' "Hamburger 2 (dark)": "Hamburger 2 (dunkel)"
'After the logo': 'Nach dem Logo' "Before the logo": "Vor dem Logo"
'Position': 'Position' "After the logo": "Nach dem Logo"
'Show only the opener (hidden logo)': 'Nur das Menü-Symbol anzeigen (Logo wird ausgeblendet)' "Position": "Position"
'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).' "Show only the opener (hidden logo)": "Nur das Menü-Symbol anzeigen (Logo wird ausgeblendet)"
'Panel': 'Panel' "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)."
'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)' "Panel": "Panel"
'Display the big menu': 'Großes Menü anzeigen' "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 logo': 'Logo anzeigen' "Display the big menu": "Großes Menü anzeigen"
'Icons and texts': 'Symbole und Texte' "Display the logo": "Logo anzeigen"
'Loader enabled': 'Fortschrittsbalken anzeigen' "Icons and texts": "Symbole und Texte"
'Tips': 'Tipps' "Loader enabled": "Fortschrittsbalken anzeigen"
'Always displayed': 'Immer anzeigen' "Tips": "Tipps"
'This is the automatic behavior when the menu is always displayed.': 'Dies ist das automatische Verhalten, wenn das Menü immer angezeigt wird.' "Always displayed": "Immer anzeigen"
'Not compatible with touch screens.': 'Nicht kompatibel mit Touchscreens.' "This is the automatic behavior when the menu is always displayed.": "Dies ist das automatische Verhalten, wenn das Menü immer angezeigt wird."
'Big menu': 'Großes Menü' "Not compatible with touch screens.": "Nicht kompatibel mit Touchscreens."
'Live preview': 'Live-Vorschau' "Big menu": "Großes Menü"
'Open apps in new tab': 'Öffne Apps in einem neuen Tab' "Live preview": "Live-Vorschau"
'Use the global setting': 'Verwende die globale Einstellung' "Open apps in new tab": "Öffne Apps in einem neuen Tab"
'Use my selection': 'Verwende meine Auswahl' "Use the global setting": "Verwende die globale Einstellung"
'Show and hide the list of applications': 'Ein- und Ausblenden der Appliste' "Use my selection": "Verwende meine Auswahl"
'Use the avatar instead of the logo': 'Avatar anstelle des Logos anzeigen' "Show and hide the list of applications": "Ein- und Ausblenden der Appliste"
'You do not have permission to change the settings.': 'Du hast keine Berechtigung, die Einstellungen dieser App zu ändern.' "Use the avatar instead of the logo": "Avatar anstelle des Logos anzeigen"
'Force this configuration to users': 'Konfiguration für alle Benutzer erzwingen' "You do not have permission to change the settings.": "Du hast keine Berechtigung, die Einstellungen dieser App zu ändern."
'Export the configuration': 'Konfiguration exportieren' "Force this configuration to users": "Konfiguration für alle Benutzer erzwingen"
'Purge the cache': 'Cache leeren' "Export the configuration": "Konfiguration exportieren"
'Show the link to settings': 'Link zu den Einstellungen anzeigen' "Purge the cache": "Cache leeren"
'The menu is enabled by default for users': 'Das Menü ist standardmäßig für alle Benutzer aktiviert' "Show the link to settings": "Link zu den Einstellungen anzeigen"
'Except when the configuration is forced.': 'Gilt nicht, wenn die Konfiguration erzwungen wird.' "The menu is enabled by default for users": "Das Menü ist standardmäßig für alle Benutzer aktiviert"
'Apps that should not be displayed in the menu': 'Apps, die nicht im Menü angezeigt werden sollen' "Except when the configuration is forced.": "Gilt nicht, wenn die Konfiguration erzwungen wird."
'This feature is only compatible with the <code>big menu</code> display.': 'Kompatibel mit dem <code>großen Menü</code>.' "Apps that should not be displayed in the menu": "Apps, die nicht im Menü angezeigt werden sollen"
'The logo is a link to the default app': 'Das Logo ist ein Link zur Standard-App' "This feature is only compatible with the <code>big menu</code> display.": "Kompatibel mit dem <code>großen Menü</code>."
'Others': 'Andere' "The logo is a link to the default app": "Das Logo ist ein Link zur Standard-App"
'Categories': 'Kategorien' "Others": "Andere"
'Customize sorting': 'Sortierung anpassen' "Categories": "Kategorien"
'Order by': 'Sortieren nach' "Customize sorting": "Sortierung anpassen"
'Name': 'Name' "Order by": "Sortieren nach"
'Customed': 'Benutzerdefiniert' "Name": "Name"
'Show and hide the list of categories': 'Liste der Kategorien ein- und ausblenden' "Customed": "Benutzerdefiniert"
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Diese Optionen werden auf <code>Dark Theme</code> oder <code>Breeze Dark Theme</code> angewendet.' "Show and hide the list of categories": "Liste der Kategorien ein- und ausblenden"
'Dark mode colors': 'Farben für den dunklen Modus' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Diese Optionen werden auf <code>Dark Theme</code> oder <code>Breeze Dark Theme</code> angewendet."
'With categories': 'Mit Kategorien' "Dark mode colors": "Farben für den dunklen Modus"
'Custom categories': 'Benutzerdefinierte Kategorien' "With categories": "Mit Kategorien"
'Customize application categories': 'App-Kategorien anpassen' "Custom categories": "Benutzerdefinierte Kategorien"
'Reset to default': 'Auf Standard zurücksetzen' "Customize application categories": "App-Kategorien anpassen"
'Hidden icon': 'Verstecktes Symbol' "Reset to default": "Auf Standard zurücksetzen"
'Small icon': 'Kleines Symbol' "Hidden icon": "Verstecktes Symbol"
'Normal icon': 'Normales Symbol' "Small icon": "Kleines Symbol"
'Big icon': 'Großes Icon' "Normal icon": "Normales Symbol"
'Hidden text': 'Versteckter Text' "Big icon": "Großes Icon"
'Small text': 'Kleiner Text' "Hidden text": "Versteckter Text"
'Normal text': 'Normaler Text' "Small text": "Kleiner Text"
'Big text': 'Großer Text' "Normal text": "Normaler Text"
'Applications': 'Apps' "Big text": "Großer Text"
'Applications kept in the top menu': 'Apps in der oberen Navigationsleiste' "Applications": "Apps"
'Applications kept in the top menu but also shown in side menu': 'Apps in der oberen Navigationsleiste, die auch im Seitenmenü angezeigt werden sollen' "Applications kept in the top menu": "Apps in der oberen Navigationsleiste"
'These applications must be selected in the previous option.': 'Diese Apps müssen auch in der vorherigen Einstellung ausgewählt werden.' "Applications kept in the top menu but also shown in side menu": "Apps in der oberen Navigationsleiste, die auch im Seitenmenü angezeigt werden sollen"
'Hide labels on mouse over': 'Labels ausblenden, wenn sich die Maus darüber befindet (Hover)' "These applications must be selected in the previous option.": "Diese Apps müssen auch in der vorherigen Einstellung ausgewählt werden."
'Except the hovered app': 'Außer die markierte App' "Hide labels on mouse over": "Labels ausblenden, wenn sich die Maus darüber befindet (Hover)"
'Search': 'Suche' "Except the hovered app": "Except the hovered app"
'Toggle the menu': 'Menü ein- und ausblenden' "Search": "Search"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Ask the developer'
'New request': 'New request'
'Report a bug': 'Report a bug'
'Show the configuration': 'Show the configuration'
'Configuration:': 'Configuration:'
'Done!': 'Done!'
'Copy': 'Copy'
'Need help': 'Need help'
'I would like a new feature': 'I would like a new feature'
'Something went wrong': 'Something went wrong'
'Select apps': 'Select apps'
'Sort': 'Sort'
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'

View file

@ -1,111 +1,95 @@
'Custom menu': 'Menú personalizado' "Custom menu": "Menú personalizado"
'Enable the custom menu': 'Activar el menú personalizado' "Enable the custom menu": "Habilitar el menú personalizado"
'No': 'No' "No": "No"
'Yes': 'Sí' "Yes": "Sí"
'Menu': 'Menú' "Menu": "Menú"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Usa la combinación de teclas Ctrl+o para activar y desactivar el menú lateral. Use tab key para navegar.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Menu principal' : 'Usa la combinación de teclas <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> para activar y desactivar el menú lateral. Use <span class="keyboard-key">tab</span> para navegar.'
'Apps that not must be moved in the side menu': 'Aplicaciones que no se deben mover al menú lateral' "Top menu": "Menu principal"
'If there is no selection then the global configuration is applied.': 'Si no hay selección, se aplica la configuración global.' "Apps that not must be moved in the side menu": "Aplicaciones que no se deben mover al menú lateral"
'Experimental': 'En pruebas' "If there is no selection then the global configuration is applied.": "Si no hay selección, se aplica la configuración global."
'Save': 'Guardar' "Experimental": "En pruebas"
'You like this app and you want to support me?': '¿Te gusta esta aplicación y quieres apoyarme?' "Save": "Guardar"
'Buy me a coffee ☕': 'Cómprame un café ☕' "You like this app and you want to support me?": "¿Te gusta esta aplicación y quieres apoyarme?"
'Hidden': 'Oculto' "Buy me a coffee ☕": "Cómprame un café ☕"
'Small': 'Pequeño' "Hidden": "Oculto"
'Normal': 'Normal' "Small": "Pequeño"
'Big': 'Grande' "Normal": "Normal"
'Hidden icon': 'Ocultar Icono' "Big": "Grande"
'Small icon': 'Icono pequeño' "Hidden icon": "Ocultar Icono"
'Normal icon': 'Icono normal' "Small icon": "Icono pequeño"
'Big icon': 'Icono grande' "Normal icon": "Icono normal"
'Hidden text': 'Texto oculto' "Big icon": "Icono grande"
'Small text': 'Texto pequeño' "Hidden text": "Texto oculto"
'Normal text': 'Texto normal' "Small text": "Texto pequeño"
'Big text': 'Texto grande' "Normal text": "Texto normal"
'Colors': 'Colores' "Big text": "Texto grande"
'Background color': 'Color de fondo' "Colors": "Colores"
'Background color of current app': 'Color de fondo de la aplicación actual' "Background color": "Color de fondo"
'Text color': 'Color del texto' "Background color of current app": "Color de fondo de la aplicación actual"
'Loader': 'Cargador' "Text color": "Color del texto"
'Icon': 'Icono' "Loader": "Cargador"
'Same color': 'El mismo color' "Icon": "Icono"
'Opposite color': 'Color opuesto' "Same color": "El mismo color"
'Transparent': 'Transparente' "Opposite color": "Color opuesto"
'Opaque': 'Opaco' "Transparent": "Transparente"
'Opener': 'Abrir' "Opaque": "Opaco"
'Default': 'Por defecto' "Opener": "Abrir"
'Default (dark)': 'Por defecto (oscuro)' "Default": "Por defecto"
'Hamburger': 'Hamburguesa' "Default (dark)": "Por defecto (oscuro)"
'Hamburger (dark)': 'Hamburger (negro)' "Hamburger": "Hamburguesa"
'Hamburger 2': 'Hamburguesa 2' "Hamburger (dark)": "Hamburger (negro)"
'Hamburger 2 (dark)': 'Hamburger 2 (negro)' "Hamburger 2": "Hamburguesa 2"
'Before the logo': 'Antes del logotipo' "Hamburger 2 (dark)": "Hamburger 2 (negro)"
'After the logo': 'Después del logotipo' "Before the logo": "Antes del logotipo"
'Position': 'Posición' "After the logo": "Después del logotipo"
'Show only the opener (hidden logo)': 'Mostrar solo abrir (ocultar logotipo)' "Position": "Posición"
'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).' "Show only the opener (hidden logo)": "Mostrar solo abrir (ocultar logotipo)"
'Panel': 'Panel' "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)."
'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)' "Panel": "Panel"
'Display the big menu': 'Mostrar el menú grande' "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 logo': 'Mostrar el logotipo' "Display the big menu": "Mostrar el menú grande"
'Icons and texts': 'Iconos y textos' "Display the logo": "Mostrar el logotipo"
'Loader enabled': 'Cargador activado' "Icons and texts": "Iconos y textos"
'Tips': 'Consejos' "Loader enabled": "Cargador activado"
'Always displayed': 'Siempre se muestra' "Tips": "Consejos"
'This is the automatic behavior when the menu is always displayed.': 'Este es el comportamiento automático cuando aún se muestra el menú.' "Always displayed": "Siempre se muestra"
'Not compatible with touch screens.': 'No es compatible con las pantallas táctiles.' "This is the automatic behavior when the menu is always displayed.": "Este es el comportamiento automático cuando aún se muestra el menú."
'Big menu': 'Menú grande' "Not compatible with touch screens.": "No es compatible con las pantallas táctiles."
'Live preview': 'Previsualización en directo' "Big menu": "Menú grande"
'Open apps in new tab': 'Abrir las aplicaciones en una nueva pestaña' "Live preview": "Previsualización en directo"
'Use the global setting': 'Utilizar la configuración global' "Open apps in new tab": "Abrir las aplicaciones en una nueva pestaña"
'Use my selection': 'Utilizar mi selección' "Use the global setting": "Utilizar la configuración global"
'Show and hide the list of applications': 'Mostrar y ocultar la lista de aplicaciones' "Use my selection": "Utilizar mi selección"
'Use the avatar instead of the logo': 'Utilizar un avatar en lugar de un logotipo' "Show and hide the list of applications": "Mostrar y ocultar la lista de aplicaciones"
'You do not have permission to change the settings.': 'No tienes permiso para cambiar la configuración.' "Use the avatar instead of the logo": "Utilizar un avatar en lugar de un logotipo"
'Force this configuration to users': 'Forzar esta configuración a todos los usuarios' "You do not have permission to change the settings.": "No tienes permiso para cambiar la configuración."
'Export the configuration': 'Exportar la configuración' "Force this configuration to users": "Forzar esta configuración a todos los usuarios"
'Purge the cache': 'Vaciar la caché' "Export the configuration": "Exportar la configuración"
'Show the link to settings': 'Mostrar un enlace a la configuración' "Purge the cache": "Vaciar la caché"
'The menu is enabled by default for users': 'El menú está activado por defecto para los usuarios' "Show the link to settings": "Mostrar un enlace a la configuración"
'Except when the configuration is forced.': 'Excepto cuando la configuración es forzada.' "The menu is enabled by default for users": "El menú está activado por defecto para los usuarios"
'Apps that should not be displayed in the menu': 'Aplicaciones que no deben aparecer en el menú' "Except when the configuration is forced.": "Excepto cuando la configuración es forzada."
'This feature is only compatible with the <code>big menu</code> display.': 'Esta función sólo es compatible con la pantalla del <code>menú grande</code>.' "Apps that should not be displayed in the menu": "Aplicaciones que no deben aparecer en el menú"
'The logo is a link to the default app': 'El logotipo es un enlace a la aplicación por defecto' "This feature is only compatible with the <code>big menu</code> display.": "Esta función sólo es compatible con la pantalla del <code>menú grande</code>."
'Others': 'Otros' "The logo is a link to the default app": "El logotipo es un enlace a la aplicación por defecto"
'Categories': 'Categorías' "Others": "Otros"
'Customize sorting': 'Personalizar la clasificación' "Categories": "Categorías"
'Order by': 'Ordenar por' "Customize sorting": "Personalizar la clasificación"
'Name': 'Nombre' "Order by": "Ordenar por"
'Customed': 'Personalizado' "Name": "Nombre"
'Show and hide the list of categories': 'Mostrar y ocultar la lista de categorías' "Customed": "Personalizado"
'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.' "Show and hide the list of categories": "Mostrar y ocultar la lista de categorías"
'Dark mode colors': 'Colores del modo oscuro' "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."
'With categories': 'Con categorías' "Dark mode colors": "Colores del modo oscuro"
'Custom categories': 'Categorías personalizadas' "With categories": "Con categorías"
'Customize application categories': 'Personalizar las categorías de las aplicaciones' "Custom categories": "Categorías personalizadas"
'Reset to default': 'Restablecer los valores por defecto' "Customize application categories": "Personalizar las categorías de las aplicaciones"
'Applications': 'Aplicaciones' "Reset to default": "Restablecer los valores por defecto"
'Applications kept in the top menu': 'Aplicaciones guardadas en el menú superior' "Applications": "Aplicaciones"
'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' "Applications kept in the top menu": "Aplicaciones guardadas en el menú superior"
'These applications must be selected in the previous option.': 'Estas aplicaciones deben ser seleccionadas en las opciones anteriores.' "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"
'Hide labels on mouse over': 'Ocultar las etiquetas al pasar el ratón' "These applications must be selected in the previous option.": "Estas aplicaciones deben ser seleccionadas en las opciones anteriores."
'Except the hovered app': 'Excepto la aplicación sobre la que se pasa el cursor' "Hide labels on mouse over": "Ocultar las etiquetas al pasar el ratón"
'Search': 'Buscar' "Except the hovered app": "Except the hovered app"
'Toggle the menu': 'Alternar el menú' "Search": "Search"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Pregúntale al desarrollador'
'New request': 'Nueva solicitud'
'Report a bug': 'Informar de un fallo'
'Show the configuration': 'Mostrar los ajustes'
'Configuration:': 'Configuration:'
'Done!': '¡Realizado!'
'Copy': 'Copiar'
'Need help': 'Ayudame'
'I would like a new feature': 'Me gustaría una nueva función'
'Something went wrong': 'Algo salió mal'
'Select apps': 'Selecciona las aplicaciones'
'Sort': 'Ordenar'
'Customize': 'Personalizar'
'Custom': 'Custom'
'Close': 'Cerrar'

View file

@ -1,130 +1,95 @@
'Custom menu': 'Menu personnalisé' "Custom menu": "Menu personnalisé"
'Enable the custom menu': 'Activer le menu personnalisé' "Enable the custom menu": "Activer le menu personnalisé"
'No': 'Non' "No": "Non"
'Yes': 'Oui' "Yes": "Oui"
'Menu': 'Menu' "Menu": "Menu"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Utiliser ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
le raccourcis clavier Ctrl+o pour ouvrir et fermer le menu latéral. Utiliser tab : 'Utiliser le raccourcis clavier <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> pour ouvrir et fermer le menu latéral. Utiliser <span class="keyboard-key">tab</span> pour naviguer.'
key pour naviguer.' "Top menu": "Menu supérieur"
'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"
'Apps that not must be moved in the side menu': 'Les applications qui ne doivent pas "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."
être affichées dans le menu latéral' "Experimental": "Expérimental"
'If there is no selection then the global configuration is applied.': "Si il n'y a "Save": "Sauvegarder"
aucune sélection alors la configuration globale sera appliquée." "You like this app and you want to support me?": "Vous aimer cette application et vous souhaitez m'aider ?"
'Experimental': 'Expérimental' "Buy me a coffee ☕": "Offrez moi un café ☕"
'Save': 'Sauvegarder' "Hidden": "Caché"
'You like this app and you want to support me?': "Vous aimer cette application et "Small": "Petit"
vous souhaitez m'aider ?" "Normal": "Normal"
'Buy me a coffee ☕': 'Offrez moi un café ☕' "Big": "Gros"
'Hidden': 'Caché' "Hidden icon": "Icône masqué"
'Small': 'Petit' "Small icon": "Petit icône"
'Normal': 'Normal' "Normal icon": "Icône normal"
'Big': 'Gros' "Big icon": "Gros icône"
'Hidden icon': 'Icône masqué' "Hidden text": "Text masqué"
'Small icon': 'Petit icône' "Small text": "Texte petit"
'Normal icon': 'Icône normal' "Normal text": "Texte normal"
'Big icon': 'Gros icône' "Big text": "Gros texte"
'Hidden text': 'Text masqué' "Colors": "Couleurs"
'Small text': 'Texte petit' "Background color": "Couleur de fond"
'Normal text': 'Texte normal' "Background color of current app": "Couleur de fond de l'application en cours"
'Big text': 'Gros texte' "Text color": "Couleur du texte"
'Colors': 'Couleurs' "Loader": "Indicateur de chargement"
'Background color': 'Couleur de fond' "Icon": "Icône"
'Background color of current app': "Couleur de fond de l'application en cours" "Same color": "Même couleur"
'Text color': 'Couleur du texte' "Opposite color": "Couleur opposée"
'Loader': 'Indicateur de chargement' "Transparent": "Transparent"
'Icon': 'Icône' "Opaque": "Opaque"
'Same color': 'Même couleur' "Opener": "Bouton d'ouverture"
'Opposite color': 'Couleur opposée' "Default": "Par défaut"
'Transparent': 'Transparent' "Default (dark)": "Par défaut (sombre)"
'Opaque': 'Opaque' "Hamburger": "Hamburger"
'Opener': "Bouton d'ouverture" "Hamburger (dark)": "Hamburger (sombre)"
'Default': 'Par défaut' "Hamburger 2": "Hamburger 2"
'Default (dark)': 'Par défaut (sombre)' "Hamburger 2 (dark)": "Hamburger 2 (sombre)"
'Hamburger': 'Hamburger' "Before the logo": "Avant le logo"
'Hamburger (dark)': 'Hamburger (sombre)' "After the logo": "Après le logo"
'Hamburger 2': 'Hamburger 2' "Position": "Position"
'Hamburger 2 (dark)': 'Hamburger 2 (sombre)' "Show only the opener (hidden logo)": "Afficher uniquement le bouton d'ouverture (masquer le logo)"
'Before the logo': 'Avant le logo' "Do not display the side menu and the opener if there is no application (eg: public pages).": "Ne pas afficher le menu latéral et le bouton d'ouverture s'il n'y a aucune application (exemple : page publiques)."
'After the logo': 'Après le logo' "Panel": "Panneau"
'Position': 'Position' "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)"
'Show only the opener (hidden logo)': "Afficher uniquement le bouton d'ouverture (masquer "Display the big menu": "Afficher le menu large"
le logo)" "Display the logo": "Afficher le logo"
'Do not display the side menu and the opener if there is no application (eg: public pages).': "Ne "Icons and texts": "Icônes et textes"
pas afficher le menu latéral et le bouton d'ouverture s'il n'y a aucune application "Loader enabled": "Activation de l'indicateur de chargement"
(exemple : page publiques)." "Tips": "Astuces"
'Panel': 'Panneau' "Always displayed": "Toujours affiché"
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Ouvrir "This is the automatic behavior when the menu is always displayed.": "C'est le comportement automatique lorsque le menu est toujours affiché."
le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)' "Not compatible with touch screens.": "Incompatible avec les écrans tactiles."
'Display the big menu': 'Afficher le menu large' "Big menu": "Menu large"
'Display the logo': 'Afficher le logo' "Live preview": "Aperçu en direct"
'Icons and texts': 'Icônes et textes' "Open apps in new tab": "Ouvrir les applications dans un nouvel onglet"
'Loader enabled': "Activation de l'indicateur de chargement" "Use the global setting": "Utiliser la configuration globale"
'Tips': 'Astuces' "Use my selection": "Utiliser ma sélection"
'Always displayed': 'Toujours affiché' "Show and hide the list of applications": "Afficher et masquer la liste des applications"
'This is the automatic behavior when the menu is always displayed.': "C'est le comportement "Use the avatar instead of the logo": "Utiliser l'avatar à la place du logo"
automatique lorsque le menu est toujours affiché." "You do not have permission to change the settings.": "Vous n'avez pas la permission de changer les paramètres."
'Not compatible with touch screens.': 'Incompatible avec les écrans tactiles.' "Force this configuration to users": "Forcer cette configuration aux utilisateurs"
'Big menu': 'Menu large' "Export the configuration": "Exporter la configuration"
'Live preview': 'Aperçu en direct' "Purge the cache": "Purger le cache"
'Open apps in new tab': 'Ouvrir les applications dans un nouvel onglet' "Show the link to settings": "Afficher le lien vers les paramètres"
'Use the global setting': 'Utiliser la configuration globale' "The menu is enabled by default for users": "Le menu est activé par défaut pour les utilisateurs"
'Use my selection': 'Utiliser ma sélection' "Except when the configuration is forced.": "Sauf lorsque la configuration est forcée."
'Show and hide the list of applications': 'Afficher et masquer la liste des applications' "Apps that should not be displayed in the menu": "Applications qui ne doivent pas être affichées dans le menu"
'Use the avatar instead of the logo': "Utiliser l'avatar à la place du logo" "This feature is only compatible with the <code>big menu</code> display.": "Compatible avec l'affichage <code>Menu large</code>."
'You do not have permission to change the settings.': "Vous n'avez pas la permission "The logo is a link to the default app": "Le logo est un lien vers l'application par défaut"
de changer les paramètres." "Others": "Autres"
'Force this configuration to users': 'Forcer cette configuration aux utilisateurs' "Categories": "Catégories"
'Export the configuration': 'Exporter la configuration' "Customize sorting": "Personnaliser le tri"
'Purge the cache': 'Purger le cache' "Order by": "Trier par"
'Show the link to settings': 'Afficher le lien vers les paramètres' "Name": "Nom"
'The menu is enabled by default for users': 'Le menu est activé par défaut pour les "Customed": "Personnalisé"
utilisateurs' "Show and hide the list of categories": "Afficher et masquer la liste des catégories"
'Except when the configuration is forced.': 'Sauf lorsque la configuration est forcée.' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Ces paramètres sont utilisés lorsque le thème sombre ou le thème Breeze Dark sont activés."
'Apps that should not be displayed in the menu': 'Applications qui ne doivent pas "Dark mode colors": "Couleurs du mode sombre"
être affichées dans le menu' "With categories": "Avec les catégories"
'This feature is only compatible with the <code>big menu</code> display.': "Compatible "Custom categories": "Catégories personnalisées"
avec l'affichage <code>Menu large</code>." "Customize application categories": "Personnaliser les catégories des applications"
'The logo is a link to the default app': "Le logo est un lien vers l'application par "Reset to default": "Restaurer les valeurs par défaut"
défaut" "Applications": "Applications"
'Others': 'Autres' "Applications kept in the top menu": "Applications conservées dans le menu supérieur"
'Categories': 'Catégories' "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"
'Customize sorting': 'Personnaliser le tri' "These applications must be selected in the previous option.": "Ces applications doivent également être sélectionnées dans l'option précédente."
'Order by': 'Trier par' "Hide labels on mouse over": "Masquer le libellé des applications au passage de la souris"
'Name': 'Nom' "Except the hovered app": "À l'exception de l'application survolée"
'Customed': 'Personnalisé' "Search": "Rechercher"
'Show and hide the list of categories': 'Afficher et masquer la liste des catégories'
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Ces
paramètres sont utilisés lorsque le thème sombre ou le thème Breeze Dark sont activés.'
'Dark mode colors': 'Couleurs du mode sombre'
'With categories': 'Avec les catégories'
'Custom categories': 'Catégories personnalisées'
'Customize application categories': 'Personnaliser les catégories des applications'
'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'
'Open the documentation': 'Afficher la documentation'
'Ask the developer': 'Demander au(x) développeurs⋅euses'
'New request': 'Nouvelle requête'
'Report a bug': 'Rapporter un bug'
'Show the configuration': 'Afficher la configuration'
'Configuration:': 'Configuration :'
'Done!': 'Fait !'
'Copy': 'Copié'
'Need help': "Besoin d'aide"
'I would like a new feature': 'Je souhaiterais une fonctionnalité'
'Something went wrong': "Quelque chose s'est mal passé"
'Select apps': 'Selection des apps'
'Sort': 'Ordonner'
'Customize': 'Personnaliser'
'Custom': 'Personnalisé'
'Close': 'Fermer'

View file

@ -1,111 +0,0 @@
'Custom menu': 'Menú personalizado'
'Enable the custom menu': 'Activar o menú personalizado'
'No': 'Non'
'Yes': 'Si'
'Menu': 'Menú'
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Use o atallo Ctrl+o para abrir e agochar o menú lateral. Use a tecla Tab para navegar.'
'Top menu': 'Top menu'
'Apps that not must be moved in the side menu': 'As aplicacións que non deben moverse no menú lateral'
'If there is no selection then the global configuration is applied.': 'Se non hai selección, aplícase a configuración global.'
'Experimental': 'Experimental'
'Save': 'Gardar'
'You like this app and you want to support me?': 'Gústalle esta aplicación e quere axudarme?'
'Buy me a coffee ☕': 'Convídeme a un café ☕'
'Hidden': 'Agochado'
'Small': 'Pequeno'
'Normal': 'Normal'
'Big': 'Grande'
'Hidden icon': 'Icona agochada'
'Small icon': 'Icona pequena'
'Normal icon': 'Icona normal'
'Big icon': 'Icona grande'
'Hidden text': 'Texto agochado'
'Small text': 'Texto pequeno'
'Normal text': 'Texto normal'
'Big text': 'Texto grande'
'Colors': 'Cores'
'Background color': 'Cor do fondo'
'Background color of current app': 'Cor do fondo da aplicación actual'
'Text color': 'Cor do texto'
'Loader': 'Cargador'
'Icon': 'Icona'
'Same color': 'A mesma cor'
'Opposite color': 'A cor oposta'
'Transparent': 'Transparente'
'Opaque': 'Opaco'
'Opener': 'Abrir'
'Default': 'Predeterminado'
'Default (dark)': 'Predeterminado (escuro)'
'Hamburger': 'Hamburguesa'
'Hamburger (dark)': 'Hamburguesa (escuro)'
'Hamburger 2': 'Hamburguesa 2'
'Hamburger 2 (dark)': 'Hamburguesa 2 (escuro)'
'Before the logo': 'Antes do logotipo'
'After the logo': 'Após o logotipo'
'Position': 'Posición'
'Show only the opener (hidden logo)': 'Amosar só a icona de abrir (agochar o logotipo)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Non amosar o menú lateral e a icona de abrir se non hai ningunha aplicación (por exemplo: páxinas públicas).'
'Panel': 'Panel'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Abre o menú cando o rato está sobre a icona de abrir (desactivado automaticamente nas pantallas táctiles)'
'Display the big menu': 'Amosar o menú en grande'
'Display the logo': 'Amosar o logotipo'
'Icons and texts': 'Iconas e textos'
'Loader enabled': 'Cargador activado'
'Tips': 'Consellos'
'Always displayed': 'Amosado sempre'
'This is the automatic behavior when the menu is always displayed.': 'Este é o comportamento automático cando se amosa sempre o menú.'
'Not compatible with touch screens.': 'Non é compatíbel coas pantallas táctiles.'
'Big menu': 'Menú grande'
'Live preview': 'Vista previa en directo'
'Open apps in new tab': 'Abrir as aplicacións nunha nova lapela'
'Use the global setting': 'Usar o axuste global'
'Use my selection': 'Usar a miña selección'
'Show and hide the list of applications': 'Amosar e agochar a lista de aplicacións'
'Use the avatar instead of the logo': 'Usar o avatar no canto do logotipo'
'You do not have permission to change the settings.': 'Non ten permiso para cambiar os axustes.'
'Force this configuration to users': 'Forzar esta configuración para os usuarios'
'Export the configuration': 'Exportar a configuración'
'Purge the cache': 'Limpar a caché'
'Show the link to settings': 'Amosar a ligazón aos axustes'
'The menu is enabled by default for users': 'De xeito predeterminado o menú está activado para os usuarios'
'Except when the configuration is forced.': 'Agás cando a configuración é forzada.'
'Apps that should not be displayed in the menu': 'Aplicacións que non deben amosarse no menú'
'This feature is only compatible with the <code>big menu</code> display.': 'Esta función só é compatíbel coa presentación do <code>menú grande</code>.'
'The logo is a link to the default app': 'O logotipo é unha ligazón á aplicación predeterminada'
'Others': 'Outros'
'Categories': 'Categorías'
'Customize sorting': 'Personalizar a ordenación'
'Order by': 'Ordenar por'
'Name': 'Nome'
'Customed': 'Personalizado'
'Show and hide the list of categories': 'Amosar e agochar a lista de categorías'
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Estes parámetros úsanse cando o tema escuro ou o tema escuro de Breeze están activados.'
'Dark mode colors': 'Cores do modo escuro'
'With categories': 'Con categorías'
'Custom categories': 'Categorías personalizadas'
'Customize application categories': 'Personalizar as categorías das aplicacións'
'Reset to default': 'Restabelecer os valores predeterminados'
'Applications': 'Aplicacións'
'Applications kept in the top menu': 'As aplicacións mantéñense no menú superior'
'Applications kept in the top menu but also shown in side menu': 'As aplicacións mantéñense no menú superior mais tamén aparecen no menú lateral'
'These applications must be selected in the previous option.': 'Estas aplicacións deben ser seleccionadas na opción anterior.'
'Hide labels on mouse over': 'Agochar as etiquetas ao pasar o rato'
'Except the hovered app': 'Agás a aplicación que pasa o rato'
'Search': 'Buscar'
'Toggle the menu': 'Alternar o menú'
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Preguntar ao desenvolvedor'
'New request': 'Nova solicitude'
'Report a bug': 'Informar dun fallo'
'Show the configuration': 'Amosar a configuración'
'Configuration:': 'Configuración:'
'Done!': 'Feito!'
'Copy': 'Copiar'
'Need help': 'Necesito axuda'
'I would like a new feature': 'Gustaríame unha nova característica'
'Something went wrong': 'Algo foi mal'
'Select apps': 'Seleccionar as aplicacións'
'Sort': 'Ordenar'
'Customize': 'Personalizar'
'Custom': 'Personalizado'
'Close': 'Pechar'

View file

@ -1,111 +1,95 @@
'Custom menu': 'Aangepast menu' "Custom menu": "Aangepast menu"
'Enable the custom menu': 'Het aangepaste menu inschakelen' "Enable the custom menu": "Het aangepaste menu inschakelen"
'No': 'Nee' "No": "Nee"
'Yes': 'Ja' "Yes": "Ja"
'Menu': 'Menu' "Menu": "Menu"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Gebruik de snelkoppeling Ctrl+o om het zijmenu te openen en te verbergen. Gebruik tab key om te navigeren.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Bovenste menu' : 'Gebruik de snelkoppeling <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> om het zijmenu te openen en te verbergen. Gebruik <span class="keyboard-key">tab</span> om te navigeren.'
'Apps that not must be moved in the side menu': 'Apps die niet moeten worden verplaatst in het zijmenu' "Top menu": "Bovenste menu"
'If there is no selection then the global configuration is applied.': 'Als er geen keuze is, wordt de globale configuratie toegepast.' "Apps that not must be moved in the side menu": "Apps die niet moeten worden verplaatst in het zijmenu"
'Experimental': 'Experimenteel' "If there is no selection then the global configuration is applied.": "Als er geen keuze is, wordt de globale configuratie toegepast."
'Save': 'Opslaan' "Experimental": "Experimenteel"
'You like this app and you want to support me?': 'Vind je deze app leuk en wil je me steunen?' "Save": "Opslaan"
'Buy me a coffee ☕': 'Koop een koffie voor me ☕' "You like this app and you want to support me?": "Vind je deze app leuk en wil je me steunen?"
'Hidden': 'Verborgen' "Buy me a coffee ☕": "Koop een koffie voor me ☕"
'Small': 'Klein' "Hidden": "Verborgen"
'Normal': 'Normaal' "Small": "Klein"
'Big': 'Groot' "Normal": "Normaal"
'Hidden icon': 'Verborgen icoon' "Big": "Groot"
'Small icon': 'Klein icoon' "Hidden icon": "Verborgen icoon"
'Normal icon': 'Normaal icoon' "Small icon": "Klein icoon"
'Big icon': 'Groot icoon' "Normal icon": "Normaal icoon"
'Hidden text': 'Verborgen tekst' "Big icon": "Groot icoon"
'Small text': 'Kleine tekst' "Hidden text": "Verborgen tekst"
'Normal text': 'Normale tekst' "Small text": "Kleine tekst"
'Big text': 'Grote tekst' "Normal text": "Normale tekst"
'Colors': 'Kleuren' "Big text": "Grote tekst"
'Background color': 'Achtergrond kleur' "Colors": "Kleuren"
'Background color of current app': 'Achtergrondkleur van huidige app' "Background color": "Achtergrond kleur"
'Text color': 'Tekst kleur' "Background color of current app": "Achtergrondkleur van huidige app"
'Loader': 'Lader' "Text color": "Tekst kleur"
'Icon': 'Icoon' "Loader": "Lader"
'Same color': 'Zelfde kleur' "Icon": "Icoon"
'Opposite color': 'Tegenovergestelde kleur' "Same color": "Zelfde kleur"
'Transparent': 'Transparant' "Opposite color": "Tegenovergestelde kleur"
'Opaque': 'Ondoorzichtig' "Transparent": "Transparant"
'Opener': 'Opener' "Opaque": "Ondoorzichtig"
'Default': 'Standaard' "Opener": "Opener"
'Default (dark)': 'Standaard (donker)' "Default": "Standaard"
'Hamburger': 'Hamburger' "Default (dark)": "Standaard (donker)"
'Hamburger (dark)': 'Hamburger (donker)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (donker)"
'Hamburger 2 (dark)': 'Hamburger 2 (donker)' "Hamburger 2": "Hamburger 2"
'Before the logo': 'Voor het logo' "Hamburger 2 (dark)": "Hamburger 2 (donker)"
'After the logo': 'Na het logo' "Before the logo": "Voor het logo"
'Position': 'Positie' "After the logo": "Na het logo"
'Show only the opener (hidden logo)': 'Toon alleen de opener (verborgen logo)' "Position": "Positie"
'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).' "Show only the opener (hidden logo)": "Toon alleen de opener (verborgen logo)"
'Panel': 'Paneel' "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)."
'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)' "Panel": "Paneel"
'Display the big menu': 'Toon het grote menu' "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 logo': 'Toon het logo' "Display the big menu": "Toon het grote menu"
'Icons and texts': 'Iconen en teksten' "Display the logo": "Toon het logo"
'Loader enabled': 'Lader ingeschakeld' "Icons and texts": "Iconen en teksten"
'Tips': 'Tips' "Loader enabled": "Lader ingeschakeld"
'Always displayed': 'Altijd weergegeven' "Tips": "Tips"
'This is the automatic behavior when the menu is always displayed.': 'Dit is het automatische gedrag wanneer het menu altijd wordt weergegeven.' "Always displayed": "Altijd weergegeven"
'Not compatible with touch screens.': 'Niet compatibel met aanraakschermen.' "This is the automatic behavior when the menu is always displayed.": "Dit is het automatische gedrag wanneer het menu altijd wordt weergegeven."
'Big menu': 'Groot menu' "Not compatible with touch screens.": "Niet compatibel met aanraakschermen."
'Live preview': 'Live voorbeeld' "Big menu": "Groot menu"
'Open apps in new tab': 'Open apps in nieuwe tab' "Live preview": "Live voorbeeld"
'Use the global setting': 'Gebruik de globale instellingen' "Open apps in new tab": "Open apps in nieuwe tab"
'Use my selection': 'Gebruik mijn selectie' "Use the global setting": "Gebruik de globale instellingen"
'Show and hide the list of applications': 'De lijst met toepassingen tonen en verbergen' "Use my selection": "Gebruik mijn selectie"
'Use the avatar instead of the logo': 'Gebruik avatar in plaats van het logo' "Show and hide the list of applications": "De lijst met toepassingen tonen en verbergen"
'You do not have permission to change the settings.': 'Je hebt geen toestemming om de instellingen te veranderen.' "Use the avatar instead of the logo": "Gebruik avatar in plaats van het logo"
'Force this configuration to users': 'Forceer deze configuratie aan gebruikers' "You do not have permission to change the settings.": "Je hebt geen toestemming om de instellingen te veranderen."
'Export the configuration': 'Exporteer de configuratie' "Force this configuration to users": "Forceer deze configuratie aan gebruikers"
'Purge the cache': 'De cache wissen' "Export the configuration": "Exporteer de configuratie"
'Show the link to settings': 'Toon de link naar de instellingen' "Purge the cache": "De cache wissen"
'The menu is enabled by default for users': 'Het menu is standaard ingeschakeld voor gebruikers' "Show the link to settings": "Toon de link naar de instellingen"
'Except when the configuration is forced.': 'Behalve als de configuratie geforceerd is.' "The menu is enabled by default for users": "Het menu is standaard ingeschakeld voor gebruikers"
'Apps that should not be displayed in the menu': 'Apps die niet in het menu weergegeven mogen worden' "Except when the configuration is forced.": "Behalve als de configuratie geforceerd is."
'This feature is only compatible with the <code>big menu</code> display.': 'Deze functie is alleen compatibel met het <code>grote menu</code> scherm.' "Apps that should not be displayed in the menu": "Apps die niet in het menu weergegeven mogen worden"
'The logo is a link to the default app': 'Het logo is een link naar de standaard app' "This feature is only compatible with the <code>big menu</code> display.": "Deze functie is alleen compatibel met het <code>grote menu</code> scherm."
'Others': 'Overige' "The logo is a link to the default app": "Het logo is een link naar de standaard app"
'Categories': 'Categorieën' "Others": "Overige"
'Customize sorting': 'Sortering aanpassen' "Categories": "Categorieën"
'Order by': 'Sorteer op' "Customize sorting": "Sortering aanpassen"
'Name': 'Naam' "Order by": "Sorteer op"
'Customed': 'Aangepast' "Name": "Naam"
'Show and hide the list of categories': 'De lijst met categorieën tonen en verbergen' "Customed": "Aangepast"
'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.' "Show and hide the list of categories": "De lijst met categorieën tonen en verbergen"
'Dark mode colors': 'Donkere modus kleuren' "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."
'With categories': 'Met categorieën' "Dark mode colors": "Donkere modus kleuren"
'Custom categories': 'Aangepaste categorieën' "With categories": "Met categorieën"
'Customize application categories': 'Toepassingscategorieën aanpassen' "Custom categories": "Aangepaste categorieën"
'Reset to default': 'Terugzetten naar standaard' "Customize application categories": "Toepassingscategorieën aanpassen"
'Applications': 'Applicaties' "Reset to default": "Terugzetten naar standaard"
'Applications kept in the top menu': 'Applicaties bewaard in het bovenste menu' "Applications": "Applicaties"
'Applications kept in the top menu but also shown in side menu': 'Applicaties blijven in het topmenu maar worden ook in het zijmenu getoond' "Applications kept in the top menu": "Applicaties bewaard in het bovenste menu"
'These applications must be selected in the previous option.': 'Deze toepassingen moeten bij de vorige optie zijn geselecteerd.' "Applications kept in the top menu but also shown in side menu": "Applicaties blijven in het topmenu maar worden ook in het zijmenu getoond"
'Hide labels on mouse over': 'Hide labels on mouse over' "These applications must be selected in the previous option.": "Deze toepassingen moeten bij de vorige optie zijn geselecteerd."
'Except the hovered app': 'Except the hovered app' "Hide labels on mouse over": "Hide labels on mouse over"
'Search': 'Search' "Except the hovered app": "Except the hovered app"
'Toggle the menu': 'Toggle the menu' "Search": "Search"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Ask the developer'
'New request': 'New request'
'Report a bug': 'Report a bug'
'Show the configuration': 'Show the configuration'
'Configuration:': 'Configuration:'
'Done!': 'Done!'
'Copy': 'Copy'
'Need help': 'Need help'
'I would like a new feature': 'I would like a new feature'
'Something went wrong': 'Something went wrong'
'Select apps': 'Select apps'
'Sort': 'Sort'
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'

View file

@ -1,109 +1,93 @@
'Custom menu': 'Menú personalizado' "Custom menu": "Menu personalizado"
'Enable the custom menu': 'Activar o menu personalizado' "Enable the custom menu": "Habilitar o menu personalizado"
'No': 'Não' "No": "Não"
'Yes': 'Sim' "Yes": "Sim"
'Menu': 'Menu' "Menu": "Menu"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Use o atalho Ctrl+o para exibir e para esconder o menu lateral. Use tab key para navegar.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Menu superior' : 'Use o atalho <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> para exibir e para esconder o menu lateral. Use <span class="keyboard-key">tab</span> para navegar.'
'Apps that not must be moved in the side menu': 'Apps que não devem ser movidos para o menu lateral' "Top menu": "Menu superior"
'If there is no selection then the global configuration is applied.': 'Se não houver seleção, a configuração global será aplicada.' "Apps that not must be moved in the side menu": "Apps que não devem ser movidos para o menu lateral"
'Experimental': 'Experimental' "If there is no selection then the global configuration is applied.": "Se não houver seleção, a configuração global será aplicada."
'Save': 'Salvar' "Experimental": "Experimental"
'You like this app and you want to support me?': 'Você gosta deste aplicativo e quer me apoiar?' "Save": "Salvar"
'Buy me a coffee ☕': 'Me pague um café ☕' "You like this app and you want to support me?": "Você gosta deste aplicativo e quer me apoiar?"
'Hidden': 'Oculto' "Buy me a coffee ☕": "Me pague um café ☕"
'Small': 'Pequeno' "Hidden": "Oculto"
'Normal': 'Normal' "Small": "Pequeno"
'Big': 'Grande' "Normal": "Normal"
'Hidden icon': 'Ícone oculto' "Big": "Grande"
'Small icon': 'Ícone pequeno' "Hidden icon": "Ícone oculto"
'Normal icon': 'Ícone normal' "Small icon": "Ícone pequeno"
'Big icon': 'Ícone grance' "Normal icon": "Ícone normal"
'Hidden text': 'Texto oculto' "Big icon": "Ícone grance"
'Small text': 'Texto pequeno' "Hidden text": "Texto oculto"
'Normal text': 'Texto normal' "Small text": "Texto pequeno"
'Big text': 'Texto grande' "Normal text": "Texto normal"
'Colors': 'Cores' "Big text": "Texto grande"
'Background color': 'Cor de fundo' "Colors": "Cores"
'Background color of current app': 'Cor de fundo do app atual' "Background color": "Cor de fundo"
'Text color': 'Cor do texto' "Background color of current app": "Cor de fundo do app atual"
'Loader': 'Progresso' "Text color": "Cor do texto"
'Icon': 'Ícone' "Loader": "Progresso"
'Same color': 'Mesma cor' "Icon": "Ícone"
'Opposite color': 'Cor oposta' "Same color": "Mesma cor"
'Transparent': 'Transparente' "Opposite color": "Cor oposta"
'Opaque': 'Opaco' "Transparent": "Transparente"
'Opener': 'Abrir' "Opaque": "Opaco"
'Default': 'Padrão' "Opener": "Abrir"
'Default (dark)': 'Padrão (escuro)' "Default": "Padrão"
'Hamburger': 'Hamburger' "Default (dark)": "Padrão (escuro)"
'Hamburger (dark)': 'Hamburger (escuro)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (escuro)"
'Hamburger 2 (dark)': 'Hamburger 2 (escuro)' "Hamburger 2": "Hamburger 2"
'Before the logo': 'Antes da logo' "Hamburger 2 (dark)": "Hamburger 2 (escuro)"
'After the logo': 'Depois da logo' "Before the logo": "Antes da logo"
'Position': 'Posição' "After the logo": "Depois da logo"
'Show only the opener (hidden logo)': 'Mostrar apenas o Abrir (ocultar logo)' "Position": "Posição"
'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).' "Show only the opener (hidden logo)": "Mostrar apenas o Abrir (ocultar logo)"
'Panel': 'Painel' "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)."
'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)' "Panel": "Painel"
'Display the big menu': 'Mostrar o menu grande' "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 logo': 'Mostrar a logo' "Display the big menu": "Mostrar o menu grande"
'Icons and texts': 'Ícones e textos' "Display the logo": "Mostrar a logo"
'Loader enabled': 'Progresso ativado' "Icons and texts": "Ícones e textos"
'Tips': 'Dicas' "Loader enabled": "Progresso ativado"
'Always displayed': 'Sempre visível' "Tips": "Dicas"
'This is the automatic behavior when the menu is always displayed.': 'Este é o comportamento automático quando o menu está sempre visível.' "Always displayed": "Sempre visível"
'Not compatible with touch screens.': 'Não compatível com telas de toque.' "This is the automatic behavior when the menu is always displayed.": "Este é o comportamento automático quando o menu está sempre visível."
'Big menu': 'Menu grande' "Not compatible with touch screens.": "Não compatível com telas de toque."
'Live preview': 'Visualização ao vivo' "Big menu": "Menu grande"
'Open apps in new tab': 'Abrir apps em nova aba' "Live preview": "Visualização ao vivo"
'Use the global setting': 'Usar configurações globais' "Open apps in new tab": "Abrir apps em nova aba"
'Use my selection': 'Usar minha seleção' "Use the global setting": "Usar configurações globais"
'Show and hide the list of applications': 'Mostrar e ocultar a lista de aplicativos' "Use my selection": "Usar minha seleção"
'Use the avatar instead of the logo': 'Use o avatar ao invés da logo' "Show and hide the list of applications": "Mostrar e ocultar a lista de aplicativos"
'You do not have permission to change the settings.': 'Você não tem permissão para alterar as configurações.' "Use the avatar instead of the logo": "Use o avatar ao invés da logo"
'Force this configuration to users': 'Forçar esta configuração para os usuários' "You do not have permission to change the settings.": "Você não tem permissão para alterar as configurações."
'Export the configuration': 'Exportar a configuração' "Force this configuration to users": "Forçar esta configuração para os usuários"
'Purge the cache': 'Limpar o cache' "Export the configuration": "Exportar a configuração"
'Show the link to settings': 'Mostrar o link para configurações' "Purge the cache": "Limpar o cache"
'The menu is enabled by default for users': 'O menu é habilitado por padrão para os usuários' "Show the link to settings": "Mostrar o link para configurações"
'Except when the configuration is forced.': 'Exceto quando a configuração é forçada.' "The menu is enabled by default for users": "O menu é habilitado por padrão para os usuários"
'Apps that should not be displayed in the menu': 'Apps que não devem ser mostrados no menu' "Except when the configuration is forced.": "Exceto quando a configuração é forçada."
'This feature is only compatible with the <code>big menu</code> display.': 'Este recurso só é compatível com a exibição do <code>menu grande</code>.' "Apps that should not be displayed in the menu": "Apps que não devem ser mostrados no menu"
'The logo is a link to the default app': 'A logo é um link para o app padrão' "This feature is only compatible with the <code>big menu</code> display.": "Este recurso só é compatível com a exibição do <code>menu grande</code>."
'Others': 'Outros' "The logo is a link to the default app": "A logo é um link para o app padrão"
'Categories': 'Categorias' "Others": "Outros"
'Customize sorting': 'Personalizar classificação' "Categories": "Categorias"
'Order by': 'Ordenar por' "Customize sorting": "Personalizar classificação"
'Name': 'Nome' "Order by": "Ordenar por"
'Customed': 'Personalizado' "Name": "Nome"
'Show and hide the list of categories': 'Mostrar e esconder a lista de categorias' "Customed": "Personalizado"
'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.' "Show and hide the list of categories": "Mostrar e esconder a lista de categorias"
'Dark mode colors': 'Cores do modo escuro' "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."
'With categories': 'Com categorias' "Dark mode colors": "Cores do modo escuro"
'Custom categories': 'Categorias personalizadas' "With categories": "Com categorias"
'Customize application categories': 'Personalizar categorias de apps' "Custom categories": "Categorias personalizadas"
'Reset to default': 'Restaurar padrão' "Customize application categories": "Personalizar categorias de apps"
'Applications': 'Aplicativos' "Reset to default": "Restaurar padrão"
'Applications kept in the top menu': 'Aplicativos mantidos no menu superior' "Applications": "Aplicativos"
'Applications kept in the top menu but also shown in side menu': 'Aplicativos mantidos no menu superior, mas também mostrados no menu lateral' "Applications kept in the top menu": "Aplicativos mantidos no menu superior"
'These applications must be selected in the previous option.': 'Estes aplicativos devem ser selecionados na opção anterior.' "Applications kept in the top menu but also shown in side menu": "Aplicativos mantidos no menu superior, mas também mostrados no menu lateral"
'Hide labels on mouse over': 'Ocultar descrição ao passar o mouse' "These applications must be selected in the previous option.": "Estes aplicativos devem ser selecionados na opção anterior."
'Toggle the menu': 'Toggle the menu' "Hide labels on mouse over": "Ocultar descrição ao passar o mouse"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Ask the developer'
'New request': 'New request'
'Report a bug': 'Report a bug'
'Show the configuration': 'Show the configuration'
'Configuration:': 'Configuration:'
'Done!': 'Done!'
'Copy': 'Copy'
'Need help': 'Need help'
'I would like a new feature': 'I would like a new feature'
'Something went wrong': 'Something went wrong'
'Select apps': 'Select apps'
'Sort': 'Sort'
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'

View file

@ -1,111 +1,95 @@
'Custom menu': 'Пользовательское меню' "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 key to navigate.': 'Используйте сочетание клавиш Ctrl+o, чтобы открыть или скрыть боковое меню. Используйте tab key для навигации.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Верхнее меню' : 'Используйте сочетание клавиш <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span>, чтобы открыть или скрыть боковое меню. Используйте <span class="keyboard-key">Tab</span> для навигации.'
'Apps that not must be moved in the side menu': 'Приложения не перемещаемые в боковое меню' "Top menu": "Верхнее меню"
'If there is no selection then the global configuration is applied.': 'Если тут ничего не отмечено, применяются глобальные настройки.' "Apps that not must be moved in the side menu": "Приложения не перемещаемые в боковое меню"
'Experimental': 'Экспериментальный' "If there is no selection then the global configuration is applied.": "Если тут ничего не отмечено, применяются глобальные настройки."
'Save': 'Сохранить' "Experimental": "Экспериментальный"
'You like this app and you want to support me?': 'Вам нравится приложение или вы хотите поддержать меня?' "Save": "Сохранить"
'Buy me a coffee ☕': 'Купить мне чашку кофе ☕' "You like this app and you want to support me?": "Вам нравится приложение или вы хотите поддержать меня?"
'Hidden': 'Скрыто' "Buy me a coffee ☕": "Купить мне чашку кофе ☕"
'Small': 'Маленький' "Hidden": "Скрыто"
'Normal': 'Средний' "Small": "Маленький"
'Big': 'Большой' "Normal": "Средний"
'Hidden icon': 'Без иконки' "Big": "Большой"
'Small icon': 'Маленькая иконка' "Hidden icon": "Без иконки"
'Normal icon': 'Средняя иконка' "Small icon": "Маленькая иконка"
'Big icon': 'Большая иконка' "Normal icon": "Средняя иконка"
'Hidden text': 'Без текста' "Big icon": "Большая иконка"
'Small text': 'Маленький текст' "Hidden text": "Без текста"
'Normal text': 'Средний текст' "Small text": "Маленький текст"
'Big text': 'Большой текст' "Normal text": "Средний текст"
'Colors': 'Цвета' "Big text": "Большой текст"
'Background color': 'Цвет фона' "Colors": "Цвета"
'Background color of current app': 'Цвет фона выбранного приложения' "Background color": "Цвет фона"
'Text color': 'Цвет текста' "Background color of current app": "Цвет фона выбранного приложения"
'Loader': 'Загрузчик' "Text color": "Цвет текста"
'Icon': 'Иконка' "Loader": "Загрузчик"
'Same color': 'Такой же цвет' "Icon": "Иконка"
'Opposite color': 'Противоположный цвет' "Same color": "Такой же цвет"
'Transparent': 'Прозрачный' "Opposite color": "Противоположный цвет"
'Opaque': 'Непрозрачный' "Transparent": "Прозрачный"
'Opener': 'Открывалка' "Opaque": "Непрозрачный"
'Default': 'По умолчанию' "Opener": "Открывалка"
'Default (dark)': 'По умолчанию (тёмный)' "Default": "По умолчанию"
'Hamburger': 'Гамбургер' "Default (dark)": "По умолчанию (тёмный)"
'Hamburger (dark)': 'Гамбургер (тёмный)' "Hamburger": "Гамбургер"
'Hamburger 2': 'Гамбургер 2' "Hamburger (dark)": "Гамбургер (тёмный)"
'Hamburger 2 (dark)': 'Гамбургер 2 (тёмный)' "Hamburger 2": "Гамбургер 2"
'Before the logo': 'Перед логотипом' "Hamburger 2 (dark)": "Гамбургер 2 (тёмный)"
'After the logo': 'После логотипа' "Before the logo": "Перед логотипом"
'Position': 'Положение' "After the logo": "После логотипа"
'Show only the opener (hidden logo)': 'Показать только открывающую кнопку (скрытый логотип)' "Position": "Положение"
'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Не отображать боковое меню и открывалку, если нет доступного приложения (н.п. публичные страницы).' "Show only the opener (hidden logo)": "Показать только открывающую часть (скрытый логотип)"
'Panel': 'Панель' "Do not display the side menu and the opener if there is no application (eg: public pages).": "Не отображать боковое меню и открывалку, если нет приложения (например, публичные страницы)."
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Открывать меню при наведении мыши на открывалку (автоматически отключается на сенсорных экранах)' "Panel": "Панель"
'Display the big menu': 'Отобразить большое меню' "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Открывать меню при наведении мыши на экран (автоматически отключается на сенсорных экранах)"
'Display the logo': 'Показать логотип' "Display the big menu": "Отобразить большое меню"
'Icons and texts': 'Иконки и текст' "Display the logo": "Показать логотип"
'Loader enabled': 'Загрузчик включен' "Icons and texts": "Иконки и текст"
'Tips': 'Советы' "Loader enabled": "Загрузчик включен"
'Always displayed': 'Всегда отображается' "Tips": "Советы"
'This is the automatic behavior when the menu is always displayed.': 'Это автоматическое поведение, когда меню отображается всегда.' "Always displayed": "Всегда отображается"
'Not compatible with touch screens.': 'Не совместимо с сенсорными экранами.' "This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed."
'Big menu': 'Большое меню' "Not compatible with touch screens.": "Не совместимо с сенсорными экранами."
'Live preview': 'Предпросмотр в реальном времени' "Big menu": "Большое меню"
'Open apps in new tab': 'Открывать приложения в новой вкладке' "Live preview": "Live preview"
'Use the global setting': 'Использовать глобальные настройки' "Open apps in new tab": "Открывать приложения в новой вкладке"
'Use my selection': 'Использовать мои настройки' "Use the global setting": "Использовать глобальные настройки"
'Show and hide the list of applications': 'Показать или скрыть список приложений' "Use my selection": "Использовать мои настройки"
'Use the avatar instead of the logo': 'Использовать аватар вместо логотипа' "Show and hide the list of applications": "Показать или скрыть список приложений"
'You do not have permission to change the settings.': 'У вас нет разрешения изменять настройки.' "Use the avatar instead of the logo": "Использовать аватар вместо логотипа"
'Force this configuration to users': 'Для обеспечения соблюдения этих настроек пользователями' "You do not have permission to change the settings.": "У вас нет разрешения изменять настройки."
'Export the configuration': 'Экспортировать конфигурацию' "Force this configuration to users": "Force this configuration to users"
'Purge the cache': 'Очистить кэш' "Export the configuration": "Экспортировать конфигурацию"
'Show the link to settings': 'Показать ссылку на настройки' "Purge the cache": "Очистить кэш"
'The menu is enabled by default for users': 'Это меню включено по умолчанию для пользователей' "Show the link to settings": "Показать ссылку на настройки"
'Except when the configuration is forced.': 'За исключением случаев, когда настройка принудительная.' "The menu is enabled by default for users": "Это меню включено по умолчанию для пользователей"
'Apps that should not be displayed in the menu': 'Ппрограммы, скрытые из меню' "Except when the configuration is forced.": "Except when the configuration is forced."
'This feature is only compatible with the <code>big menu</code> display.': 'Эта возможность совместима только с отображением <code>большого меню</code>.' "Apps that should not be displayed in the menu": "Ппрограммы, скрытые из меню"
'The logo is a link to the default app': 'Логотип открывает приложение по умолчанию' "This feature is only compatible with the <code>big menu</code> display.": "This feature is only compatible with the <code>big menu</code> display."
'Others': 'Прочие' "The logo is a link to the default app": "Логотип открывает приложение по умолчанию"
'Categories': 'Категории' "Others": "Прочие"
'Customize sorting': 'Настроить сортировку' "Categories": "Категории"
'Order by': 'В порядке' "Customize sorting": "Настроить сортировку"
'Name': 'Название' "Order by": "В порядке"
'Customed': 'Пользовательское' "Name": "Название"
'Show and hide the list of categories': 'Показать или скрыть список категорий' "Customed": "Customed"
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Эти настройки используются темами Тёмная и Тёмная Breeze.' "Show and hide the list of categories": "Показать или скрыть список категорий"
'Dark mode colors': 'Цвета тёмной темы' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Эти настройки используются темами Тёмная и Тёмная Breeze."
'With categories': 'С категориями' "Dark mode colors": "Цвета тёмной темы"
'Custom categories': 'Пользовательские категории' "With categories": "С категориями"
'Customize application categories': 'Изменить категории приложений' "Custom categories": "Пользовательские категории"
'Reset to default': 'Сбросить к значениям по умолчанию' "Customize application categories": "Изменить категории приложений"
'Applications': 'Приложения' "Reset to default": "Сбросить к значениям по умолчанию"
'Applications kept in the top menu': 'Приложения, хранящиеся в верхнем меню' "Applications": "Приложения"
'Applications kept in the top menu but also shown in side menu': 'Приложения хранящиеся в верхнем меню, но также отображающиеся в боковом меню' "Applications kept in the top menu": "Applications kept in the top menu"
'These applications must be selected in the previous option.': 'Эти приложения необходимо выбрать в предыдущем выборе.' "Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu"
'Hide labels on mouse over': 'Скрыть название при наведении мыши' "These applications must be selected in the previous option.": "These applications must be selected in the previous option."
'Except the hovered app': 'Кроме приложения на котором сейчас' "Hide labels on mouse over": "Скрыть название при наведении мыши"
'Search': 'Поиск' "Except the hovered app": "Except the hovered app"
'Toggle the menu': 'Переключить меню' "Search": "Search"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Спросить разработчика'
'New request': 'Новый запрос'
'Report a bug': 'Пожаловаться на ошибку'
'Show the configuration': 'Показать конфигурацию'
'Configuration:': 'Configuration:'
'Done!': 'Готово!'
'Copy': 'Копировать'
'Need help': 'Нужна помощь'
'I would like a new feature': 'Я хочу новую возможность'
'Something went wrong': 'Что-то пошло не так'
'Select apps': 'Выберете приложения'
'Sort': 'Сортировать'
'Customize': 'Приспособить'
'Custom': 'Custom'
'Close': 'Закрыть'

View file

@ -1,109 +1,93 @@
'Custom menu': 'Custom menu' "Custom menu": "Custom menu"
'Enable the custom menu': 'Enable the custom menu' "Enable the custom menu": "Enable the custom menu"
'No': 'No' "No": "No"
'Yes': 'Yes' "Yes": "Yes"
'Menu': 'Menu' "Menu": "Menu"
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Top menu': 'Top menu' : 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to navigate.'
'Apps that not must be moved in the side menu': 'Apps that not must be moved in the side menu' "Top menu": "Top menu"
'If there is no selection then the global configuration is applied.': 'If there is no selection then the global configuration is applied.' "Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu"
'Experimental': 'Experimental' "If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied."
'Save': 'Save' "Experimental": "Experimental"
'You like this app and you want to support me?': 'You like this app and you want to support me?' "Save": "Save"
'Buy me a coffee ☕': 'Buy me a coffee ☕' "You like this app and you want to support me?": "You like this app and you want to support me?"
'Hidden': 'Hidden' "Buy me a coffee ☕": "Buy me a coffee ☕"
'Small': 'Small' "Hidden": "Hidden"
'Normal': 'Normal' "Small": "Small"
'Big': 'Big' "Normal": "Normal"
'Hidden icon': 'Hidden icon' "Big": "Big"
'Small icon': 'Small icon' "Hidden icon": "Hidden icon"
'Normal icon': 'Normal icon' "Small icon": "Small icon"
'Big icon': 'Big icon' "Normal icon": "Normal icon"
'Hidden text': 'Hidden text' "Big icon": "Big icon"
'Small text': 'Small text' "Hidden text": "Hidden text"
'Normal text': 'Normal text' "Small text": "Small text"
'Big text': 'Big text' "Normal text": "Normal text"
'Colors': 'Colors' "Big text": "Big text"
'Background color': 'Background color' "Colors": "Colors"
'Background color of current app': 'Background color of current app' "Background color": "Background color"
'Text color': 'Text color' "Background color of current app": "Background color of current app"
'Loader': 'Loader' "Text color": "Text color"
'Icon': 'Icon' "Loader": "Loader"
'Same color': 'Same color' "Icon": "Icon"
'Opposite color': 'Opposite color' "Same color": "Same color"
'Transparent': 'Transparent' "Opposite color": "Opposite color"
'Opaque': 'Opaque' "Transparent": "Transparent"
'Opener': 'Opener' "Opaque": "Opaque"
'Default': 'Default' "Opener": "Opener"
'Default (dark)': 'Default (dark)' "Default": "Default"
'Hamburger': 'Hamburger' "Default (dark)": "Default (dark)"
'Hamburger (dark)': 'Hamburger (dark)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (dark)"
'Hamburger 2 (dark)': 'Hamburger 2 (dark)' "Hamburger 2": "Hamburger 2"
'Before the logo': 'Before the logo' "Hamburger 2 (dark)": "Hamburger 2 (dark)"
'After the logo': 'After the logo' "Before the logo": "Before the logo"
'Position': 'Position' "After the logo": "After the logo"
'Show only the opener (hidden logo)': 'Show only the opener (hidden logo)' "Position": "Position"
'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).' "Show only the opener (hidden logo)": "Show only the opener (hidden logo)"
'Panel': 'Panel' "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)."
'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)' "Panel": "Panel"
'Display the big menu': 'Display the big menu' "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 logo': 'Display the logo' "Display the big menu": "Display the big menu"
'Icons and texts': 'Icons and texts' "Display the logo": "Display the logo"
'Loader enabled': 'Loader enabled' "Icons and texts": "Icons and texts"
'Tips': 'Tips' "Loader enabled": "Loader enabled"
'Always displayed': 'Always displayed' "Tips": "Tips"
'This is the automatic behavior when the menu is always displayed.': 'This is the automatic behavior when the menu is always displayed.' "Always displayed": "Always displayed"
'Not compatible with touch screens.': 'Not compatible with touch screens.' "This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed."
'Big menu': 'Big menu' "Not compatible with touch screens.": "Not compatible with touch screens."
'Live preview': 'Live preview' "Big menu": "Big menu"
'Open apps in new tab': 'Open apps in new tab' "Live preview": "Live preview"
'Use the global setting': 'Use the global setting' "Open apps in new tab": "Open apps in new tab"
'Use my selection': 'Use my selection' "Use the global setting": "Use the global setting"
'Show and hide the list of applications': 'Show and hide the list of applications' "Use my selection": "Use my selection"
'Use the avatar instead of the logo': 'Use the avatar instead of the logo' "Show and hide the list of applications": "Show and hide the list of applications"
'You do not have permission to change the settings.': 'You do not have permission to change the settings.' "Use the avatar instead of the logo": "Use the avatar instead of the logo"
'Force this configuration to users': 'Force this configuration to users' "You do not have permission to change the settings.": "You do not have permission to change the settings."
'Export the configuration': 'Export the configuration' "Force this configuration to users": "Force this configuration to users"
'Purge the cache': 'Purge the cache' "Export the configuration": "Export the configuration"
'Show the link to settings': 'Show the link to settings' "Purge the cache": "Purge the cache"
'The menu is enabled by default for users': 'The menu is enabled by default for users' "Show the link to settings": "Show the link to settings"
'Except when the configuration is forced.': 'Except when the configuration is forced.' "The menu is enabled by default for users": "The menu is enabled by default for users"
'Apps that should not be displayed in the menu': 'Apps that should not be displayed in the menu' "Except when the configuration is forced.": "Except when the configuration is forced."
'This feature is only compatible with the <code>big menu</code> display.': 'This feature is only compatible with the <code>big menu</code> display.' "Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu"
'The logo is a link to the default app': 'The logo is a link to the default app' "This feature is only compatible with the <code>big menu</code> display.": "This feature is only compatible with the <code>big menu</code> display."
'Others': 'Others' "The logo is a link to the default app": "The logo is a link to the default app"
'Categories': 'Categories' "Others": "Others"
'Customize sorting': 'Customize sorting' "Categories": "Categories"
'Order by': 'Order by' "Customize sorting": "Customize sorting"
'Name': 'Name' "Order by": "Order by"
'Customed': 'Customed' "Name": "Name"
'Show and hide the list of categories': 'Show and hide the list of categories' "Customed": "Customed"
'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.' "Show and hide the list of categories": "Show and hide the list of categories"
'Dark mode colors': 'Dark mode colors' "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."
'With categories': 'With categories' "Dark mode colors": "Dark mode colors"
'Custom categories': 'Custom categories' "With categories": "With categories"
'Customize application categories': 'Customize application categories' "Custom categories": "Custom categories"
'Reset to default': 'Reset to default' "Customize application categories": "Customize application categories"
'Applications': 'Applications' "Reset to default": "Reset to default"
'Applications kept in the top menu': 'Applications kept in the top menu' "Applications": "Applications"
'Applications kept in the top menu but also shown in side menu': 'Applications kept in the top menu but also shown in side menu' "Applications kept in the top menu": "Applications kept in the top menu"
'These applications must be selected in the previous option.': 'These applications must be selected in the previous option.' "Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu"
'Hide labels on mouse over': 'Hide labels on mouse over' "These applications must be selected in the previous option.": "These applications must be selected in the previous option."
'Toggle the menu': 'Prepnite ponuku' "Hide labels on mouse over": "Hide labels on mouse over"
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Ask the developer'
'New request': 'New request'
'Report a bug': 'Report a bug'
'Show the configuration': 'Show the configuration'
'Configuration:': 'Configuration:'
'Done!': 'Done!'
'Copy': 'Copy'
'Need help': 'Need help'
'I would like a new feature': 'I would like a new feature'
'Something went wrong': 'Something went wrong'
'Select apps': 'Select apps'
'Sort': 'Sort'
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'

View file

@ -1,111 +0,0 @@
'Custom menu': 'Anpassad meny'
'Enable the custom menu': 'Aktivera den anpassade menyn'
'No': 'Nej'
'Yes': 'Ja'
'Menu': 'Meny'
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Använd genvägen Ctrl+o för att visa eller dölja sidomenyn. Använd tabb-tangenten för att navigera.'
'Top menu': 'Toppmeny'
'Apps that not must be moved in the side menu': 'Appar som inte får flyttas i sidomenyn'
'If there is no selection then the global configuration is applied.': 'Om inget val har gjorts tillämpas den globala konfigurationen.'
'Experimental': 'Experimentell'
'Save': 'Spara'
'You like this app and you want to support me?': 'Gillar du den här appen och vill stödja mig?'
'Buy me a coffee ☕': 'Bjud mig på en kaffe ☕'
'Hidden': 'Dold'
'Small': 'Liten'
'Normal': 'Normal'
'Big': 'Stor'
'Hidden icon': 'Dold ikon'
'Small icon': 'Liten ikon'
'Normal icon': 'Normal ikon'
'Big icon': 'Stor ikon'
'Hidden text': 'Dold text'
'Small text': 'Liten text'
'Normal text': 'Normal text'
'Big text': 'Stor text'
'Colors': 'Färger'
'Background color': 'Bakgrundsfärg'
'Background color of current app': 'Bakgrundsfärg för aktuell app'
'Text color': 'Textfärg'
'Loader': 'Laddare'
'Icon': 'Ikon'
'Same color': 'Samma färg'
'Opposite color': 'Motsatt färg'
'Transparent': 'Transparent'
'Opaque': 'Ogenomskinlig'
'Opener': 'Öppnare'
'Default': 'Standard'
'Default (dark)': 'Standard(mörk)'
'Hamburger': 'Hamburgermeny'
'Hamburger (dark)': 'Hamburgermeny(mörk)'
'Hamburger 2': 'Hamburgermeny 2'
'Hamburger 2 (dark)': 'Hamburgermeny 2 (mörk)'
'Before the logo': 'Före logotypen'
'After the logo': 'Efter logotypen'
'Position': 'Position'
'Show only the opener (hidden logo)': 'Visa endast öppnaren (gömd logotyp)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Visa inte sidomenyn eller öppnaren om det inte finns någon applikation (t.ex. publika sidor).'
'Panel': 'Panel'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Öppna menyn när muspekaren hovrar över öppnaren (automatiskt avaktiverat på pekskärmar)'
'Display the big menu': 'Visa den stora menyn'
'Display the logo': 'Visa logotypen'
'Icons and texts': 'Ikoner och texter'
'Loader enabled': 'Laddare aktiverad'
'Tips': 'Tips'
'Always displayed': 'Alltid visad'
'This is the automatic behavior when the menu is always displayed.': 'Detta är det automatiska beteendet när menyn alltid är visad.'
'Not compatible with touch screens.': 'Inte kompatibel med pekskärmar.'
'Big menu': 'Stor meny'
'Live preview': 'Förhandsgranskning i realtid'
'Open apps in new tab': 'Öppna appar i ny flik'
'Use the global setting': 'Använd den globala inställningen'
'Use my selection': 'Använd mitt val'
'Show and hide the list of applications': 'Visa och dölj listan över applikationer'
'Use the avatar instead of the logo': 'Använd avataren istället för logotypen'
'You do not have permission to change the settings.': 'Du har inte behörighet att ändra inställningarna.'
'Force this configuration to users': 'Tvinga denna konfiguration för användare'
'Export the configuration': 'Exportera konfigurationen'
'Purge the cache': 'Rensa cachen'
'Show the link to settings': 'Visa länken till inställningarna'
'The menu is enabled by default for users': 'Menyn är aktiverad som standard för användare'
'Except when the configuration is forced.': 'Förutom när konfigurationen är tvingad.'
'Apps that should not be displayed in the menu': 'Appar som inte ska visas i menyn'
'This feature is only compatible with the <code>big menu</code> display.': 'Denna funktion är endast kompatibel med <code>stor meny</code>.'
'The logo is a link to the default app': 'Logotypen är en länk till standardappen'
'Others': 'Övriga'
'Categories': 'Kategorier'
'Customize sorting': 'Anpassa sortering'
'Order by': 'Sortera efter'
'Name': 'Namn'
'Customed': 'Anpassad'
'Show and hide the list of categories': 'Visa och dölj listan över kategorier'
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Dessa parametrar används när mörkt tema eller Breeze Dark Theme är aktiverade.'
'Dark mode colors': 'Färger för mörkt läge'
'With categories': 'Med kategorier'
'Custom categories': 'Anpassade kategorier'
'Customize application categories': 'Anpassa app kategorier'
'Reset to default': 'Återställ till standardvärden'
'Applications': 'Applikationer'
'Applications kept in the top menu': 'Applikationer som hålls i toppmenyn'
'Applications kept in the top menu but also shown in side menu': 'Applikationer som visas både i toppmenyn och i sidomenyn'
'These applications must be selected in the previous option.': 'Dessa applikationer måste väljas i föregående alternativ.'
'Hide labels on mouse over': 'Dölj etiketter när muspekaren är över'
'Except the hovered app': 'Förutom den app som muspekaren är över'
'Search': 'Sök'
'Toggle the menu': 'Växla menyn'
'Open the documentation': 'Öppna dokumentationen'
'Ask the developer': 'Fråga utvecklaren'
'New request': 'Ny förfrågan'
'Report a bug': 'Rapportera ett fel'
'Show the configuration': 'Visa konfigurationen'
'Configuration:': 'Konfiguration:'
'Done!': 'Klar!'
'Copy': 'Kopiera'
'Need help': 'Behöver hjälp'
'I would like a new feature': 'Jag skulle vilja ha en ny funktion'
'Something went wrong': 'Något gick fel'
'Select apps': 'Välj appar'
'Sort': 'Sortera'
'Customize': 'Anpassa'
'Custom': 'Anpassad'
'Close': 'Stäng'

View file

@ -1,111 +1,97 @@
'Custom menu': 'Custom menu' "Custom menu": ""
'Enable the custom menu': 'Enable the custom menu' "Enable the custom menu": ""
'No': 'No' "No": ""
'Yes': 'Yes' "Yes": ""
'Menu': 'Menu' "Menu": ""
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.' ? 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span>
'Top menu': 'Top menu' to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to
'Apps that not must be moved in the side menu': 'Apps that not must be moved in the side menu' navigate.'
'If there is no selection then the global configuration is applied.': 'If there is no selection then the global configuration is applied.' : ''
'Experimental': 'Experimental' "Top menu": ""
'Save': 'Save' "Apps that not must be moved in the side menu": ""
'You like this app and you want to support me?': 'You like this app and you want to support me?' "If there is no selection then the global configuration is applied.": ""
'Buy me a coffee ☕': 'Buy me a coffee ☕' "Experimental": ""
'Hidden': 'Hidden' "Save": ""
'Small': 'Small' "You like this app and you want to support me?": ""
'Normal': 'Normal' "Buy me a coffee ☕": ""
'Big': 'Big' "Hidden": ""
'Hidden icon': 'Hidden icon' "Small": ""
'Small icon': 'Small icon' "Normal": ""
'Normal icon': 'Normal icon' "Big": ""
'Big icon': 'Big icon' "Hidden icon": ""
'Hidden text': 'Hidden text' "Small icon": ""
'Small text': 'Small text' "Normal icon": ""
'Normal text': 'Normal text' "Big icon": ""
'Big text': 'Big text' "Hidden text": ""
'Colors': 'Colors' "Small text": ""
'Background color': 'Background color' "Normal text": ""
'Background color of current app': 'Background color of current app' "Big text": ""
'Text color': 'Text color' "Colors": ""
'Loader': 'Loader' "Background color": ""
'Icon': 'Icon' "Background color of current app": ""
'Same color': 'Same color' "Text color": ""
'Opposite color': 'Opposite color' "Loader": ""
'Transparent': 'Transparent' "Icon": ""
'Opaque': 'Opaque' "Same color": ""
'Opener': 'Opener' "Opposite color": ""
'Default': 'Default' "Transparent": ""
'Default (dark)': 'Default (dark)' "Opaque": ""
'Hamburger': 'Hamburger' "Opener": ""
'Hamburger (dark)': 'Hamburger (dark)' "Default": ""
'Hamburger 2': 'Hamburger 2' "Default (dark)": ""
'Hamburger 2 (dark)': 'Hamburger 2 (dark)' "Hamburger": ""
'Before the logo': 'Before the logo' "Hamburger (dark)": ""
'After the logo': 'After the logo' "Hamburger 2": ""
'Position': 'Position' "Hamburger 2 (dark)": ""
'Show only the opener (hidden logo)': 'Show only the opener (hidden logo)' "Before the 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).' "After the logo": ""
'Panel': 'Panel' "Position": ""
'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)' "Show only the opener (hidden logo)": ""
'Display the big menu': 'Display the big menu' "Do not display the side menu and the opener if there is no application (eg: public pages).": ""
'Display the logo': 'Display the logo' "Panel": ""
'Icons and texts': 'Icons and texts' "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": ""
'Loader enabled': 'Loader enabled' "Display the big menu": ""
'Tips': 'Tips' "Display the logo": ""
'Always displayed': 'Always displayed' "Icons and texts": ""
'This is the automatic behavior when the menu is always displayed.': 'This is the automatic behavior when the menu is always displayed.' "Loader enabled": ""
'Not compatible with touch screens.': 'Not compatible with touch screens.' "Tips": ""
'Big menu': 'Big menu' "Always displayed": ""
'Live preview': 'Live preview' "This is the automatic behavior when the menu is always displayed.": ""
'Open apps in new tab': 'Open apps in new tab' "Not compatible with touch screens.": ""
'Use the global setting': 'Use the global setting' "Big menu": ""
'Use my selection': 'Use my selection' "Live preview": ""
'Show and hide the list of applications': 'Show and hide the list of applications' "Open apps in new tab": ""
'Use the avatar instead of the logo': 'Use the avatar instead of the logo' "Use the global setting": ""
'You do not have permission to change the settings.': 'You do not have permission to change the settings.' "Use my selection": ""
'Force this configuration to users': 'Force this configuration to users' "Show and hide the list of applications": ""
'Export the configuration': 'Export the configuration' "Use the avatar instead of the logo": ""
'Purge the cache': 'Purge the cache' "You do not have permission to change the settings.": ""
'Show the link to settings': 'Show the link to settings' "Force this configuration to users": ""
'The menu is enabled by default for users': 'The menu is enabled by default for users' "Export the configuration": ""
'Except when the configuration is forced.': 'Except when the configuration is forced.' "Purge the cache": ""
'Apps that should not be displayed in the menu': 'Apps that should not be displayed in the menu' "Show the link to settings": ""
'This feature is only compatible with the <code>big menu</code> display.': 'This feature is only compatible with the <code>big menu</code> display.' "The menu is enabled by default for users": ""
'The logo is a link to the default app': 'The logo is a link to the default app' "Except when the configuration is forced.": ""
'Others': 'Others' "Apps that should not be displayed in the menu": ""
'Categories': 'Categories' "This feature is only compatible with the <code>big menu</code> display.": ""
'Customize sorting': 'Customize sorting' "The logo is a link to the default app": ""
'Order by': 'Order by' "Others": ""
'Name': 'Name' "Categories": ""
'Customed': 'Customed' "Customize sorting": ""
'Show and hide the list of categories': 'Show and hide the list of categories' "Order by": ""
'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.' "Name": ""
'Dark mode colors': 'Dark mode colors' "Customed": ""
'With categories': 'With categories' "Show and hide the list of categories": ""
'Custom categories': 'Custom categories' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": ""
'Customize application categories': 'Customize application categories' "Dark mode colors": ""
'Reset to default': 'Reset to default' "With categories": ""
'Applications': 'Applications' "Custom categories": ""
'Applications kept in the top menu': 'Applications kept in the top menu' "Customize application categories": ""
'Applications kept in the top menu but also shown in side menu': 'Applications kept in the top menu but also shown in side menu' "Reset to default": ""
'These applications must be selected in the previous option.': 'These applications must be selected in the previous option.' "Applications": ""
'Hide labels on mouse over': 'Hide labels on mouse over' "Applications kept in the top menu": ""
'Except the hovered app': 'Except the hovered app' "Applications kept in the top menu but also shown in side menu": ""
'Search': 'Search' "These applications must be selected in the previous option.": ""
'Toggle the menu': 'Toggle the menu' "Hide labels on mouse over": ""
'Open the documentation': 'Open the documentation' "Except the hovered app": ""
'Ask the developer': 'Ask the developer' "Search": ""
'New request': 'New request'
'Report a bug': 'Report a bug'
'Show the configuration': 'Show the configuration'
'Configuration:': 'Configuration:'
'Done!': 'Done!'
'Copy': 'Copy'
'Need help': 'Need help'
'I would like a new feature': 'I would like a new feature'
'Something went wrong': 'Something went wrong'
'Select apps': 'Select apps'
'Sort': 'Sort'
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'

View file

@ -1,111 +1,95 @@
'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 <span class="keyboard-key">tab</span> to navigate.': '使用快捷键 Ctrl+o 打开或隐藏侧边栏菜单。使用 <span class="keyboard-key">Tab</span> 键来导航。' ? "Use the shortcut <span class=\"keyboard-key\">Ctrl</span>+<span class=\"keyboard-key\">o</span> to open and to hide the side menu. Use <span class=\"keyboard-key\">tab</span> to navigate."
'Top menu': '顶部菜单' : "使用快捷键 <span class=\"keyboard-key\">Ctrl</span>+<span class=\"keyboard-key\">o</span> 打开或隐藏侧边栏菜单。使用<span class=\"keyboard-key\">tab</span> 来导航。"
'Apps that not must be moved in the side menu': '禁止在侧边栏菜单移动的应用' "Top menu": "顶部菜单"
'If there is no selection then the global configuration is applied.': '如果没有选择,则应用全局配置。' "Apps that not must be moved in the side menu": "禁止在侧边栏菜单移动的应用"
'Experimental': '实验性' "If there is no selection then the global configuration is applied.": "如不选择,将应用全局设定。"
'Save': '保存' "Experimental": "实验性"
'You like this app and you want to support me?': '喜欢本应用并支持我一下?' "Save": "保存"
'Buy me a coffee ☕': '赏一杯咖啡 ☕ 给我' "You like this app and you want to support me?": "喜欢本应用并支持我一下?"
'Hidden': '隐藏' "Buy me a coffee ☕": "赏一杯咖啡 ☕ 给我"
'Small': '小型' "Hidden": "隐藏"
'Normal': '标准' "Small": "小型"
'Big': '大型' "Normal": "标准"
'Colors': '颜色' "Big": "大型"
'Background color': '背景颜色' "Colors": "颜色"
'Background color of current app': '当前应用的背景色' "Background color": "背景颜色"
'Text color': '文本颜色' "Background color of current app": "当前应用的背景色"
'Loader': '菜单指示器' "Text color": "文字颜色"
'Icon': '图标' "Loader": "菜单指示器"
'Same color': '相同颜色' "Icon": "图标"
'Opposite color': '相反颜色' "Same color": "相同颜色"
'Transparent': '透明' "Opposite color": "相反颜色"
'Opaque': '不透明' "Transparent": "透明"
'Opener': '触发器' "Opaque": "不透明"
'Default': '默认' "Opener": "容器"
'Default (dark)': '默认(深色)' "Default": "默认"
'Hamburger': 'Hamburger' "Default (dark)": "默认(深色)"
'Hamburger (dark)': 'Hamburger (深色)' "Hamburger": "Hamburger"
'Hamburger 2': 'Hamburger 2' "Hamburger (dark)": "Hamburger (深色)"
'Hamburger 2 (dark)': 'Hamburger 2 (深色)' "Hamburger 2": "Hamburger 2"
'Before the logo': '在徽标之前' "Hamburger 2 (dark)": "Hamburger 2 (深色)"
'After the logo': '在徽标之后' "Before the logo": "在logo前"
'Position': '位置' "After the logo": "在logo后"
'Show only the opener (hidden logo)': '仅显示触发器(隐藏徽标)' "Position": "位置"
'Do not display the side menu and the opener if there is no application (eg: public pages).': '如果没有应用程序(例如:公共页面),则不要显示侧边栏菜单和触发器。' "Show only the opener (hidden logo)": "只显示容器 (隐藏logo)"
'Panel': '面板' "Do not display the side menu and the opener if there is no application (eg: public pages).": "N如果没有应用不显示侧边栏菜单和容器 (例如 : 公共页面)。"
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': '当鼠标悬停在触发器上时打开菜单(在触摸屏上自动禁用)' "Panel": "面板"
'Display the big menu': '显示大型菜单' "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)"
'Display the logo': '显示徽标' "Display the big menu": "显示大型菜单"
'Icons and texts': '图标和文本' "Display the logo": "显示logo"
'Loader enabled': '菜单指示器已启用' "Icons and texts": "图标与文字"
'Tips': '技巧' "Loader enabled": "菜单指示器已激活"
'Always displayed': '始终显示' "Tips": "技巧"
'This is the automatic behavior when the menu is always displayed.': '这是菜单始终显示时的自动行为。' "Always displayed": "一直显示"
'Not compatible with touch screens.': '与触摸屏不兼容。' "This is the automatic behavior when the menu is always displayed.": "一直显示菜单时的自动动作。"
'Big menu': '大型菜单' "Not compatible with touch screens.": "与触屏不兼容。"
'Live preview': '实时预览' "Big menu": "大型菜单"
'Open apps in new tab': '在新标签页中打开应用' "Live preview": "实时预览"
'Use the global setting': '使用全局设置' "Open apps in new tab": "在新标签中打开应用"
'Use my selection': '使用自定义设置' "Use the global setting": "使用全局设定"
'Show and hide the list of applications': '显示和隐藏应用程序列表' "Use my selection": "使用自定义设定"
'Use the avatar instead of the logo': '使用头像代替徽标' "Show and hide the list of applications": "显示或隐藏应用列表"
'You do not have permission to change the settings.': '您没有更改设置的权限。' "Use the avatar instead of the logo": "使用头像代替logo"
'Force this configuration to users': '强制用户使用此配置' "You do not have permission to change the settings.": "没有更改设置的权限。"
'Export the configuration': '导出配置' "Force this configuration to users": "强制用户使用此设置"
'Purge the cache': '清除缓存' "Export the configuration": "导出设置"
'Show the link to settings': '显示设置链接' "Purge the cache": "清除缓存"
'The menu is enabled by default for users': '默认情况下为用户启用菜单' "Show the link to settings": "显示设置链接"
'Except when the configuration is forced.': '除非强制配置。' "The menu is enabled by default for users": "用户的默认菜单已激活"
'Apps that should not be displayed in the menu': '禁止在菜单中显示的应用' "Except when the configuration is forced.": "除非设置被强制使用。"
'This feature is only compatible with the <code>big menu</code> display.': '此功能只和<code>大型菜单</code>兼容。' "Apps that should not be displayed in the menu": "禁止在菜单中显示的应用"
'The logo is a link to the default app': '徽标链接到默认应用' "This feature is only compatible with the <code>big menu</code> display.": "此功能只和<code>大型菜单</code>兼容。"
'Others': '其他' "The logo is a link to the default app": "logo链接到默认应用"
'Categories': '类别' "Others": "其他"
'Customize sorting': '自定义排序' "Categories": "类别"
'Order by': '排序方式' "Customize sorting": "自定义顺序"
'Name': '名称' "Order by": "排序规则"
'Customed': '自定义' "Name": "名称"
'Show and hide the list of categories': '显示或隐藏类别列表' "Customed": "自定义"
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': '启用深色主题时使用此参数。' "Show and hide the list of categories": "显示或隐藏类别列表"
'Dark mode colors': '深色模式颜色' "This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "此参数将应用于暗黑主题激活时。"
'With categories': '按类别' "Dark mode colors": "暗黑模式颜色"
'Custom categories': '自定义类别' "With categories": "有类别"
'Customize application categories': '自定义应用程序类别' "Custom categories": "自定义类别"
'Reset to default': '重置为默认设置' "Customize application categories": "自定义应用程序类别"
'Hidden icon': '隐藏图标' "Reset to default": "重置为默认设置"
'Small icon': '小图标' "Hidden icon": "隐藏图标"
'Normal icon': '正常图标' "Small icon": "小图标"
'Big icon': '大图标' "Normal icon": "正常图标"
'Hidden text': '隐藏文本' "Big icon": "大图标"
'Small text': '小文本' "Hidden text": "隐藏文字"
'Normal text': '普通文本' "Small text": "小文本"
'Big text': '大文本' "Normal text": "普通文本"
'Applications': '应用程序' "Big text": "大文本"
'Applications kept in the top menu': '应用程序保留在顶部菜单中' "Applications": "Applications"
'Applications kept in the top menu but also shown in side menu': '应用程序保留在顶部菜单中,但也显示在侧边栏菜单中' "Applications kept in the top menu": "Applications kept in the top menu"
'These applications must be selected in the previous option.': '必须在上一个选项中选择这些应用程序。' "Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu"
'Hide labels on mouse over': '鼠标悬停时隐藏标签' "These applications must be selected in the previous option.": "These applications must be selected in the previous option."
'Except the hovered app': '除了悬停的应用' "Hide labels on mouse over": "Hide labels on mouse over"
'Search': '搜索' "Except the hovered app": "Except the hovered app"
'Toggle menu': '切换菜单' "Search": "Search"
'Open the documentation': '打开文档'
'Ask the developer': '询问开发者'
'New request': '新请求'
'Report a bug': '报告错误'
'Show the configuration': '显示配置'
'Configuration:': '配置:'
'Done!': '完成!'
'Copy': '复制'
'Need help': '需要帮助'
'I would like a new feature': '我想要一个新功能'
'Something went wrong': '出了点问题'
'Select apps': '选择应用'
'Sort': '排序'
'Customize': '自定义'
'Custom': '自定义'
'Close': '关闭'

11
src/lib/createElement.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = (tagName, attributes) => {
const element = document.createElement(tagName)
if (typeof attributes === 'object') {
for (let i in attributes) {
element.setAttribute(i, attributes[i])
}
}
return element
}

View file

@ -1,54 +0,0 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const waitContainer = async (selector) => {
return new Promise((resolve) => {
const execute = () => {
const container = document.querySelector(selector)
if (container) {
resolve(container)
} else {
setTimeout(() => {
execute(selector)
}, 50)
}
}
execute(selector)
})
}
const createElement = (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
}
export { waitContainer, createElement }

View file

@ -1,28 +0,0 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const focusActiveApp = (menu) => {
window.setTimeout(() => {
const a = menu.querySelector('.side-menu-app.active a') || menu.querySelector('.side-menu-app a')
if (a) {
a.focus()
}
}, 500)
}
export { focusActiveApp }

View file

@ -1,40 +0,0 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const containsAppsMatchingSearch = (values, search) => {
if (search.trim() === '') {
return true
}
for (let key in values) {
if (isAppMatchingSearch(values[key], search)) {
return true
}
}
return false
}
const isAppMatchingSearch = (item, search) => {
if (search.trim() === '') {
return true
}
return item.name.toLowerCase().includes(search.trim().toLowerCase())
}
export { containsAppsMatchingSearch, isAppMatchingSearch }

View file

@ -1,26 +0,0 @@
const waitPasswordConfirmation = async () => {
let tries = 0
return new Promise((resolve, reject) => {
const execute = () => {
if (!OC.PasswordConfirmation.requiresPasswordConfirmation()) {
resolve()
return
}
OC.PasswordConfirmation.requirePasswordConfirmation(() => {})
if (++tries !== 10) {
setTimeout(() => {
execute()
}, 2000)
} else {
reject()
}
}
execute()
})
}
export { waitPasswordConfirmation }

View file

@ -1,54 +0,0 @@
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import './scss/menu.scss'
import '@formatjs/intl-segmenter/polyfill.js'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createElement, waitContainer } from './lib/dom.js'
import StandardMenu from './menus/StandardMenu'
import MenuContainer from './menus/MenuContainer'
const pinia = createPinia()
const body = document.querySelector('body')
const container = createElement('div', {
id: 'side-menu-container',
})
body.appendChild(container)
const app = createApp(MenuContainer)
app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(container)
waitContainer('#header .app-menu').then((container) => {
const menu = createElement('div', {
id: 'app-menu-container',
})
container.parentNode.insertBefore(menu, container.nextSibling)
container.remove()
const app = createApp(StandardMenu)
app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(menu)
})

View file

@ -1,139 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<template v-if="display && hasApps">
<PageLoader v-if="hasPageLoader" />
<TopWideMenu
v-if="display === 'big-menu'"
id="side-menu"
:open="isOpen"
class="cm"
@close="closeMenu"
/>
<SideMenuWithCategories
v-else-if="display === 'side-with-categories'"
id="side-menu"
:open="isOpen"
class="cm"
@close="closeMenu"
/>
<SimpleSideMenu
v-else-if="display === 'simple-side-menu'"
id="side-menu"
:open="isOpen"
class="cm"
@close="closeMenu"
@open="openMenu"
@toggle="toggleMenu(!isOpen)"
/>
</template>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useConfigStore } from '../store/config.js'
import { useNavStore } from '../store/nav.js'
import { createElement } from '../lib/dom.js'
import { translate as t } from '@nextcloud/l10n'
import SimpleSideMenu from './SimpleSideMenu'
import TopWideMenu from './TopWideMenu'
import SideMenuWithCategories from './SideMenuWithCategories'
import PageLoader from '../components/PageLoader'
const config = ref(null)
const configStore = useConfigStore()
const navStore = useNavStore()
const display = ref(null)
const hasPageLoader = ref(false)
const isOpen = ref(false)
const hasApps = ref(false)
const openerHover = ref(false)
const toggleMenu = (value) => {
isOpen.value = value
}
const openMenu = () => {
toggleMenu(true)
}
const closeMenu = () => {
toggleMenu(false)
}
const createOpener = () => {
const nextcloud = document.querySelector('#nextcloud')
const logo = document.querySelector('.header-left .logo, .header-start .logo')
if (!nextcloud || !logo) {
return
}
if (logo.parentNode !== nextcloud) {
nextcloud.appendChild(logo)
}
const opener = createElement('button', {
class: 'cm-opener',
'arial-label': t('side_menu', 'Toggle the menu'),
html: `<span>${t('side_menu', 'Toggle the menu')}</span>`,
})
if (config.value['opener-position'] === 'before') {
nextcloud.parentNode.insertBefore(opener, nextcloud)
} else {
nextcloud.parentNode.insertBefore(opener, nextcloud.nextSibling)
}
opener.addEventListener('click', () => toggleMenu(true), true)
if (openerHover.value) {
opener.addEventListener('mouseenter', () => toggleMenu(true), true)
}
}
onMounted(async () => {
hasApps.value = (await navStore.getApps()).length > 0
config.value = await configStore.getConfig()
if (config.value['big-menu']) {
display.value = 'big-menu'
} else if (config.value['side-with-categories']) {
display.value = 'side-with-categories'
} else {
display.value = 'simple-side-menu'
}
hasPageLoader.value = config.value['loader-enabled']
openerHover.value = config.value['opener-hover']
if (hasApps.value) {
createOpener()
}
window.document.addEventListener('keydown', (e) => {
const key = e.key || e.keyCode
if ((key === 'o' || key === 79) && e.ctrlKey === true) {
e.preventDefault()
toggleMenu(!isOpen.value)
}
})
})
</script>

View file

@ -1,132 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div
ref="menu"
class="cm--sidemenuwithcategories"
:class="{ open: open }"
>
<div class="cm-header">
<SettingsButton
v-if="settings"
:href="settings.href"
:label="settings.name"
:avatar="settings.avatar"
/>
<AppSearch v-model="search" />
<OpenerButton
v-if="!openerHover"
@click="$emit('close')"
/>
</div>
<div class="cm-categories-wrapper">
<div class="cm-categories">
<template
v-for="(category, key) in items"
:key="key"
>
<div
v-if="containsAppsMatchingSearch(category.apps, search)"
class="cm-category"
>
<h2
v-if="category.name != ''"
class="cm-category-title"
>
{{ category.name }}
</h2>
<ul class="cm-apps">
<template
v-for="(app, appId) in category.apps"
:key="appId"
>
<SideMenuBigApp
v-if="isAppMatchingSearch(app, search)"
class="cm-app"
:classes="{ active: activeApp === appId }"
:icon="app.icon"
:label="app.name"
:href="app.href"
:target="targetBlankApps.indexOf(appId) !== -1 ? '_blank' : undefined"
/>
</template>
</ul>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup>
import { ref, useTemplateRef, onMounted, watch } from 'vue'
import { useNavStore } from '../store/nav.js'
import { useConfigStore } from '../store/config.js'
import { getActiveAppId } from '../lib/app.js'
import { focusActiveApp } from '../lib/menu.js'
import { containsAppsMatchingSearch, isAppMatchingSearch } from '../lib/search.js'
import OpenerButton from '../components/OpenerButton'
import SettingsButton from '../components/SettingsButton'
import AppSearch from '../components/AppSearch'
import SideMenuBigApp from '../components/SideMenuBigApp'
const emit = defineEmits(['close'])
const { open } = defineProps({
open: {
type: Boolean,
required: true,
},
})
const configStore = useConfigStore()
const navStore = useNavStore()
const items = ref([])
const activeApp = ref(null)
const targetBlankApps = ref([])
const settings = ref(null)
const search = ref('')
const openerHover = ref(false)
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
watch(
() => open,
(val) => {
if (val) {
focusActiveApp(menu.value)
}
},
)
onMounted(async () => {
const config = await configStore.getConfig()
targetBlankApps.value = config['target-blank-apps']
settings.value = config['settings']
openerHover.value = config['opener-hover'] && !isTouchDevice
items.value = await navStore.getCategories()
activeApp.value = getActiveAppId()
if (openerHover.value) {
menu.value.addEventListener('mouseleave', () => emit('close'))
}
})
</script>

View file

@ -1,176 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div
ref="menu"
:class="{ open: open }"
>
<div
v-if="settings || displayLogo || open || !openerHover"
class="cm-header"
>
<SettingsButton
v-if="settings && open"
:href="settings.href"
:label="settings.name"
:avatar="settings.avatar"
/>
<AppSearch
v-if="open"
v-model="search"
/>
<OpenerButton
v-if="!openerHover || isTouchDevice"
@click="$emit('toggle')"
/>
<MenuLogo
v-if="displayLogo"
class="cm-logo"
:classes="{ avatardiv: false }"
:image="useAvatarAsLogo ? avatar : logo"
:link="logoLink"
/>
</div>
<ul
class="cm-apps"
:class="{ 'side-menu-apps-list--with-logo': displayLogo }"
>
<template
v-for="(app, key) in apps"
:key="key"
>
<SideMenuApp
v-if="isAppMatchingSearch(app, search)"
class="cm-app"
:classes="{ active: app.id === activeApp }"
:icon="app.icon"
:label="app.name"
:href="app.href"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
/>
</template>
</ul>
</div>
</template>
<script setup>
import { ref, useTemplateRef, onMounted, watch } from 'vue'
import { useConfigStore } from '../store/config.js'
import { useNavStore } from '../store/nav.js'
import { focusActiveApp } from '../lib/menu.js'
import { isAppMatchingSearch } from '../lib/search.js'
import { getActiveAppId } from '../lib/app.js'
import OpenerButton from '../components/OpenerButton'
import SettingsButton from '../components/SettingsButton'
import SideMenuApp from '../components/SideMenuApp'
import AppSearch from '../components/AppSearch'
import MenuLogo from '../components/MenuLogo'
const { open } = defineProps({
open: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['close', 'open', 'toggle'])
const navStore = useNavStore()
const configStore = useConfigStore()
const targetBlankApps = ref(null)
const forceLightIcon = ref(null)
const activeApp = ref(null)
const avatar = ref(null)
const logo = ref(null)
const logoLink = ref(null)
const settings = ref(null)
const openerHover = ref(false)
const alwaysDisplayed = ref(false)
const displayLogo = ref(false)
const useAvatarAsLogo = ref(false)
const search = ref('')
const apps = ref([])
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
watch(apps, (val) => {
document.querySelector('html').classList.toggle('cm-always-displayed', alwaysDisplayed.value && val.length)
})
watch(
() => open,
(val) => {
if (val) {
focusActiveApp(menu.value)
}
},
)
function getFiltredAndSortedApps(items, order, topMenuApps, topSideMenuApps) {
const data = []
items.forEach((item) => {
if (topMenuApps.includes(item.id) && !topSideMenuApps.includes(item.id)) {
return
}
item.order = items.length + 1
order.forEach((id, key) => {
if (id === item.id) {
item.order = key + 1
}
})
data.push(item)
})
return data.sort((a, b) => {
return a.order < b.order ? -1 : 1
})
}
onMounted(async () => {
const config = await configStore.getConfig()
alwaysDisplayed.value = config['always-displayed']
targetBlankApps.value = config['target-blank-apps']
forceLightIcon.value = config['force-light-icon']
avatar.value = config['avatar']
logo.value = config['logo']
useAvatarAsLogo.value = config['use-avatar']
displayLogo.value = config['display-logo'] && !alwaysDisplayed.value && ((!useAvatarAsLogo.value && logo.value) || (useAvatarAsLogo.value && avatar.value))
logoLink.value = config['logo-link']
settings.value = config['settings']
openerHover.value = config['opener-hover'] && !isTouchDevice
activeApp.value = getActiveAppId()
apps.value = getFiltredAndSortedApps(await navStore.getApps(), config['apps-order'], config['top-menu-apps'], config['top-side-menu-apps'])
if (openerHover.value) {
menu.value.addEventListener('mouseleave', () => emit('close'))
}
if (alwaysDisplayed.value && openerHover.value) {
menu.value.addEventListener('mouseenter', () => emit('open'))
menu.value.addEventListener('mouseleave', () => emit('close'))
}
})
</script>

View file

@ -1,190 +0,0 @@
<!--
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<nav
class="cm-standardmenu app-menu show"
:aria-label="t('core', 'Applications menu')"
>
<ul
v-if="ready"
class="app-menu-main"
:class="{
'app-menu-main__hidden-label': hiddenLabels === 1,
'app-menu-main__show-hovered': hiddenLabels === 2,
}"
>
<li
v-for="app in mainAppList"
:key="app.id"
:data-app-id="app.id"
class="app-menu-entry"
:class="{
'app-menu-entry__active': app.id === activeApp,
'app-menu-entry__hidden-label': hiddenLabels === 1,
'app-menu-main__show-hovered': hiddenLabels === 2,
}"
:style="makeStyle(app)"
>
<a
:href="app.href"
:class="{ 'has-unread': app.unread > 0 }"
:aria-label="app.name"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.id === activeApp ? 'page' : false"
>
<img
:src="app.icon"
:alt="app.name"
/>
<div class="app-menu-entry--label">
{{ app.name }}
<span
v-if="app.unread > 0"
class="hidden-visually unread-counter"
>{{ app.unread }}</span
>
</div>
</a>
</li>
</ul>
<NcActions
class="cm-standardmenu-app-menu-more app-menu-more"
:aria-label="t('core', 'More apps')"
>
<NcActionLink
v-for="app in popoverAppList"
:key="app.id"
:aria-label="app.name"
:aria-current="app.id === activeApp ? 'page' : false"
:href="app.href"
:style="makeStyle(app)"
class="cm-standardmenu-app-menu-popover-entry app-menu-popover-entry"
>
<template #icon>
<div
class="app-icon"
:class="{ 'has-unread': app.unread > 0 }"
>
<img
:src="app.icon"
:alt="app.name"
/>
</div>
</template>
{{ app.name }}
<span
v-if="app.unread > 0"
class="hidden-visually unread-counter"
>{{ app.unread }}</span
>
</NcActionLink>
</NcActions>
</nav>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useConfigStore } from '../store/config.js'
import { useNavStore } from '../store/nav.js'
import { NcActions, NcActionLink } from '@nextcloud/vue'
import { getActiveAppId } from '../lib/app.js'
const navStore = useNavStore()
const configStore = useConfigStore()
const ready = ref(false)
const appList = ref([])
const targetBlankApps = ref(null)
const hiddenLabels = ref(false)
const topMenuApps = ref([])
const appsOrder = ref([])
const mainAppList = ref([])
const popoverAppList = ref([])
const activeApp = ref(null)
let resizeTimeout = null
const setApps = (value) => {
value.forEach((app) => {
Array.from(topMenuApps.value).forEach((id) => {
if (app.id === id) {
app.order = appsOrder.value.findIndex((element) => element === app.id) || null
appList.value.push(app)
}
})
})
computeLists()
}
const appLimit = () => {
const headerStart = document.querySelector('#header .header-start')
const headerEnd = document.querySelector('#header .header-end')
const body = document.querySelector('body')
let size = (headerEnd ? headerEnd.offsetWidth : 0) + 70
if (headerStart) {
Array.from(headerStart.children).forEach((child) => {
if (child.id !== 'app-menu-container') {
size += child.offsetWidth
}
})
}
return Math.max(0, Math.floor((body.offsetWidth - size) / 70))
}
const makeStyle = (app) => {
if (app.order !== null) {
return { order: app.order }
}
return {}
}
const computeLists = () => {
mainAppList.value = appList.value.slice(0, appLimit())
popoverAppList.value = appList.value.slice(appLimit()).sort((a, b) => a.order - b.order)
}
const reComputeLists = (delay) => {
window.clearTimeout(resizeTimeout)
resizeTimeout = window.setTimeout(computeLists, delay || 100)
}
onMounted(async () => {
const config = await configStore.getConfig()
targetBlankApps.value = config['target-blank-apps']
hiddenLabels.value = config['top-menu-mouse-over-hidden-label']
topMenuApps.value = config['top-menu-apps']
appsOrder.value = config['apps-order']
activeApp.value = getActiveAppId()
ready.value = true
setApps(await navStore.getCoreApps())
window.addEventListener('resize', reComputeLists)
})
</script>
<script>
export default {
compatConfig: {
GLOBAL_MOUNT_CONTAINER: false,
},
}
</script>

Some files were not shown because too many files have changed in this diff Show more