Compare commits

...

127 Commits

Author SHA1 Message Date
dillard 2c3e6a245b fix(config): clear the old category cache on upgrade
The old cache is only replaced when the categories are requested, so
on instances that use a menu without categories the 170 KB app config
value stayed and was still loaded on every request. A post-migration
repair step clears it when the app is upgraded.
2026-09-30 13:32:40 +02:00
dillard 80f6dcf73a fix(config): refresh a category cache stored with another type
If cache-categories was stored with a type other than mixed or array,
for example set by hand with occ, the typed read throws. Treat that as
a missing cache so it is refreshed instead of breaking the menu and the
settings page.
2026-09-30 13:30:01 +02:00
dillard 73dea84d89 fix(config): keep the category cache in the memory cache when available
A cache belongs in Nextcloud's memory cache rather than in the app
config. Use the distributed cache (e.g. Redis) with a one-week TTL when
one is available; Nextcloud falls back to the local cache (APCu) when
only that is configured. Without any memory cache the lazy app config
value stays as the fallback. A leftover app config value is removed
when the categories are stored in the memory cache, and saving the
settings clears both.
2026-09-30 13:26:45 +02:00
dillard 22e87e12ad fix(config): only cache category names in the languages in use
The cache held the category names in all 96 app store languages
(about 49 KB), while an instance typically uses one or two. Keep only
English, the default language and the languages users have set, as the
two-letter codes the lookup uses; on a Dutch instance this is about
1.3 KB.

A user who switches to a language that is not cached yet sees English
names until the cache is refreshed after a week, or right away when an
admin saves the settings. The README mentions this.
2026-09-30 13:17:37 +02:00
dillard ccefefa6f7 fix(config): keep the category cache small and save settings at once
The app store category cache (cache-categories) held the name and
description of every category in every language, about 170 KB, stored
as a regular app config value. Nextcloud loads all regular values of
all apps on every request, so this one value was read on every page
load. It was only refreshed with the "remove cache" button.

- Keep only the category ids and names, store the cache as a lazy
  value so it is only loaded when categories are needed, and refresh
  it after a week.
- Only load the categories where they are used: the menus with
  categories, and the app list when custom categories are set.
- Save all settings in one request to a new admin endpoint, which
  accepts only known settings, then clears the category cache and
  changes the stylesheet cache key. The settings page sent one request
  per setting and reloaded right after; with Nextcloud's short-lived
  local config cache, the reloaded page could show a half-saved
  configuration, so a change sometimes had to be made twice.
2026-09-30 12:01:50 +02:00
Simon Vieille d666cac5be Merge pull request 'fix #510 #520 #417' (#521) from feature/nc35 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/521
2026-09-18 18:33:50 +02:00
Simon Vieille 9a71429182 doc: update changelog 2026-09-18 18:30:48 +02:00
Simon Vieille f1b495809f fix #510: [Bug] Styling of the icon labels and overflow icon is not correct 2026-09-18 18:28:08 +02:00
Simon Vieille e44b2c70a7 feat(app): add compatibility with Nextcloud 35 2026-09-18 18:06:54 +02:00
Simon Vieille 52de7326ec Merge pull request 'feat: release v6.1.0' (#518) from feature/release into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/518
2026-09-07 18:07:26 +02:00
Simon Vieille 4975743441 feat: release v6.1.0 2026-09-07 18:06:55 +02:00
Simon Vieille be6e9eaced Merge pull request 'fix(admin): add toggle menu button for admin navigation (#462)' (#517) from bugfix/issue462-adminmenu into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/517
2026-09-05 16:46:37 +02:00
Simon Vieille 627b9e8bf4 fix(admin): add toggle menu button for admin navigation (#462) 2026-09-05 16:44:52 +02:00
Simon Vieille 7bc64003a5 Merge pull request 'bugfix/issue510-styling' (#516) from bugfix/issue510-styling into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/516
2026-09-05 15:56:05 +02:00
Simon Vieille d078cc68e7 fix(StandardMenu): move 'more apps' buttons down (#510) 2026-09-05 15:54:26 +02:00
Simon Vieille 55a2b21ed1 fix(StandardMenu): move app labels down (#510) 2026-09-05 15:43:35 +02:00
Simon Vieille f7a8a41f6e merge develop 2026-09-05 13:16:38 +02:00
Simon Vieille c2c6399106 Merge pull request 'feat: add search toggle option (fix #509)' (#515) from feature/issue509-search-toggle into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/515
2026-09-05 13:09:52 +02:00
Simon Vieille f4833d1956 feat: add search toggle option (fix #509) 2026-09-05 13:06:24 +02:00
Simon Vieille 9eab478c36 chore(info): set version 2026-07-18 16:28:51 +02:00
Simon Vieille c1588dfc2e Merge pull request 'feat(standard-menu): add unified-search-input size handling' (#507) from bugfix/top-menu-popover into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/507
2026-07-18 16:26:18 +02:00
Simon Vieille c24bb6f8c8 feat(standard-menu): add unified-search-input size handling 2026-07-18 16:26:06 +02:00
Simon Vieille ca689d8b6f Merge pull request 'bugfix/top-menu-popover' (#506) from bugfix/top-menu-popover into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/506
2026-07-18 15:52:52 +02:00
Simon Vieille e81f0e4b76 doc: update changelog 2026-07-18 15:52:34 +02:00
Simon Vieille 7033569bed feat(standard-menu): add unified-search-input size handling 2026-07-18 15:52:21 +02:00
Simon Vieille a9788f0ab8 Merge pull request 'bugfix/always-displays-no-apps' (#505) from bugfix/always-displays-no-apps into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/505
2026-07-18 13:03:22 +02:00
Simon Vieille 1a8063bd94 doc: update changelog 2026-07-18 13:00:51 +02:00
Simon Vieille cc841570ec fix(ui): only apply always-displayed display css rules when the menu is displayed 2026-07-18 13:00:35 +02:00
Simon Vieille d6a2da402c fix(webpack): set global appVersion 2026-07-18 12:44:22 +02:00
Simon Vieille fef11cd0cf Merge pull request 'feature/nc34' (#503) from feature/nc34 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/503
2026-07-17 18:59:17 +02:00
Simon Vieille 12b35470c7 doc: update changelog 2026-07-17 18:58:42 +02:00
Simon Vieille 23cb8fcfc1 chore(app): remove compatibility with Nextcloud < 34 2026-07-17 18:58:24 +02:00
Simon Vieille 2b4da9cfce Merge pull request 'chore(info): set version' (#502) from feature/nc34 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/502
2026-07-17 18:45:08 +02:00
Simon Vieille f2af45711d chore(info): set version 2026-07-17 18:44:57 +02:00
Simon Vieille 35afa38f73 Merge pull request 'feature/nc34' (#500) from feature/nc34 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/500
2026-07-17 18:43:53 +02:00
Simon Vieille ad162f5821 doc: update changelog 2026-07-17 18:34:53 +02:00
Simon Vieille d3f87adcca feat(app): add compatibility with Nextcloud 34 2026-07-17 18:34:53 +02:00
Simon Vieille b32e4ded42 fix(app): add compatibility with Nextcloud 34 2026-07-17 18:31:25 +02:00
Simon Vieille 88e54e01b6 chore(dep): update @nextcloud/vue 2026-07-17 18:29:11 +02:00
Simon Vieille ce01983f1c Merge pull request 'fix: #489: allow PHP 8.5 compatibility' (#490) from feature/php85 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/490
2026-04-05 09:23:27 +02:00
Simon Vieille 6ee79a3110 chore: app version 2026-04-05 09:23:09 +02:00
Simon Vieille a32bf417a7 fix: #489: allow PHP 8.5 compatibility 2026-04-05 09:12:58 +02:00
Simon Vieille 337e1a07e3 doc: update changelog 2026-03-08 19:10:29 +01:00
gallegonovato fccfe1341a Translated using Weblate (Spanish)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/es/
2026-03-08 19:09:32 +01:00
Outbreak2096 242569316f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/zh_Hans/
2026-03-08 19:09:31 +01:00
gallegonovato 78888460fc Translated using Weblate (Spanish)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/es/
2026-03-08 19:09:31 +01:00
Simon Vieille 4d453676dc Merge branch 'develop' into translations 2026-03-08 19:08:08 +01:00
Simon Vieille f49b30b20e Merge pull request 'bugfix/intl-segmenter-polyfill' (#479) from bugfix/intl-segmenter-polyfill into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/479
2026-03-08 19:06:18 +01:00
Simon Vieille 0067b4e1c8 doc: update changelog 2026-03-08 19:05:56 +01:00
Simon Vieille 77cf610c8b Merge branch 'develop' into bugfix/intl-segmenter-polyfill 2026-03-08 19:05:14 +01:00
Simon Vieille 5cd106640c Merge pull request 'bugfix/issue475-orm' (#476) from bugfix/issue475-orm into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/476
2026-03-01 13:18:24 +01:00
Simon Vieille d057cd7665 doc: update changelog 2026-03-01 13:11:26 +01:00
Simon Vieille 520225603b fix(admin/*SaveButton): cast settings to string 2026-03-01 13:09:02 +01:00
Simon Vieille c2ecc5bf98 fix(LangRepository): check orm capabilities to query entities 2026-03-01 13:08:26 +01:00
Simon Vieille 8cca320a43 chore: set side_menu as package name 2026-03-01 13:07:07 +01:00
Simon Vieille 19aaf0759b Merge pull request 'chore: app version' (#473) from feature/nc33 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/473
2026-02-27 09:18:16 +01:00
Simon Vieille 47a83f10cc chore: app version 2026-02-27 09:17:46 +01:00
Simon Vieille e1a191121d Merge pull request 'chore: add compatibility with NC33' (#471) from feature/nc33 into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/471
2026-02-27 09:16:32 +01:00
Simon Vieille 54f4dce09b chore: add compatibility with NC33 2026-02-27 09:16:02 +01:00
Simon Vieille c0aaa206ed Merge pull request 'bugfix/issue468-logo' (#469) from bugfix/issue468-logo into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/469
2026-02-16 11:52:40 +01:00
Simon Vieille 0124336558 doc: update changelog 2026-02-16 10:12:09 +01:00
Simon Vieille 5ccb56309a fix #468: force nextcloud logo display css rule (opener-only) 2026-02-16 10:09:31 +01:00
gallegonovato 0cb14258ad Translated using Weblate (Spanish)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/es/
2026-02-13 18:29:09 +00:00
Simon Vieille 7d26be253a doc: update changelog 2026-01-10 15:51:26 +01:00
Simon Vieille 0437019bb0 fix #464: add Intl.Segmenter polyfill 2026-01-10 15:49:16 +01:00
Outbreak2096 7e7397735b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/zh_Hans/
2025-11-21 16:10:09 +00:00
Simon Vieille be68f2ef55 fix(doc): fix curl cmd 2025-10-29 17:48:09 +01:00
Codeberg Translate ccfb541404 Merge branch 'origin/translations' into Weblate. 2025-09-30 21:16:55 +00:00
gallegonovato f4e35bdc5d Translated using Weblate (Spanish)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/es/
2025-09-30 21:16:53 +00:00
Simon Vieille 1bf9cee8d6 Merge pull request 'bugfix/build' (#446) from bugfix/build into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/446
2025-09-29 00:19:56 +02:00
Simon Vieille 403a7a72fb update changelog 2025-09-29 00:18:55 +02:00
Simon Vieille 81ae76cdb7 build: add package-lock.json 2025-09-29 00:17:57 +02:00
Simon Vieille 32e5ecda8f ci: rollback node to v20 2025-09-29 00:12:56 +02:00
Simon Vieille de98967da5 ci: upgrade node to v22 2025-09-28 23:50:10 +02:00
Simon Vieille f39f3b3aef Merge branch 'translations' into develop 2025-09-28 19:49:28 +02:00
Simon Vieille 0db732b921 fix(trans/fr): remove typo 2025-09-28 19:48:27 +02:00
Simon Vieille ce39df6c2d update changelog 2025-09-28 19:47:42 +02:00
Simon Vieille bc94b61c12 Merge branch 'translations' into develop 2025-09-28 19:46:14 +02:00
Simon Vieille c54c969824 release v5.1.2 2025-09-28 19:43:27 +02:00
Simon Vieille e828a914cf Merge pull request '"Refactor Side Menu JsController to Use IAvatarManager via Dependency Injection for Nextcloud 32 Com' (#442) from AndyXheli/side_menu:develop into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/442
2025-09-28 19:40:50 +02:00
mbouzada 177f88b9aa Translated using Weblate (Galician)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/gl/
2025-09-28 01:36:08 +00:00
p-bo 66a6fda385 Translated using Weblate (Czech)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/cs/
2025-09-28 01:36:08 +00:00
AndyXheli 9ad73c7bf7 Update lib/Controller/JsController.php
Signed-off-by: AndyXheli <andyxheli@gmail.com>
2025-09-20 16:43:41 +02:00
AndyXheli acde24ea71 Update lib/Controller/JsController.php 2025-09-20 16:42:52 +02:00
AndyXheli 8bd89b4565 Update lib/Controller/JsController.php 2025-09-20 16:41:49 +02:00
AndyXheli 58cfbc24af "Refactor Side Menu JsController to Use IAvatarManager via Dependency Injection for Nextcloud 32 Com
🔧 Problem: The Side Menu app was calling OC\Server::getAvatarManager(), which no longer exists in Nextcloud 32.

✅ Fix:

Injected IAvatarManager using Nextcloud’s public API instead of accessing it via OC::$server.

Updated the constructor to accept IAvatarManager, IUserSession, INavigationManager, and IURLGenerator.

Replaced all direct calls to OC::$server->getAvatarManager() and similar with the injected services.

This makes the app compatible with Nextcloud 32 and avoids deprecated internal method calls.

Signed-off-by: AndyXheli <andyxheli@gmail.com>
2025-09-20 16:25:58 +02:00
s0undy a00fcb1423 Translated using Weblate (Swedish)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/sv/
2025-06-08 19:58:24 +00:00
s0undy f335c086b6 Translated using Weblate (Swedish)
Currently translated at 15.3% (17 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/sv/
2025-06-01 23:58:24 +00:00
yurtpage 7bc8cd857c Translated using Weblate (Russian)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/ru/
2025-06-01 23:58:24 +00:00
Simon Vieille d9051304c8 Merge branch 'translations' into develop 2025-05-26 18:48:38 +02:00
yurtpage 95d7c1f0c7 Translated using Weblate (Slovak)
Currently translated at 52.2% (57 of 109 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/sk/
2025-05-26 18:45:36 +02:00
yurtpage c6fe0db0b6 Translated using Weblate (Russian)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/ru/
2025-05-26 18:45:35 +02:00
Outbreak2096 3fbfd36cba Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/zh_Hans/
2025-05-26 18:45:35 +02:00
Simon 70a3fefb3d Translated using Weblate (French)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom Menu/Application
Translate-URL: https://translate.gitnet.fr/projects/custom-menu/application/fr/
2025-05-26 18:45:35 +02:00
deblan ddb5b13849 Added translation using Weblate (Swedish) 2025-05-26 14:37:17 +00:00
Simon Vieille 3f16a674e6 release v5.1.1 2025-05-23 09:02:11 +02:00
Simon Vieille 782faf6add update changelog 2025-05-04 18:34:29 +02:00
Simon Vieille 92d15d161b Merge pull request 'fix #349: add custom controller to retrieve core apps' (#431) from bugfix/issue349-components-apps into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/431
2025-05-04 18:31:36 +02:00
Simon Vieille cd4b3b1054 fix #349: add custom controller to retrieve core apps 2025-05-04 18:24:32 +02:00
Simon Vieille f58dedf553 fix(build): define appName to fix this error: "The '@nextcloud/vue' library was used without setting / replacing the 'appName'" 2025-05-04 13:11:05 +02:00
Simon Vieille 8c6f0ad1da release v5.1.0 2025-04-30 13:01:22 +02:00
Simon Vieille 7485a5b349 Merge pull request 'fix #425: allow to set a color using hex code' (#427) from feature/issue425-colorpicker into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/427
2025-04-30 13:00:29 +02:00
Simon Vieille b0e01c2eec update changelog 2025-04-30 12:57:49 +02:00
Simon Vieille d2730afe9f fix #425: allow to set a color using hex code 2025-04-30 12:57:13 +02:00
Simon Vieille 33ab7dff97 update changelog 2025-04-30 12:50:51 +02:00
Simon Vieille 0da550e3eb Merge pull request 'Use Request object' (#426) from llaumgui-develop into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/426
2025-04-30 12:47:23 +02:00
Simon Vieille 4bea4afb07 refactor(Application): add properties types 2025-04-30 12:45:32 +02:00
llaumgui d485b728e7 Use Request object 2025-04-30 09:30:35 +02:00
Simon Vieille 8772504b76 release v5.0.3 2025-04-30 08:41:10 +02:00
Simon Vieille 75bea4be38 Merge pull request 'Update lib/AppInfo/Application.php' (#421) from Monica-Wood/side_menu:http_user_agent_check_patch into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/421
Reviewed-by: Simon Vieille <contact@deblan.fr>
2025-04-30 08:37:15 +02:00
Monica-Wood 8787918547 Update lib/AppInfo/Application.php
The current change is currently filling the nextcloud logs up with the error:
`Undefined array key \"HTTP_USER_AGENT\" at /srv/www/nextcloud/apps/side_menu/lib/AppInfo/Application.php#109`

This adds a check to ensure the index exists before read it.
2025-04-30 07:31:13 +02:00
Simon Vieille 1fcbd89d19 release v5.0.2 2025-04-29 19:26:07 +02:00
Simon Vieille f177340b13 update changelog 2025-04-29 19:24:04 +02:00
Simon Vieille aaa7afac51 Merge pull request 'fix #418: allow non admin user to access their settings' (#419) from bugfix/issue418-userconfigaccess into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/419
2025-04-29 19:23:27 +02:00
Simon Vieille fd4628d209 update changelog 2025-04-29 19:22:25 +02:00
Simon Vieille f9aa59ca04 fix #418: allow non admin user to access their settings 2025-04-29 19:21:17 +02:00
yurtpage 9f8db6be4f Translated using Weblate (Slovak)
Currently translated at 52.2% (57 of 109 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/sk/
2025-04-23 20:53:03 +00:00
yurtpage cc28ad4dc5 Translated using Weblate (Russian)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/ru/
2025-04-23 20:53:02 +00:00
Codeberg Translate 3e139d42a8 Merge branch 'origin/translations' into Weblate. 2025-04-18 20:39:10 +00:00
Outbreak2096 a956efaf15 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom menu/Application
Translate-URL: https://translate.codeberg.org/projects/custom-menu/application/zh_Hans/
2025-04-18 20:39:09 +00:00
Simon 6a54ae3781 Translated using Weblate (French)
Currently translated at 100.0% (111 of 111 strings)

Translation: Custom Menu/Application
Translate-URL: https://translate.gitnet.fr/projects/custom-menu/application/fr/
2025-04-18 16:49:52 +00:00
Simon Vieille 95e4ef136e Merge pull request 'fix #413: add user-agent check for memories mobile app' (#414) from bugfix/issues413-memories into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/414
2025-04-18 10:36:15 +02:00
Simon Vieille c0eb65547d fix #413: add user-agent check for memories mobile app 2025-04-17 15:15:03 +02:00
Simon Vieille 29d061c379 update readme 2025-04-17 08:48:14 +02:00
Simon Vieille 8e22d9ea62 Merge pull request 'bugfix/standard-menu-mobile' (#411) from bugfix/standard-menu-mobile into develop
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/411
2025-04-17 00:04:48 +02:00
Simon Vieille 8957f1ae78 update app info 2025-04-17 00:03:04 +02:00
Simon Vieille 7b4b447754 fix(StandardMenu): appLimit must return a value > 0 2025-04-17 00:02:47 +02:00
43 changed files with 10678 additions and 424 deletions
-4
View File
@@ -2,8 +2,4 @@
/node_modules
/l10n/*
/releases
/package-lock.json
!/l10n/.gitkeep
/yarn*.log
/src/admin.js.bk
/templates/settings/admin-form.php.bk
+575 -123
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -24,8 +24,7 @@ You like this app and you want to support me? ☕ [Buy me a coffee](https://www.
Requirements
------------
* PHP >= 8.0
* App `theming` enabled
* PHP >= 8.1
Installation and upgrade
------------------------
@@ -41,7 +40,7 @@ If you want to install it from source, go to https://gitnet.fr/deblan/side_menu/
```
$ cd /path/to/nextcloud/apps
$ curl -sS https://gitnet.fr/attachments/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | tar xvfz -
$ VERSION=x.y.z; curl -sS "https://gitnet.fr/deblan/side_menu/releases/download/${VERSION}/side_menu_v${VERSION}.tar.gz" | tar xvfz -
```
Administrators can edit many settings using the administration page.
@@ -50,6 +49,8 @@ Users can disable the menu using the page of personal settings.
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
The names of the app store categories are cached for a week, only in the languages that are in use on the instance (and English). If a user switches to a new language and sees English category names, save the settings on the administration page to refresh the cache right away.
### Use first top menu app as default app
You can easily let Custom Menu redirect to the first app in the top menu by changing the following parameter in your `config/config.php`:
+8 -3
View File
@@ -30,7 +30,7 @@ Notice
Because I believe in a free and decentralized Internet, [Gitnet](https://gitnet.fr) is **self-hosted at home**.
In case of downtime, you can download **Custom Menu** from [here](https://kim.deblan.fr/~side_menu/).
]]></description>
<version>5.0.0</version>
<version>6.2.0</version>
<licence>agpl</licence>
<author mail="contact@deblan.fr" homepage="https://www.deblan.fr/">Simon Vieille</author>
<namespace>SideMenu</namespace>
@@ -52,9 +52,14 @@ In case of downtime, you can download **Custom Menu** from [here](https://kim.de
<screenshot><![CDATA[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>
<dependencies>
<php min-version="8.1" max-version="8.4" />
<nextcloud min-version="31" max-version="32"/>
<php min-version="8.1" max-version="8.5" />
<nextcloud min-version="34" max-version="35"/>
</dependencies>
<repair-steps>
<post-migration>
<step>OCA\SideMenu\Migration\ClearCategoryCache</step>
</post-migration>
</repair-steps>
<settings>
<admin>OCA\SideMenu\Settings\Admin</admin>
<admin-section>OCA\SideMenu\Settings\AdminSection</admin-section>
+27 -21
View File
@@ -2,24 +2,33 @@
namespace OCA\SideMenu\AppInfo;
use OC;
use OC\AllConfig;
use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\AppFramework\Http\Request;
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OC\Security\CSRF\CsrfTokenManager;
use OC\User\User;
use OCA\SideMenu\Service\AppRepository;
use OCA\SideMenu\Service\CategoryRepository;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy;
use OCA\SideMenu\Service\LangRepository;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Util;
use Psr\Container\ContainerInterface;
@@ -31,23 +40,12 @@ use Psr\Container\ContainerInterface;
class Application extends App implements IBootstrap
{
public const APP_ID = 'side_menu';
public const APP_NAME = 'Custom menu';
/**
* @var OC\AllConfig
*/
protected $config;
/**
* @var ContentSecurityPolicyNonceManager
*/
protected $cspnm;
/**
* @var User
*/
protected $user;
protected IConfig $config;
protected ContentSecurityPolicyNonceManager $cspnm;
protected IRequest $request;
protected ?User $user = null;
public function __construct(array $urlParams = [])
{
@@ -62,7 +60,11 @@ class Application extends App implements IBootstrap
$c->get(ConfigProxy::class),
$c->get(IConfig::class),
$c->get(IFactory::class),
$c->get(IUserSession::class)
$c->get(IUserSession::class),
$c->get(IAppConfig::class),
$c->get(ITimeFactory::class),
$c->get(LangRepository::class),
$c->get(ICacheFactory::class)
);
});
@@ -93,9 +95,9 @@ class Application extends App implements IBootstrap
public function boot(IBootContext $context): void
{
$this->config = \OC::$server->getConfig();
$this->cspnm = \OC::$server->getContentSecurityPolicyNonceManager();
$this->user = \OC::$server[IUserSession::class]->getUser();
$this->config = Server::get(IConfig::class);
$this->user = Server::get(IUserSession::class)->getUser();
$this->request = Server::get(IRequest::class);
if (!$this->isEnabled()) {
return;
@@ -106,6 +108,10 @@ class Application extends App implements IBootstrap
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;
$isForced = (bool) $this->config->getAppValue(self::APP_ID, 'force', '0');
@@ -143,7 +149,7 @@ class Application extends App implements IBootstrap
$cache = $this->config->getAppValue(self::APP_ID, 'cache', '0');
foreach ($assets as $value) {
$route = \OC::$server->getURLGenerator()->linkToRoute(
$route = Server::get(IURLGenerator::class)->linkToRoute(
$value['route'],
['v' => $cache]
);
+83 -30
View File
@@ -20,12 +20,14 @@
namespace OCA\SideMenu\Controller;
use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\CategoryRepository;
use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\ConfigProxy;
use OCA\SideMenu\Service\LangRepository;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
@@ -35,6 +37,12 @@ use OCP\IURLGenerator;
class AdminSettingController extends Controller
{
/**
* Keys that are managed by the app or by Nextcloud itself ("enabled")
* and must not be written from the settings form.
*/
protected const UNSAVED_KEYS = ['cache', 'cache-categories', 'enabled'];
public function __construct(
$appName,
IRequest $request,
@@ -43,6 +51,7 @@ class AdminSettingController extends Controller
protected IURLGenerator $urlGenerator,
protected Color $color,
protected LangRepository $langRepository,
protected CategoryRepository $categoryRepository,
) {
parent::__construct($appName, $request);
}
@@ -51,7 +60,7 @@ class AdminSettingController extends Controller
#[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')]
public function removeCache(): RedirectResponse
{
$this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]');
$this->categoryRepository->clearCache();
return new RedirectResponse($this->urlGenerator->linkToRoute('settings.AdminSettings.index', [
'section' => Application::APP_ID,
@@ -97,6 +106,7 @@ class AdminSettingController extends Controller
'use-avatar',
'add-logo-link',
'show-settings',
'show-search-form',
'loader-enabled',
'always-displayed',
'enabled',
@@ -130,13 +140,84 @@ class AdminSettingController extends Controller
'top-menu-mouse-over-hidden-label',
];
$defaults = [
$defaults = $this->getDefaults();
$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)) {
continue;
}
if (in_array($key, $booleans)) {
$config[$key] = $default === '1';
} elseif (in_array($key, $arrays)) {
$config[$key] = json_decode($default);
} elseif (in_array($key, $integers)) {
$config[$key] = (int) $default;
} else {
$config[$key] = $default;
}
}
$config['langs'] = $this->langRepository->getUsedLangs();
return new JSONResponse($config);
}
#[PasswordConfirmationRequired]
#[FrontpageRoute(verb: 'POST', url: '/admin/config')]
public function saveConfiguration(array $config): JSONResponse
{
$allowed = array_diff(array_keys($this->getDefaults()), self::UNSAVED_KEYS);
foreach ($config as $key => $value) {
if (!in_array($key, $allowed, true)) {
continue;
}
if (is_array($value)) {
$value = json_encode($value);
} elseif (is_bool($value)) {
$value = $value ? '1' : '0';
}
$this->config->setAppValue(Application::APP_ID, $key, (string) $value);
}
// New stylesheet URL, and category names that match the new settings
$this->config->setAppValue(Application::APP_ID, 'cache', (string) time());
$this->categoryRepository->clearCache();
return new JSONResponse(['status' => 'ok']);
}
protected function getDefaults(): array
{
return [
'opener-only' => '0',
'opener-hover' => '0',
'display-logo' => '1',
'use-avatar' => '0',
'add-logo-link' => '1',
'show-settings' => '0',
'show-search-form' => '1',
'loader-enabled' => '1',
'always-displayed' => '0',
'enabled' => '1',
@@ -186,33 +267,5 @@ class AdminSettingController extends Controller
'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);
}
}
+74
View File
@@ -0,0 +1,74 @@
<?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,
]);
}
}
+16 -15
View File
@@ -20,7 +20,6 @@
namespace OCA\SideMenu\Controller;
use OC\User\User;
use OCA\SideMenu\AppInfo\Application;
use OCA\SideMenu\Service\ConfigProxy;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Controller;
@@ -29,10 +28,12 @@ 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\TemplateResponse;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\IAvatarManager;
use OCP\INavigationManager;
use OCP\IURLGenerator;
class JsController extends Controller
{
@@ -44,14 +45,13 @@ class JsController extends Controller
protected ConfigProxy $config,
protected ThemingDefaults $themingDefaults,
protected IFactory $l10nFactory,
protected IAvatarManager $avatarManager,
protected IUserSession $userSession,
protected INavigationManager $navigationManager,
protected IURLGenerator $urlGenerator,
) {
parent::__construct($appName, $request);
$this->themingDefaults = $themingDefaults;
$this->user = \OC::$server[IUserSession::class]->getUser();
$this->config = $config;
$this->l10nFactory = $l10nFactory;
$this->user = $this->userSession->getUser();
}
#[NoCSRFRequired]
@@ -99,25 +99,25 @@ class JsController extends Controller
$targetBlankApps = $userTargetBlankApps;
}
$isAvatarSet = \OC::$server->getAvatarManager()->getAvatar($this->user->getUid())->exists();
$isAvatarSet = $this->avatarManager->getAvatar($this->user->getUID())->exists();
if ($useAvatar && $isAvatarSet) {
$avatar = \OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUid(),
$avatar = $this->urlGenerator->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUID(),
'size' => 128,
'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0),
]);
}
if ($this->config->getAppValueBool('show-settings', '0')) {
$settingsNav = \OC::$server->getNavigationManager()->getAll('settings');
$settingsNav = $this->navigationManager->getAll('settings');
if (isset($settingsNav['settings'])) {
$settings = [
'href' => $settingsNav['settings']['href'],
'name' => $settingsNav['settings']['name'],
'avatar' => \OC::$server->getURLGenerator()->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUid(),
'avatar' => $this->urlGenerator->linkToRoute('core.avatar.getAvatar', [
'userId' => $this->user->getUID(),
'size' => 32,
'v' => $this->config->getUserValueInt($this->user, 'avatar', 'version', 0),
]),
@@ -126,7 +126,7 @@ class JsController extends Controller
}
}
$indexUrl = \OC::$server->getURLGenerator()->linkTo('', 'index.php');
$indexUrl = $this->urlGenerator->linkTo('', 'index.php');
return [
'opener-position' => $this->config->getAppValue('opener-position', 'before'),
@@ -135,6 +135,7 @@ class JsController extends Controller
'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'),
'show-search-form' => $this->config->getAppValueBool('show-search-form', '1'),
'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'),
'loader-enabled' => $this->config->getAppValueBool('loader-enabled', '1'),
'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'),
@@ -98,6 +98,7 @@ class PersonalSettingController extends Controller
}
#[NoCSRFRequired]
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/user/config')]
public function configuration(): JSONResponse
{
+46
View File
@@ -0,0 +1,46 @@
<?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\Migration;
use OCA\SideMenu\Service\CategoryRepository;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
/**
* Earlier versions stored the category cache as a regular app config
* value, which Nextcloud loads on every request. Clear it on upgrade so
* it does not linger on instances that never request the categories.
*/
class ClearCategoryCache implements IRepairStep
{
public function __construct(protected CategoryRepository $categoryRepository)
{
}
public function getName(): string
{
return 'Clear the category cache';
}
public function run(IOutput $output): void
{
$this->categoryRepository->clearCache();
}
}
+2 -1
View File
@@ -40,7 +40,8 @@ class AppRepository
{
$navigation = $this->navigationManager->getAll();
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
$categories = $this->categoryRepository->getOrderedCategories();
// Only needed to check custom categories, skip loading them otherwise
$categories = empty(array_filter($appCategoriesCustom)) ? [] : $this->categoryRepository->getOrderedCategories();
$apps = $this->ocApp->listAllApps();
$visibleApps = [];
+122 -7
View File
@@ -4,6 +4,11 @@ namespace OCA\SideMenu\Service;
use OC\App\AppStore\Fetcher\CategoryFetcher;
use OCA\SideMenu\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Exceptions\AppConfigTypeConflictException;
use OCP\IAppConfig;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\L10N\IFactory;
@@ -15,12 +20,23 @@ use OCP\L10N\IFactory;
*/
class CategoryRepository
{
public const CACHE_KEY = 'cache-categories';
/**
* The app store categories rarely change, refresh them once a week.
*/
protected const CACHE_TTL = 604800;
public function __construct(
protected CategoryFetcher $categoryFetcher,
protected ConfigProxy $config,
protected IConfig $iConfig,
protected IFactory $l10nFactory,
protected IUserSession $userSession,
protected IAppConfig $appConfig,
protected ITimeFactory $timeFactory,
protected LangRepository $langRepository,
protected ICacheFactory $cacheFactory,
) {
}
@@ -32,15 +48,9 @@ class CategoryRepository
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
$type = $this->config->getAppValue('categories-order-type', 'default');
$order = $this->config->getAppValueArray('categories-order', '[]');
$categoriesLabels = $this->config->getAppValueArray('cache-categories', '[]');
$categoriesLabels = $this->getCategories();
$customCategories = $this->config->getAppValueArray('categories-custom', '[]');
if (empty($categoriesLabels)) {
$categoriesLabels = $this->categoryFetcher->get();
$this->iConfig->setAppValue(Application::APP_ID, 'cache-categories', json_encode($categoriesLabels));
}
foreach ($categoriesLabels as $k => $category) {
$categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ??
$category['translations']['en']['name'];
@@ -85,4 +95,109 @@ class CategoryRepository
return $categoriesLabels;
}
public function clearCache(): void
{
$this->getDistributedCache()?->remove(self::CACHE_KEY);
$this->appConfig->deleteKey(Application::APP_ID, self::CACHE_KEY);
}
/**
* The languages to keep in the cache: English as fallback, the
* default language and the languages of the users, as two-letter
* codes like getOrderedCategories() looks them up. A user who picks
* a new language sees English names until the cache is refreshed,
* which saving the settings does right away.
*/
protected function getCachedLangs(): array
{
$langs = $this->langRepository->getUsedLangs();
$langs[] = (string) $this->iConfig->getSystemValue('default_language', 'en');
$langs[] = (string) $this->iConfig->getSystemValue('force_language', 'en');
return array_values(array_unique(array_filter(array_map(
static fn (string $lang) => strtolower(substr($lang, 0, 2)),
array_merge(['en'], $langs)
))));
}
/**
* Returns the app store categories with only their names. They are
* cached in the distributed cache (e.g. Redis) when one is configured,
* otherwise in a lazy app config value, so they are not loaded on
* every request with the other settings.
*/
protected function getCategories(): array
{
$cache = $this->getDistributedCache();
if ($cache !== null) {
$categories = $cache->get(self::CACHE_KEY);
if (is_array($categories)) {
return $categories;
}
} else {
try {
$stored = $this->appConfig->getValueArray(Application::APP_ID, self::CACHE_KEY, [], lazy: true);
} catch (AppConfigTypeConflictException) {
// Stored with another type, e.g. set by hand; refreshed below
$stored = [];
}
if (isset($stored['expires'], $stored['categories']) && $stored['expires'] > $this->timeFactory->getTime()) {
return $stored['categories'];
}
}
$categories = $this->fetchCategories();
// Don't cache an empty list, e.g. when the app store is unreachable
if (empty($categories)) {
return $categories;
}
$this->clearCache();
if ($cache !== null) {
$cache->set(self::CACHE_KEY, $categories, self::CACHE_TTL);
} else {
$this->appConfig->setValueArray(Application::APP_ID, self::CACHE_KEY, [
'expires' => $this->timeFactory->getTime() + self::CACHE_TTL,
'categories' => $categories,
], lazy: true);
}
return $categories;
}
protected function fetchCategories(): array
{
$langs = $this->getCachedLangs();
$categories = [];
foreach ($this->categoryFetcher->get() as $category) {
$names = [];
foreach ($category['translations'] ?? [] as $lang => $translation) {
if (in_array($lang, $langs, true)) {
$names[$lang] = ['name' => $translation['name'] ?? ''];
}
}
$categories[] = [
'id' => $category['id'],
'translations' => $names,
];
}
return $categories;
}
protected function getDistributedCache(): ?ICache
{
return $this->cacheFactory->isAvailable()
? $this->cacheFactory->createDistributed(Application::APP_ID)
: null;
}
}
+6 -1
View File
@@ -30,7 +30,12 @@ class LangRepository
->from('preferences')
;
$stmt = $qb->execute();
// Nextcloud >=33+
if (method_exists($qb, 'executeQuery')) {
$stmt = $qb->executeQuery();
} else {
$stmt = $qb->execute();
}
$langs = ['en'];
+9300
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -11,8 +11,9 @@
},
"dependencies": {
"@babel/core": ">=7.12.0 <8.0.0",
"@formatjs/intl-segmenter": "^12.0.8",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^9.0.0-alpha.8",
"@nextcloud/vue": "^9.9.0",
"node-polyfill-webpack-plugin": "^4.1.0",
"pinia": "^3.0.1",
"postcss": "^7.0.0 || ^8.0.1",
+2
View File
@@ -17,6 +17,8 @@
import './scss/admin.scss'
import '@formatjs/intl-segmenter/polyfill.js'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
+19 -40
View File
@@ -20,6 +20,9 @@
</template>
<script setup>
import axios from '@nextcloud/axios'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
import { ref } from 'vue'
import { waitPasswordConfirmation } from '../../lib/setting.js'
@@ -46,52 +49,28 @@ const filterConfig = (value) => {
}
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()
}
}
try {
await waitPasswordConfirmation()
} catch {
loading.value = false
return
}
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, {
success() {
update()
},
error() {
error.value = `Error while saving ${key}`
update()
},
})
}
})
.catch(() => {
counter = 0
loading.value = false
error.value = null
try {
// One request for all settings: separate requests per setting could
// leave a half-saved configuration in Nextcloud's config cache when
// the page reloads.
await axios.post(generateUrl('/apps/side_menu/admin/config'), {
config: filterConfig(config),
})
location.reload()
} catch {
error.value = t('side_menu', 'Error while saving the settings')
loading.value = false
}
}
</script>
+2 -2
View File
@@ -69,11 +69,11 @@ const save = async () => {
if (Array.isArray(value) || typeof value === 'object') {
value = JSON.stringify(value)
} else if (typeof value === 'boolean') {
value = value ? 1 : 0
value = value ? '1' : '0'
}
formData.push('name=' + encodeURIComponent(key))
formData.push('value=' + encodeURIComponent(value))
formData.push('value=' + encodeURIComponent(value.toString()))
fetch(url, {
method: 'POST',
@@ -17,6 +17,7 @@ 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
+14 -13
View File
@@ -94,18 +94,19 @@
'Search': 'Hledat'
'Toggle the menu': 'Vyp/zap nabídku'
'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'
'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!': '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'
'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': 'Close'
'Close': 'Zavřít'
'Show the search form': 'Show the search form'
+1
View File
@@ -109,3 +109,4 @@
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'
'Show the search form': 'Show the search form'
+14 -13
View File
@@ -94,18 +94,19 @@
'Search': 'Buscar'
'Toggle the menu': 'Alternar el menú'
'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'
'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!': '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'
'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': 'Close'
'Close': 'Cerrar'
'Show the search form': 'Show the search form'
+37 -17
View File
@@ -3,13 +3,18 @@
'No': 'Non'
'Yes': 'Oui'
'Menu': 'Menu'
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Utiliser le raccourcis clavier Ctrl+o pour ouvrir et fermer le menu latéral. Utiliser tab key pour naviguer.'
'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Utiliser
le raccourcis clavier Ctrl+o pour ouvrir et fermer le menu latéral. Utiliser tab
key pour naviguer.'
'Top menu': 'Menu supérieur'
'Apps that not must be moved in the side menu': 'Les applications qui ne doivent pas être affichées dans le menu latéral'
'If there is no selection then the global configuration is applied.': "Si il n'y a aucune sélection alors la configuration globale sera appliquée."
'Apps that not must be moved in the side menu': 'Les applications qui ne doivent pas
être affichées dans le menu latéral'
'If there is no selection then the global configuration is applied.': "Si il n'y a
aucune sélection alors la configuration globale sera appliquée."
'Experimental': 'Expérimental'
'Save': 'Sauvegarder'
'You like this app and you want to support me?': "Vous aimer cette application et vous souhaitez m'aider ?"
'You like this app and you want to support me?': "Vous aimer cette application et
vous souhaitez m'aider ?"
'Buy me a coffee ☕': 'Offrez moi un café ☕'
'Hidden': 'Caché'
'Small': 'Petit'
@@ -43,17 +48,22 @@
'Before the logo': 'Avant le logo'
'After the logo': 'Après le logo'
'Position': 'Position'
'Show only the opener (hidden logo)': "Afficher uniquement le bouton d'ouverture (masquer 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)."
'Show only the opener (hidden logo)': "Afficher uniquement le bouton d'ouverture (masquer
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)."
'Panel': 'Panneau'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Ouvrir le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Ouvrir
le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)'
'Display the big menu': 'Afficher le menu large'
'Display the logo': 'Afficher le logo'
'Icons and texts': 'Icônes et textes'
'Loader enabled': "Activation de l'indicateur de chargement"
'Tips': 'Astuces'
'Always displayed': 'Toujours affiché'
'This is the automatic behavior when the menu is always displayed.': "C'est le comportement automatique lorsque le menu est toujours affiché."
'This is the automatic behavior when the menu is always displayed.': "C'est le comportement
automatique lorsque le menu est toujours affiché."
'Not compatible with touch screens.': 'Incompatible avec les écrans tactiles.'
'Big menu': 'Menu large'
'Live preview': 'Aperçu en direct'
@@ -62,16 +72,21 @@
'Use my selection': 'Utiliser ma sélection'
'Show and hide the list of applications': 'Afficher et masquer la liste des applications'
'Use the avatar instead of the logo': "Utiliser l'avatar à la place du logo"
'You do not have permission to change the settings.': "Vous n'avez pas la permission de changer les paramètres."
'You do not have permission to change the settings.': "Vous n'avez pas la permission
de changer les paramètres."
'Force this configuration to users': 'Forcer cette configuration aux utilisateurs'
'Export the configuration': 'Exporter la configuration'
'Purge the cache': 'Purger le cache'
'Show the link to settings': 'Afficher le lien vers les paramètres'
'The menu is enabled by default for users': 'Le menu est activé par défaut pour les utilisateurs'
'The menu is enabled by default for users': 'Le menu est activé par défaut pour les
utilisateurs'
'Except when the configuration is forced.': 'Sauf lorsque la configuration est forcée.'
'Apps that should not be displayed in the menu': 'Applications qui ne doivent pas être affichées dans le menu'
'This feature is only compatible with the <code>big menu</code> display.': "Compatible avec l'affichage <code>Menu large</code>."
'The logo is a link to the default app': "Le logo est un lien vers l'application par défaut"
'Apps that should not be displayed in the menu': 'Applications qui ne doivent pas
être affichées dans le menu'
'This feature is only compatible with the <code>big menu</code> display.': "Compatible
avec l'affichage <code>Menu large</code>."
'The logo is a link to the default app': "Le logo est un lien vers l'application par
défaut"
'Others': 'Autres'
'Categories': 'Catégories'
'Customize sorting': 'Personnaliser le tri'
@@ -79,7 +94,8 @@
'Name': 'Nom'
'Customed': 'Personnalisé'
'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.'
'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'
@@ -87,9 +103,12 @@
'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'
'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'
@@ -109,3 +128,4 @@
'Customize': 'Personnaliser'
'Custom': 'Personnalisé'
'Close': 'Fermer'
'Show the search form': 'Afficher le formulaire de recherche'
+17 -16
View File
@@ -3,7 +3,7 @@
'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 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 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.'
@@ -94,18 +94,19 @@
'Search': 'Buscar'
'Toggle the menu': 'Alternar o menú'
'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'
'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'
'Show the search form': 'Show the search form'
+1
View File
@@ -109,3 +109,4 @@
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'
'Show the search form': 'Show the search form'
+1
View File
@@ -107,3 +107,4 @@
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'
'Show the search form': 'Show the search form'
+22 -21
View File
@@ -1,4 +1,4 @@
'Custom menu': 'Custom menu'
'Custom menu': 'Пользовательское меню'
'Enable the custom menu': 'Включить пользовательское меню'
'No': 'Нет'
'Yes': 'Да'
@@ -43,8 +43,8 @@
'Before the logo': 'Перед логотипом'
'After the logo': 'После логотипа'
'Position': 'Положение'
'Show only the opener (hidden logo)': 'Показать только открывающую часть (скрытый логотип)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Не отображать боковое меню и открывалку, если нет приложения (например, публичные страницы).'
'Show only the opener (hidden logo)': 'Показать только открывающую кнопку (скрытый логотип)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Не отображать боковое меню и открывалку, если нет доступного приложения (н.п. публичные страницы).'
'Panel': 'Панель'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Открывать меню при наведении мыши на открывалку (автоматически отключается на сенсорных экранах)'
'Display the big menu': 'Отобразить большое меню'
@@ -63,21 +63,21 @@
'Show and hide the list of applications': 'Показать или скрыть список приложений'
'Use the avatar instead of the logo': 'Использовать аватар вместо логотипа'
'You do not have permission to change the settings.': 'У вас нет разрешения изменять настройки.'
'Force this configuration to users': 'Принудительно предоставить эту конфигурацию пользователям'
'Force this configuration to users': 'Для обеспечения соблюдения этих настроек пользователями'
'Export the configuration': 'Экспортировать конфигурацию'
'Purge the cache': 'Очистить кэш'
'Show the link to settings': 'Показать ссылку на настройки'
'The menu is enabled by default for users': 'Это меню включено по умолчанию для пользователей'
'Except when the configuration is forced.': 'За исключением случаев, когда конфигурация является принудительной.'
'Except when the configuration is forced.': 'За исключением случаев, когда настройка принудительная.'
'Apps that should not be displayed in the menu': 'Ппрограммы, скрытые из меню'
'This feature is only compatible with the <code>big menu</code> display.': 'Эта функция совместима только с отображением <code>большого меню</code>.'
'This feature is only compatible with the <code>big menu</code> display.': 'Эта возможность совместима только с отображением <code>большого меню</code>.'
'The logo is a link to the default app': 'Логотип открывает приложение по умолчанию'
'Others': 'Прочие'
'Categories': 'Категории'
'Customize sorting': 'Настроить сортировку'
'Order by': 'В порядке'
'Name': 'Название'
'Customed': 'Customed'
'Customed': 'Пользовательское'
'Show and hide the list of categories': 'Показать или скрыть список категорий'
'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Эти настройки используются темами Тёмная и Тёмная Breeze.'
'Dark mode colors': 'Цвета тёмной темы'
@@ -90,22 +90,23 @@
'Applications kept in the top menu but also shown in side menu': 'Приложения хранящиеся в верхнем меню, но также отображающиеся в боковом меню'
'These applications must be selected in the previous option.': 'Эти приложения необходимо выбрать в предыдущем выборе.'
'Hide labels on mouse over': 'Скрыть название при наведении мыши'
'Except the hovered app': 'Кроме приложения, на котором курсор'
'Except the hovered app': 'Кроме приложения на котором сейчас'
'Search': 'Поиск'
'Toggle the menu': 'Переключить меню'
'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'
'Ask the developer': 'Спросить разработчика'
'New request': 'Новый запрос'
'Report a bug': 'Пожаловаться на ошибку'
'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'
'Done!': 'Готово!'
'Copy': 'Копировать'
'Need help': 'Нужна помощь'
'I would like a new feature': 'Я хочу новую возможность'
'Something went wrong': 'Что-то пошло не так'
'Select apps': 'Выберете приложения'
'Sort': 'Сортировать'
'Customize': 'Приспособить'
'Custom': 'Custom'
'Close': 'Close'
'Close': 'Закрыть'
'Show the search form': 'Show the search form'
+2 -1
View File
@@ -90,7 +90,7 @@
'Applications kept in the top menu but also shown in side menu': 'Applications kept in the top menu but also shown in side menu'
'These applications must be selected in the previous option.': 'These applications must be selected in the previous option.'
'Hide labels on mouse over': 'Hide labels on mouse over'
'Toggle the menu': 'Toggle the menu'
'Toggle the menu': 'Prepnite ponuku'
'Open the documentation': 'Open the documentation'
'Ask the developer': 'Ask the developer'
'New request': 'New request'
@@ -107,3 +107,4 @@
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'
'Show the search form': 'Show the search form'
+112
View File
@@ -0,0 +1,112 @@
'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'
'Show the search form': 'Show the search form'
+1
View File
@@ -109,3 +109,4 @@
'Customize': 'Customize'
'Custom': 'Custom'
'Close': 'Close'
'Show the search form': 'Show the search form'
+33 -32
View File
@@ -1,9 +1,9 @@
'Custom menu': '自定义菜单'
'Enable the custom menu': '启用自定义菜单'
'No': '取消'
'Yes': '确定'
'No': '否'
'Yes': '是'
'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 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> 键来导航。'
'Top menu': '顶部菜单'
'Apps that not must be moved in the side menu': '禁止在侧边栏菜单移动的应用'
'If there is no selection then the global configuration is applied.': '如果没有选择,则应用全局配置。'
@@ -25,7 +25,7 @@
'Opposite color': '相反颜色'
'Transparent': '透明'
'Opaque': '不透明'
'Opener': '容器'
'Opener': '触发器'
'Default': '默认'
'Default (dark)': '默认(深色)'
'Hamburger': 'Hamburger'
@@ -35,10 +35,10 @@
'Before the logo': '在徽标之前'
'After the logo': '在徽标之后'
'Position': '位置'
'Show only the opener (hidden logo)': '只显示容器(隐藏徽标)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': '如果没有应用程序(例如:公共页面),则不要显示侧边栏菜单和容器。'
'Show only the opener (hidden logo)': '仅显示触发器(隐藏徽标)'
'Do not display the side menu and the opener if there is no application (eg: public pages).': '如果没有应用程序(例如:公共页面),则不要显示侧边栏菜单和触发器。'
'Panel': '面板'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': '鼠标悬停时打开菜单 (触摸屏时将自动禁用)'
'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': '当鼠标悬停在触发器上时打开菜单(在触摸屏上自动禁用)'
'Display the big menu': '显示大型菜单'
'Display the logo': '显示徽标'
'Icons and texts': '图标和文本'
@@ -63,7 +63,7 @@
'Except when the configuration is forced.': '除非强制配置。'
'Apps that should not be displayed in the menu': '禁止在菜单中显示的应用'
'This feature is only compatible with the <code>big menu</code> display.': '此功能只和<code>大型菜单</code>兼容。'
'The logo is a link to the default app': 'logo链接到默认应用'
'The logo is a link to the default app': '徽标链接到默认应用'
'Others': '其他'
'Categories': '类别'
'Customize sorting': '自定义排序'
@@ -85,27 +85,28 @@
'Small text': '小文本'
'Normal text': '普通文本'
'Big text': '大文本'
'Applications': 'Applications'
'Applications kept in the top menu': 'Applications kept in the top menu'
'Applications kept in the top menu but also shown in side menu': 'Applications kept in the top menu but also shown in side menu'
'These applications must be selected in the previous option.': 'These applications must be selected in the previous option.'
'Hide labels on mouse over': 'Hide labels on mouse over'
'Except the hovered app': 'Except the hovered app'
'Search': 'Search'
'Toggle menu': 'Toggle menu'
'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'
'Applications': '应用程序'
'Applications kept in the top menu': '应用程序保留在顶部菜单中'
'Applications kept in the top menu but also shown in side menu': '应用程序保留在顶部菜单中,但也显示在侧边栏菜单中'
'These applications must be selected in the previous option.': '必须在上一个选项中选择这些应用程序。'
'Hide labels on mouse over': '鼠标悬停时隐藏标签'
'Except the hovered app': '除了悬停的应用'
'Search': '搜索'
'Toggle menu': '切换菜单'
'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': '关闭'
'Show the search form': 'Show the search form'
+2
View File
@@ -17,6 +17,8 @@
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'
+3 -1
View File
@@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:label="settings.name"
:avatar="settings.avatar"
/>
<AppSearch v-model="search" />
<AppSearch v-if="showSearchForm" v-model="search" />
<OpenerButton
v-if="!openerHover"
@click="$emit('close')"
@@ -102,6 +102,7 @@ const activeApp = ref(null)
const targetBlankApps = ref([])
const settings = ref(null)
const search = ref('')
const showSearchForm = ref(true)
const openerHover = ref(false)
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
@@ -120,6 +121,7 @@ onMounted(async () => {
targetBlankApps.value = config['target-blank-apps']
settings.value = config['settings']
showSearchForm.value = config['show-search-form']
openerHover.value = config['opener-hover'] && !isTouchDevice
items.value = await navStore.getCategories()
+3 -1
View File
@@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:avatar="settings.avatar"
/>
<AppSearch
v-if="open"
v-if="showSearchForm && open"
v-model="search"
/>
<OpenerButton
@@ -103,6 +103,7 @@ const openerHover = ref(false)
const alwaysDisplayed = ref(false)
const displayLogo = ref(false)
const useAvatarAsLogo = ref(false)
const showSearchForm = ref(true)
const search = ref('')
const apps = ref([])
const menu = useTemplateRef('menu')
@@ -151,6 +152,7 @@ onMounted(async () => {
alwaysDisplayed.value = config['always-displayed']
targetBlankApps.value = config['target-blank-apps']
forceLightIcon.value = config['force-light-icon']
showSearchForm.value = config['show-search-form']
avatar.value = config['avatar']
logo.value = config['logo']
+23 -13
View File
@@ -33,7 +33,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-app-id="app.id"
class="app-menu-entry"
:class="{
'app-menu-entry__active': app.active,
'app-menu-entry__active': app.id === activeApp,
'app-menu-entry__hidden-label': hiddenLabels === 1,
'app-menu-main__show-hovered': hiddenLabels === 2,
}"
@@ -44,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:class="{ 'has-unread': app.unread > 0 }"
:aria-label="app.name"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.active ? 'page' : false"
:aria-current="app.id === activeApp ? 'page' : false"
>
<img
:src="app.icon"
@@ -62,14 +62,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</li>
</ul>
<NcActions
class="cm-standardmenu-app-menu-more app-menu-more"
class="cm-standardmenu-app-menu-more"
:aria-label="t('core', 'More apps')"
:forceMenu="true"
>
<NcActionLink
v-for="app in popoverAppList"
:key="app.id"
:aria-label="app.name"
:aria-current="app.active ? 'page' : false"
:aria-current="app.id === activeApp ? 'page' : false"
:href="app.href"
:style="makeStyle(app)"
class="cm-standardmenu-app-menu-popover-entry app-menu-popover-entry"
@@ -101,6 +102,7 @@ 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()
@@ -112,6 +114,7 @@ const topMenuApps = ref([])
const appsOrder = ref([])
const mainAppList = ref([])
const popoverAppList = ref([])
const activeApp = ref(null)
let resizeTimeout = null
const setApps = (value) => {
@@ -130,19 +133,27 @@ const setApps = (value) => {
const appLimit = () => {
const headerStart = document.querySelector('#header .header-start')
const headerEnd = document.querySelector('#header .header-end')
const unifiedSearch = document.querySelector('#header .unified-search-input')
const body = document.querySelector('body')
let size = (headerEnd ? headerEnd.offsetWidth : 0) + 70
let totalWidth = body.offsetWidth
let otherElementsWidth = headerEnd ? headerEnd.offsetWidth : 0
if (unifiedSearch && headerEnd.offsetWidth < unifiedSearch.offsetWidth) {
otherElementsWidth += unifiedSearch.offsetWidth
}
let maxWidth = body.offsetWidth - otherElementsWidth
if (headerStart) {
Array.from(headerStart.children).forEach((child) => {
if (child.id !== 'app-menu-container') {
size += child.offsetWidth
maxWidth -= child.offsetWidth
}
})
}
return Math.floor((body.offsetWidth - size) / 70)
return Math.max(0, Math.floor(maxWidth / 60))
}
const makeStyle = (app) => {
@@ -154,8 +165,9 @@ const makeStyle = (app) => {
}
const computeLists = () => {
mainAppList.value = appList.value.slice(0, appLimit())
popoverAppList.value = appList.value.slice(appLimit()).sort((a, b) => a.order - b.order)
const limit = appLimit()
mainAppList.value = appList.value.slice(0, limit)
popoverAppList.value = appList.value.slice(limit).sort((a, b) => a.order - b.order)
}
onMounted(async () => {
@@ -165,14 +177,12 @@ onMounted(async () => {
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', () => {
window.clearTimeout(resizeTimeout)
resizeTimeout = window.setTimeout(computeLists, 100)
})
window.setInterval(computeLists, 100)
})
</script>
+3 -1
View File
@@ -31,7 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:label="settings.name"
:avatar="settings.avatar"
/>
<AppSearch v-model="search" />
<AppSearch v-if="showSearchForm" v-model="search" />
<OpenerButton
v-if="!openerHover"
@click="$emit('close')"
@@ -107,6 +107,7 @@ const activeApp = ref(null)
const targetBlankApps = ref([])
const settings = ref(null)
const search = ref('')
const showSearchForm = ref(true)
const openerHover = ref(false)
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
@@ -125,6 +126,7 @@ onMounted(async () => {
targetBlankApps.value = config['target-blank-apps']
settings.value = config['settings']
showSearchForm.value = config['show-search-form']
openerHover.value = config['opener-hover'] && !isTouchDevice
items.value = await navStore.getCategories()
+15
View File
@@ -29,6 +29,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</NcAppNavigation>
<NcAppContent class="cm-settings cm-settings--nav">
<button @click="toggleNav" class="cm-settings-nav-toggle">{{ trans('Toggle menu') }}</button>
<SettingsSection :hidden="section !== 'panel'">
<SectionTitle label="Panel" />
@@ -80,6 +82,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<FormYesNo v-model="config['show-settings']" />
</SettingValue>
</SettingItem>
<SettingItem :disabled="config['always-displayed']">
<SettingLabel
label="Show the search form"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['show-search-form']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Icons"
@@ -619,6 +630,10 @@ const filterConfig = (value) => {
return result
}
const toggleNav = () => {
document.querySelector('.cm-settings-nav .app-navigation-toggle-wrapper > button').click()
}
onMounted(async () => {
config.value = await configStore.getAppConfig()
+16 -5
View File
@@ -15,14 +15,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
.cm-settings {
&--nav {
padding-top: 30px;
}
.cm-settings-nav {
padding-top: 30px !important;
}
.cm-settings-nav-toggle {
margin-top: 40px !important;
margin-left: 15px !important;
}
.cm-settings-nav .app-navigation-toggle-wrapper > button {
display: none;
}
.cm-settings {
&-nav {
max-width: 50vw !important;
.app-navigation__content {
padding: 20px;
padding: 20px 10px 20px 10px;
}
.app-navigation-entry-icon {
+58 -22
View File
@@ -287,14 +287,6 @@
}
}
.cm-standardmenu {
visibility: hidden;
&.show {
visibility: visible;
}
}
.cm-always-displayed {
body {
width: calc(100% - 50px) !important;
@@ -363,6 +355,16 @@
right: 0 !important;
margin-left: 0 !important;
}
#content {
left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px);
}
#content-vue {
width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px;
}
}
@media screen and (max-width: 1024px) {
@@ -385,6 +387,16 @@
padding: 10px 0;
}
}
#header {
.unified-search-input {
width: 80px !important;
.unified-search-input__label {
display: none;
}
}
}
}
@media screen and (min-width: 1024px) {
@@ -406,11 +418,25 @@
$header-icon-size: 20px;
.cm-standardmenu {
visibility: hidden;
width: 100%;
display: flex;
flex-shrink: 1;
flex-wrap: wrap;
&.show {
visibility: visible;
}
svg {
color: var(--color-background-plain-text) !important;
}
.app-menu-entry--label {
color: var(--color-background-plain-text) !important;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright)) !important;
}
.app-menu-main {
display: flex;
flex-wrap: nowrap;
@@ -470,7 +496,7 @@ $header-icon-size: 20px;
color: var(--color-primary-text);
text-align: center;
left: 50%;
top: 45%;
top: 55%;
display: block;
min-width: 100%;
transform: translateX(-50%);
@@ -542,23 +568,27 @@ $header-icon-size: 20px;
}
}
.app-menu-more .button-vue--vue-tertiary {
opacity: 0.7;
margin: 8px 3px 3px 3px;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
&-app-menu-more {
padding-top: 9px !important;
&:not([aria-expanded='true']) {
color: var(--color-main-text);
.button-vue--vue-tertiary {
opacity: 0.7;
margin: 8px 3px 3px 3px;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
&:hover {
opacity: 1;
background-color: transparent !important;
&:not([aria-expanded='true']) {
color: var(--color-main-text);
&:hover {
opacity: 1;
background-color: transparent !important;
}
}
}
&:focus-visible {
opacity: 1;
outline: none !important;
&:focus-visible {
opacity: 1;
outline: none !important;
}
}
}
@@ -599,3 +629,9 @@ $header-icon-size: 20px;
display: none;
}
}
#header {
.unified-search-input:not(.unified-search-input--mobile) {
position: unset !important;
}
}
+1 -4
View File
@@ -41,10 +41,7 @@ export const useNavStore = defineStore('nav', () => {
async function getCoreApps() {
if (coreApps == null) {
coreApps = await axios
.get(generateOcsUrl('core/navigation', 2) + '/apps?format=json')
.then((response) => response.data)
.then((value) => value.ocs.data)
coreApps = await await axios.get(generateUrl('/apps/side_menu/core/apps')).then((response) => response.data.items)
}
return coreApps
+2
View File
@@ -17,6 +17,8 @@
import './scss/admin.scss'
import '@formatjs/intl-segmenter/polyfill.js'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
+1 -13
View File
@@ -38,7 +38,7 @@ body[data-theme-light], body[data-theme-light-highcontrast] {
<?php if ($_['opener-only']) { ?>
#nextcloud {
display: none;
display: none !important;
}
<?php } ?>
@@ -117,15 +117,3 @@ body[data-theme-light], body[data-theme-light-highcontrast] {
font-size: 16px;
}
<?php } ?>
<?php if ($_['always-displayed']) { ?>
#content {
left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px);
}
#content-vue {
width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px;
}
<?php } ?>
+6
View File
@@ -7,6 +7,7 @@ const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const appName = 'side_menu'
const appVersion = '6.x'
const buildMode = process.env.NODE_ENV
const isDev = buildMode === 'development'
@@ -77,6 +78,11 @@ module.exports = {
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new webpack.DefinePlugin({
appName: JSON.stringify(appName),
appVersion: JSON.stringify(appVersion),
}),
],
resolve: {