Merge pull request 'migration from vue2 to vue3' (#405) from feature/vue3 into develop
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/push/security Pipeline is pending approval
ci/woodpecker/pr/build Pipeline is pending approval
ci/woodpecker/pr/security Pipeline is pending approval

Reviewed-on: #405
This commit is contained in:
Simon Vieille 2025-04-16 19:56:02 +02:00
commit 15cc6a129b
102 changed files with 6272 additions and 5214 deletions

View file

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

2
.gitignore vendored
View file

@ -5,3 +5,5 @@
/package-lock.json /package-lock.json
!/l10n/.gitkeep !/l10n/.gitkeep
/yarn*.log /yarn*.log
/src/admin.js.bk
/templates/settings/admin-form.php.bk

8
.prettierrc.json Normal file
View file

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

View file

@ -62,5 +62,5 @@ steps:
api_key: api_key:
from_secret: gitnet_api_key from_secret: gitnet_api_key
base_url: https://gitnet.fr base_url: https://gitnet.fr
note: ${CI_COMMIT_MESSAGE} # note: ${CI_COMMIT_MESSAGE}
files: /var/www/html/artifacts/deblan/side_menu/${CI_COMMIT_TAG/v//}/* files: /var/www/html/artifacts/deblan/side_menu/${CI_COMMIT_TAG/v//}/*

View file

@ -1,5 +1,23 @@
## [Unreleased] ## [Unreleased]
## 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 ## 4.1.1
### Fixed ### Fixed
* fix(CssController): add missing NoCSRFRequired import (#397) * fix(CssController): add missing NoCSRFRequired import (#397)
@ -47,9 +65,9 @@
### Added ### Added
* update translations * update translations
* update ci steps names * update ci steps names
* fully apply Nextcloud AppMenu.vue updates
### Fixed ### Fixed
* add accessibility to open and close buttons (#311) * add accessibility to open and close buttons (#311)
* fully apply Nextcloud AppMenu.vue updated (#326)
* add missing label on the 'save' button in personal settings (fix #318) * add missing label on the 'save' button in personal settings (fix #318)
### Changed ### Changed
* upgrade axios * upgrade axios

View file

@ -6,7 +6,6 @@ watch: dep
dep: dep:
npm i npm i
npm link @nextcloud/vue || sudo npm link @nextcloud/vue
.ONESHELL: .ONESHELL:
release: release:

View file

@ -10,14 +10,13 @@ This application is rather suitable for instances that activate a lot of applica
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate. Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
You can customize colors depending of the theme (Dark theme and Breeze Dark). You can customize colors depending of the theme.
You can report a bug or request a feature by opening an issue. To report a bug or request a feature, please open an issue.
Requirements: Requirements:
* PHP >= 8.1 * PHP >= 8.1
* 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:
@ -31,7 +30,7 @@ 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>4.1.1</version> <version>5.0.0</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.fr/">Simon Vieille</author>
<namespace>SideMenu</namespace> <namespace>SideMenu</namespace>
@ -54,7 +53,7 @@ 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_default_menu.png]]></screenshot> <screenshot><![CDATA[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" /> <php min-version="8.1" max-version="8.4" />
<nextcloud min-version="30" max-version="32"/> <nextcloud min-version="31" max-version="32"/>
</dependencies> </dependencies>
<settings> <settings>
<admin>OCA\SideMenu\Settings\Admin</admin> <admin>OCA\SideMenu\Settings\Admin</admin>

View file

@ -31,6 +31,7 @@ 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,220 +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/>.
*/
#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;
}

View file

@ -1,386 +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/>.
*/
#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;
overflow: hidden;
}
.side-menu-opener span {
position: relative;
left: 50px;
display: block;
width: 1px;
height: 1px;
overflow: hidden;
}
.side-menu-opener:active, .side-menu-opener:focus {
background-color: var(--side-menu-current-app-background-color, #444) !important;
}
.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);
font-weight: bold;
font-size: 20px;
margin-bottom: 12px;
line-height: 30px;
margin-top: 0;
}
.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-toggle-wrapper {
right: 0 !important;
margin-left: 0 !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

@ -8,7 +8,9 @@ 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;
@ -81,6 +83,12 @@ class Application extends App implements IBootstrap
$c->get(IConfig::class), $c->get(IConfig::class),
); );
}); });
$context->registerService(Color::class, function (ContainerInterface $c) {
return new Color(
$c->get(ThemingDefaults::class),
);
});
} }
public function boot(IBootContext $context): void public function boot(IBootContext $context): void
@ -119,8 +127,7 @@ class Application extends App implements IBootstrap
protected function addAssets() protected function addAssets()
{ {
Util::addScript(self::APP_ID, 'sideMenu'); Util::addScript(self::APP_ID, 'side_menu-menu');
Util::addStyle(self::APP_ID, 'sideMenu');
$assets = [ $assets = [
'stylesheet' => [ 'stylesheet' => [
@ -131,14 +138,6 @@ 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');

View file

@ -20,10 +20,14 @@
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\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; 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\IConfig; use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
@ -35,7 +39,10 @@ class AdminSettingController extends Controller
$appName, $appName,
IRequest $request, IRequest $request,
protected IConfig $config, protected IConfig $config,
protected IURLGenerator $urlGenerator protected ConfigProxy $configProxy,
protected IURLGenerator $urlGenerator,
protected Color $color,
protected LangRepository $langRepository,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }
@ -60,6 +67,7 @@ class AdminSettingController extends Controller
$excludedKeys = [ $excludedKeys = [
'cache', 'cache',
'cache-categories', 'cache-categories',
'langs',
]; ];
foreach ($keys as $key) { foreach ($keys as $key) {
@ -76,4 +84,135 @@ 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

@ -37,7 +37,7 @@ class AppController extends Controller
IRequest $request, IRequest $request,
protected AppRepository $appRepository, protected AppRepository $appRepository,
protected IURLGenerator $urlGenerator, protected IURLGenerator $urlGenerator,
protected ConfigProxy $config protected ConfigProxy $config,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }

View file

@ -41,7 +41,7 @@ class CssController extends Controller
IRequest $request, IRequest $request,
protected ConfigProxy $config, protected ConfigProxy $config,
protected ThemingDefaults $theming, protected ThemingDefaults $theming,
protected Color $color protected Color $color,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
@ -66,10 +66,6 @@ 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', '[]');
@ -81,81 +77,62 @@ 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;
} }
$isDarkMode = ($isAccessibilityAppEnabled && $isDarkThemeUserEnabled) $lightenPrimaryColor = $this->color->getLightenPrimaryColor();
|| ($isBreezeDarkAppEnabled && $isBreezeDarkUserEnabled); $darkenPrimaryColor = $this->color->getDarkenPrimaryColor();
$darkenPrimaryColor2 = $this->color->getDarkenPrimaryColor2();
$textColor = $this->color->getTextColorPrimary();
$primaryColor = $this->theming->getColorPrimary(); $backgroundColor = $this->config->getAppValue('background-color', $darkenPrimaryColor);
$lightenPrimaryColor = $this->color->adjustBrightness($primaryColor, 0.2); $backgroundColorTo = $this->config->getAppValue('background-color-to', $darkenPrimaryColor);
$darkenPrimaryColor = $this->color->adjustBrightness($primaryColor, -0.2); $opacity = $this->config->getAppValueInt('background-color-opacity', '100');
$darkenPrimaryColor2 = $this->color->adjustBrightness($primaryColor, -0.3); $backgroundOpacity = dechex($opacity * 255 / 100);
$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' => [
'background-color' => $backgroundColor, 'light' => [
'background-color-to' => $backgroundColorTo, 'background-color' => $backgroundColor,
'current-app-background-color' => $currentAppBackgroundColor, 'background-color-to' => $backgroundColorTo,
'loader-color' => $loaderColor, 'current-app-background-color' => $this->config->getAppValue(
'text-color' => $textColor, 'current-app-background-color',
'opener' => $opener, $darkenPrimaryColor2
'icon-invert-filter' => $iconInvertFilter, ),
'icon-opacity' => $iconOpacity, 'loader-color' => $this->config->getAppValue('loader-color', $lightenPrimaryColor),
'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

@ -43,7 +43,7 @@ class JsController extends Controller
IRequest $request, IRequest $request,
protected ConfigProxy $config, protected ConfigProxy $config,
protected ThemingDefaults $themingDefaults, protected ThemingDefaults $themingDefaults,
protected IFactory $l10nFactory protected IFactory $l10nFactory,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
@ -54,18 +54,6 @@ class JsController extends Controller
$this->l10nFactory = $l10nFactory; $this->l10nFactory = $l10nFactory;
} }
#[NoCSRFRequired]
#[NoAdminRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'GET', url: '/js/script')]
public function script(): TemplateResponse
{
$response = new TemplateResponse(Application::APP_ID, 'js/script', $this->getConfig(), 'blank');
$response->addHeader('Content-Type', 'text/javascript');
return $response;
}
#[NoCSRFRequired] #[NoCSRFRequired]
#[NoAdminRequired] #[NoAdminRequired]
#[PublicPage] #[PublicPage]
@ -145,6 +133,8 @@ class JsController extends Controller
'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

@ -42,7 +42,7 @@ class NavController extends Controller
protected AppRepository $appRepository, protected AppRepository $appRepository,
protected CategoryRepository $categoryRepository, protected CategoryRepository $categoryRepository,
protected URLGenerator $router, protected URLGenerator $router,
protected IFactory $l10nFactory protected IFactory $l10nFactory,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }
@ -115,6 +115,7 @@ 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'],

View file

@ -25,6 +25,7 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; 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;
@ -36,14 +37,14 @@ class PersonalSettingController extends Controller
IRequest $request, IRequest $request,
protected IConfig $config, protected IConfig $config,
protected ConfigProxy $configProxy, protected ConfigProxy $configProxy,
protected IUserSession $userSession protected IUserSession $userSession,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }
#[NoCSRFRequired] #[NoCSRFRequired]
#[NoAdminRequired] #[NoAdminRequired]
#[FrontpageRoute(verb: 'POST', url: '/personalSetting/valueSet')] #[FrontpageRoute(verb: 'POST', url: '/user/valueSet')]
public function valueSet($name, $value): array public function valueSet($name, $value): array
{ {
$doSave = false; $doSave = false;
@ -65,22 +66,7 @@ class PersonalSettingController extends Controller
} }
} }
if ('target-blank-apps' === $name) { if (in_array($name, ['target-blank-apps', 'top-menu-apps', 'top-side-menu-apps', 'apps-order'])) {
$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);
@ -110,4 +96,62 @@ class PersonalSettingController extends Controller
return []; return [];
} }
#[NoCSRFRequired]
#[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

@ -20,8 +20,9 @@ class CategoryRepository
protected ConfigProxy $config, protected ConfigProxy $config,
protected IConfig $iConfig, protected IConfig $iConfig,
protected IFactory $l10nFactory, protected IFactory $l10nFactory,
protected IUserSession $userSession protected IUserSession $userSession,
) {} ) {
}
/** /**
* Retrieves categories. * Retrieves categories.

View file

@ -2,6 +2,8 @@
namespace OCA\SideMenu\Service; namespace OCA\SideMenu\Service;
use OCA\Theming\ThemingDefaults;
/** /**
* class Color. * class Color.
* *
@ -9,6 +11,10 @@ namespace OCA\SideMenu\Service;
*/ */
class Color class Color
{ {
public function __construct(protected ThemingDefaults $theming)
{
}
/** /**
* @thanks https://stackoverflow.com/posts/54393956/revision * @thanks https://stackoverflow.com/posts/54393956/revision
*/ */
@ -31,4 +37,29 @@ 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

@ -39,8 +39,9 @@ class Admin implements ISettings
protected CategoryRepository $categoryRepository, protected CategoryRepository $categoryRepository,
protected ThemingDefaults $theming, protected ThemingDefaults $theming,
protected Color $color, protected Color $color,
protected LangRepository $langRepository protected LangRepository $langRepository,
) {} ) {
}
/** /**
* @return TemplateResponse * @return TemplateResponse

View file

@ -28,8 +28,9 @@ class AdminSection implements IIconSection
{ {
public function __construct( public function __construct(
protected IURLGenerator $url, protected IURLGenerator $url,
protected IL10N $l protected IL10N $l,
) {} ) {
}
public function getID() public function getID()
{ {

View file

@ -33,8 +33,9 @@ class Personal implements ISettings
protected IL10N $l, protected IL10N $l,
protected ConfigProxy $config, protected ConfigProxy $config,
protected IUserSession $userSession, protected IUserSession $userSession,
protected AppRepository $appRepository protected AppRepository $appRepository,
) {} ) {
}
/** /**
* @return TemplateResponse * @return TemplateResponse

View file

@ -30,8 +30,9 @@ class PersonalSection implements IIconSection
public function __construct( public function __construct(
protected IURLGenerator $url, protected IURLGenerator $url,
protected IL10N $l, protected IL10N $l,
protected ConfigProxy $configProxy protected ConfigProxy $configProxy,
) {} ) {
}
public function getID() public function getID()
{ {

View file

@ -1,73 +1,54 @@
{ {
"license": "agpl", "license": "agpl",
"private": true, "private": true,
"scripts": { "module": true,
"build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.js", "scripts": {
"dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.js", "build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.config.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.js", "dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.config.js",
"lint": "./node_modules/.bin/eslint --ext .js,.vue src", "watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.config.js",
"lint:fix": "./node_modules/.bin/eslint --ext .js,.vue src --fix", "lint": "ESLINT_USE_FLAT_CONFIG=false ./node_modules/.bin/eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"stylelint": "./node_modules/.bin/stylelint src", "format": "./node_modules/.bin/prettier src --write"
"stylelint:fix": "./node_modules/.bin/stylelint src --fix" },
}, "dependencies": {
"dependencies": { "@babel/core": ">=7.12.0 <8.0.0",
"@nextcloud/axios": "^2.5.1", "@nextcloud/router": "^3.0.1",
"@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/vue": "^9.0.0-alpha.8",
"@nextcloud/event-bus": "^3.3.1", "node-polyfill-webpack-plugin": "^4.1.0",
"@nextcloud/initial-state": "^2.2.0", "pinia": "^3.0.1",
"@nextcloud/l10n": "^3.1.0", "postcss": "^7.0.0 || ^8.0.1",
"@nextcloud/vue": "^8.19.0", "vue": "^3.5.13",
"@vueuse/core": "^11.1.0", "vuedraggable": "^4.1.0"
"axios": "^1.6.7", },
"trim": "^1.0.1" "browserslist": [
}, "extends @nextcloud/browserslist-config"
"browserslist": [ ],
"extends @nextcloud/browserslist-config" "engines": {
], "node": ">=16.0.0"
"engines": { },
"node": ">=16.0.0" "devDependencies": {
}, "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"devDependencies": { "@nextcloud/axios": "^2.5.1",
"@babel/node": "^7.25.7", "@nextcloud/browserslist-config": "^3.0.1",
"@babel/plugin-transform-private-methods": "^7.25.7", "@nextcloud/event-bus": "^3.3.1",
"@babel/preset-typescript": "^7.24.7", "@nextcloud/initial-state": "^2.2.0",
"@cypress/vue2": "^2.1.1", "@nextcloud/l10n": "^3.2.0",
"@cypress/webpack-preprocessor": "^6.0.2", "babel-loader": "^9.1.3",
"@nextcloud/babel-config": "^1.2.0", "css-loader": "^7.1.2",
"@nextcloud/eslint-config": "^8.4.1", "eslint": "^9.19.0",
"@nextcloud/stylelint-config": "^3.0.1", "eslint-config-prettier": "^10.0.1",
"@nextcloud/typings": "^1.9.1", "eslint-plugin-vue": "^9.32.0",
"@nextcloud/webpack-vue-config": "^6.0.1", "file-loader": "^6.2.0",
"@simplewebauthn/types": "^10.0.0", "mini-css-extract-plugin": "^2.9.1",
"@types/dockerode": "^3.3.29", "postcss-loader": "^8.1.1",
"@types/wait-on": "^5.3.4", "prettier": "3.4.2",
"@vue/tsconfig": "^0.5.1", "sass": "^1.78.0",
"babel-loader": "^9.2.1", "sass-loader": "^16.0.1",
"babel-loader-exclude-node-modules-except": "^1.2.1", "source-map-loader": "^5.0.0",
"babel-plugin-module-resolver": "^5.0.2", "style-loader": "^4.0.0",
"colord": "^2.9.3", "vue-loader": "^17.4.2",
"eslint-plugin-cypress": "^3.5.0", "vue-router": "^4.4.5",
"eslint-plugin-es": "^4.1.0", "webpack": "^5.94.0",
"exports-loader": "^5.0.0", "webpack-cli": "^5.1.4",
"file-loader": "^6.2.0", "webpack-notifier": "^1.15.0"
"handlebars-loader": "^1.7.3", }
"jasmine-core": "~2.5.2",
"jasmine-sinon": "^0.4.0",
"jsdoc": "^4.0.2",
"raw-loader": "^4.0.2",
"sass": "^1.79.3",
"stylelint": "^16.9.0",
"stylelint-use-logical": "^2.1.2",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.7.16",
"wait-on": "^8.0.1",
"webpack": "^5.94.0",
"webpack-cli": "^5.0.2",
"webpack-merge": "^6.0.1",
"workbox-webpack-plugin": "^7.1.0"
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Before After
Before After

View file

@ -1,181 +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>
<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.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
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>

View file

@ -1,393 +0,0 @@
<!--
- @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"
:aria-label="t('core', 'Applications menu')"
>
<ul
class="app-menu-main"
:class="{ 'app-menu-main__hidden-label': hiddenLabels === 1, 'app-menu-main__show-hovered': hiddenLabels === 2 }"
v-if="appList.length"
>
<li v-for="app in mainAppList(state)"
: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')">
<NcActionLink v-for="app in popoverAppList(state)"
: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 { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { n, t } from '@nextcloud/l10n'
import { useElementSize } from '@vueuse/core'
import { defineComponent, ref } from 'vue'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
export default defineComponent({
name: 'AppMenu',
components: {
NcActions,
NcActionLink,
},
setup() {
return {
t,
n,
}
},
data() {
return {
apps: null,
appList: [],
observer: null,
targetBlankApps: [],
hiddenLabels: true,
state: 1,
}
},
mounted() {
axios.get(generateOcsUrl('core/navigation', 2) + '/apps?format=json')
.then((response) => response.data)
.then((data) => {
if (data.ocs.meta.statuscode !== 200) {
return
}
this.setApps(data.ocs.data)
})
this.targetBlankApps = window.targetBlankApps
this.hiddenLabels = window.topMenuAppsMouseOverHiddenLabel
let timeout = null
window.addEventListener('resize', () => {
timeout = window.setTimeout(() => {
this.update()
}, 300)
})
},
methods: {
update() {
++this.state
},
mainAppList() {
return this.appList.slice(0, this.appLimit())
},
popoverAppList() {
return this.appList.slice(this.appLimit())
},
appLimit() {
const maxApps = Math.floor(this.$root.$el.offsetWidth / 60)
if (maxApps < this.appList.length) {
// Ensure there is space for the overflow menu
return Math.max(maxApps - 1, 0)
}
return maxApps
},
setNavigationCounter(id, counter) {
const app = this.appList.find(({ app }) => app === id)
if (app) {
this.$set(app, 'unread', counter)
} else {
logger.warn(`Could not find app "${id}" for setting navigation count`)
}
},
setApps(apps) {
this.appList = []
let orders = {}
window.menuAppsOrder.forEach((app, order) => {
orders[app] = order + 1
})
apps.forEach((app) => {
Array.from(window.topMenuApps).forEach((id) => {
if (app.id === id) {
app.order = orders[id] || null
this.appList.push(app)
}
})
})
},
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 }) + ')' : '')
},
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;
&.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);
box-sizing: content-box;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
}
.app-menu-entry--label {
opacity: 0;
position: absolute;
font-size: 12px;
color: var(--color-primary-text);
text-align: center;
left: 50%;
top: 45%;
display: block;
min-width: 100%;
transform: translateX(-50%);
transition: all 0.1s ease-in-out;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
letter-spacing: -0.5px;
}
&: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: bolder;
bottom: 0;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
// 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 {
opacity: 1;
img {
margin-top: -8px;
}
.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: -8px;
}
.app-menu-entry--label {
opacity: 1;
bottom: 0;
}
&::before, .app-menu-entry::before {
opacity: 0;
}
}
}
::v-deep .app-menu-more .button-vue--vue-tertiary {
opacity: .7;
margin: 3px;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
&:not([aria-expanded="true"]) {
color: var(--color-main-text);
&:hover {
opacity: 1;
background-color: transparent !important;
}
}
&:focus-visible {
opacity: 1;
outline: none !important;
}
}
.app-menu-popover-entry {
.app-icon {
position: relative;
height: 44px;
width: 48px;
display: flex;
align-items: center;
justify-content: center;
filter: var(--background-invert-if-bright, var(--primary-invert-if-bright));
&.has-unread::after {
background-color: var(--color-main-text);
}
img {
width: $header-icon-size;
height: $header-icon-size;
}
}
}
.has-unread::after {
content: "";
width: 8px;
height: 8px;
background-color: var(--color-primary-element-text);
border-radius: 50%;
position: absolute;
display: block;
top: 10px;
right: 10px;
}
.unread-counter {
display: none;
}
</style>

View file

@ -1,42 +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="side-menu-loader">
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<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>
</template>
<script>
export default {
name: 'Loader',
}
</script>

View file

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

@ -1,69 +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 Vue from 'vue'
import AppMenu from './AppMenu.vue'
import SideMenu from './SideMenu.vue'
import SideMenuBig from './SideMenuBig.vue'
import SideMenuWithCategories from './SideMenuWithCategories.vue'
import PageLoader from './PageLoader'
Vue.prototype.OC = OC
Vue.prototype.t = OC.L10N.translate
window.PageLoader = PageLoader
const mountSideMenuComponent = () => {
const 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()

View file

@ -1,170 +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 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 v-if="!alwaysDisplayed" />
<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="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 app of ncApps) {
if (window.topMenuApps.includes(app.id) && !window.topSideMenuApps.includes(app.id)) {
continue
}
if (this.hiddenApps.includes(app.id)) {
continue
}
app.order = orders[app.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

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

@ -1,152 +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 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,275 +8,25 @@
* *
* 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 AdminCategoriesCustom from './AdminCategoriesCustom.vue' import './scss/admin.scss'
import Vue from 'vue'
Vue.prototype.OC = window.OC import { createApp } from 'vue'
Vue.prototype.OCA = window.OCA import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
let elements = [] import AdminSettings from './pages/AdminSettings'
const selector = '#side-menu-message' waitContainer('#side-menu-admin-settings').then((selector) => {
const pinia = createPinia()
const userConfig = (name, value, callbacks) => { const app = createApp(AdminSettings)
const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet') app.use(pinia)
const formData = [] app.mixin({ methods: { t, n } })
app.mount(selector)
formData.push('name=' + encodeURIComponent(name))
formData.push('value=' + encodeURIComponent(value))
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.join('&')
})
.then(callbacks.success)
.catch(callbacks.error)
}
const appConfig = (name, value, callbacks) => {
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

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

@ -15,18 +15,15 @@ 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 class="side-menu-opener side-menu-closer" :arial-label="label"> <button
<span v-text="label"></span> 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> </button>
</template> </template>
<script> <script setup>
export default { defineEmits(['click'])
name: 'CloserButton',
data() {
return {
label: t('side_menu', 'Close the menu'),
}
}
}
</script> </script>

View file

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

@ -15,18 +15,15 @@ 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 class="side-menu-opener" :arial-label="label"> <button
<span v-text="label"></span> class="cm-opener"
:arial-label="label"
@click="$emit('click')"
>
<span>{{ t('side_menu', 'Toggle the menu') }}</span>
</button> </button>
</template> </template>
<script> <script setup>
export default { defineEmits(['click'])
name: 'OpenerButton',
data() {
return {
label: t('side_menu', 'Toggle the menu'),
}
}
}
</script> </script>

View file

@ -0,0 +1,49 @@
<!--
@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-loader">
<div
class="cm-loader-bar"
:style="createStyle(width)"
></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
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>

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="side-menu-settings"> <div class="cm-setting">
<a v-bind:href="href"> <a :href="href">
<!-- <!--
{{ label }} {{ label }}
--> -->
<span class="avatardiv avatardiv-shown"> <span class="avatardiv avatardiv-shown">
<img v-bind:src="avatar" :alt="label"> <img
:src="avatar"
:alt="label"
/>
</span> </span>
</a> </a>
</div> </div>
</template> </template>
<script> <script setup>
export default { const { label, href, avatar } = defineProps({
name: 'SettingsButton', label: {
props: { type: String,
label: { required: true,
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>

View file

@ -15,38 +15,44 @@ 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 v-bind:class="classes"> <li :class="classes">
<a v-bind:href="href" :target="target" v-bind:title="label"> <a
<img class="side-menu-app-icon" v-bind:src="icon" v-bind:alt="label" /> :href="href"
<span class="side-menu-app-text" v-text="label"></span> :target="target"
: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> <script setup>
export default { const { label, icon, href, classes, target } = defineProps({
name: 'SideMenuApp', label: {
props: { type: String,
label: { required: true,
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

@ -15,38 +15,44 @@ 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 v-bind:class="classes"> <li :class="classes">
<a v-bind:href="href" :target="target" v-bind:title="label"> <a
<img class="side-menu-app-icon" v-bind:src="icon" v-bind:alt="label" /> :href="href"
<span class="side-menu-app-text" v-text="label"></span> :target="target"
: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> <script setup>
export default { const { label, icon, href, classes, target } = defineProps({
name: 'SideMenuBigApp', label: {
props: { type: String,
label: { required: true,
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,103 @@
<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, {
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

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

View file

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

@ -15,30 +15,26 @@ 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 v-bind:class="classes"> <div
<a v-if="link !== null" v-bind:href="link"> class="cm-settings-item"
<img v-bind:src="image" alt="Logo"> :class="{ 'cm-settings-item--disabled': disabled }"
</a> >
<img v-else v-bind:src="image" alt="Logo"> <slot></slot>
</div> </div>
</template> </template>
<script> <script setup>
export default { const { disabled } = defineProps({
name: 'Logo', disabled: {
props: { type: Boolean,
image: { required: false,
type: String, default: false,
required: true
},
link: {
type: String,
required: false
},
classes: {
type: Object,
required: true
},
}, },
} })
</script> </script>
<style scoped>
.disabled {
display: block;
}
</style>

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/>.
-->
<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

@ -15,18 +15,20 @@ 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="side-menu-search"> <div
<input type="text" :value="value" :placeholder="t('side_menu', 'Search')" @input="$emit('input', $event.target.value)"> class="side-menu-setting-form"
:class="{ 'side-menu-setting-form-long': long }"
>
<slot></slot>
</div> </div>
</template> </template>
<script> <script setup>
export default { const { long } = defineProps({
name: 'AppSearch', long: {
props: { type: Boolean,
value: { required: false,
required: true default: false,
},
}, },
} })
</script> </script>

View file

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

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

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

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

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

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

@ -0,0 +1,33 @@
<!--
@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"
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

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

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

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

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

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

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

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

@ -0,0 +1,10 @@
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,96 +1,111 @@
"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 <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.' '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.'
: '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>.' 'Top menu': 'Horní nabídka'
"Top menu": "Horní nabídka" 'Apps that not must be moved in the side menu': 'Aplikace, které nepřesouvat do postranní nabídky'
"Apps that not must be moved in the side menu": "Aplikace, které nepřesouvat do postranní nabídky" 'If there is no selection then the global configuration is applied.': 'Pokud neexistuje žádný výběr, je uplatněno globální nastavení.'
"If there is no selection then the global configuration is applied.": "Pokud neexistuje žádný výběr, je uplatněno globální nastavení." 'Experimental': 'Experimentální'
"Experimental": "Experimentální" 'Save': 'Uložit'
"Save": "Uložit" 'You like this app and you want to support me?': 'Líbí se vám tato aplikace a chcete podpořit její vývoj?'
"You like this app and you want to support me?": "Líbí se vám tato aplikace a chcete podpořit její vývoj?" 'Buy me a coffee ☕': 'Kupte mi kafe ☕'
"Buy me a coffee ☕": "Kupte mi kafe ☕" 'Hidden': 'Skryté'
"Hidden": "Skryté" 'Small': 'Malé'
"Small": "Malé" 'Normal': 'Normální'
"Normal": "Normální" 'Big': 'Velké'
"Big": "Velké" 'Colors': 'Barvy'
"Colors": "Barvy" 'Background color': 'Barva pozadí'
"Background color": "Barva pozadí" 'Background color of current app': 'Barva pozadí stávající aplikace'
"Background color of current app": "Barva pozadí stávající aplikace" 'Text color': 'Barva textu'
"Text color": "Barva textu" 'Loader': 'Nástroj pro načítání'
"Loader": "Nástroj pro načítání" 'Icon': 'Ikona'
"Icon": "Ikona" 'Same color': 'Stejná barva'
"Same color": "Stejná barva" 'Opposite color': 'Doplňková barva'
"Opposite color": "Doplňková barva" 'Transparent': 'Průhledné'
"Transparent": "Průhledné" 'Opaque': 'Neprůhledné'
"Opaque": "Neprůhledné" 'Opener': 'Tlačítko pro otevření'
"Opener": "Tlačítko pro otevření" 'Default': 'Výchozí'
"Default": "Výchozí" 'Default (dark)': 'Výchozí (tmavé)'
"Default (dark)": "Výchozí (tmavé)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (tmavé)'
"Hamburger (dark)": "Hamburger (tmavé)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (tmavé)'
"Hamburger 2 (dark)": "Hamburger 2 (tmavé)" 'Before the logo': 'Před logem'
"Before the logo": "Před logem" 'After the logo': 'Za logem'
"After the logo": "Za logem" 'Position': 'Pozice'
"Position": "Pozice" 'Show only the opener (hidden logo)': 'Zobrazovat pouze otevírací tlačítko (logo skryto)'
"Show only the opener (hidden logo)": "Zobrazovat pouze otevírací tlačítko (logo skryto)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Nezobrazovat postranní nabídku a její otevírací tlačítko pokud nejsou dostupné žádné aplikace (např. na veřejných stránkách).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Nezobrazovat postranní nabídku a její otevírací tlačítko pokud nejsou dostupné žádné aplikace (např. na veřejných stránkách)." 'Panel': 'Panel'
"Panel": "Panel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Otevřít nabídku při najetím ukazatelem na tlačítko nabídky (automaticky vypnuto pro dotykové obrazovky)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Otevřít nabídku při najetím ukazatelem na tlačítko nabídky (automaticky vypnuto pro dotykové obrazovky)" 'Display the big menu': 'Zobrazit velkou nabídku'
"Display the big menu": "Zobrazit velkou nabídku" 'Display the logo': 'Zobrazit logo'
"Display the logo": "Zobrazit logo" 'Icons and texts': 'Ikony a texty'
"Icons and texts": "Ikony a texty" 'Loader enabled': 'Načítání zapnuto'
"Loader enabled": "Načítání zapnuto" 'Tips': 'Tipy'
"Tips": "Tipy" 'Always displayed': 'Vždy zobrazeno'
"Always displayed": "Vždy zobrazeno" 'This is the automatic behavior when the menu is always displayed.': 'Toto je automatické chování, kdy je nabídka vždy zobrazena.'
"This is the automatic behavior when the menu is always displayed.": "Toto je automatické chování, kdy je nabídka vždy zobrazena." 'Not compatible with touch screens.': 'Nekompatibilní s dotykovými obrazovkami.'
"Not compatible with touch screens.": "Nekompatibilní s dotykovými obrazovkami." 'Big menu': 'Velká nabídka'
"Big menu": "Velká nabídka" 'Live preview': 'Živý náhled'
"Live preview": "Živý náhled" 'Open apps in new tab': 'Otevírat aplikace v novém panelu'
"Open apps in new tab": "Otevírat aplikace v novém panelu" 'Use the global setting': 'Použít globální nastavení'
"Use the global setting": "Použít globální nastavení" 'Use my selection': 'Použít můj výběr'
"Use my selection": "Použít můj výběr" 'Show and hide the list of applications': 'Zobrazit/skrýt seznam aplikací'
"Show and hide the list of applications": "Zobrazit/skrýt seznam aplikací" 'Use the avatar instead of the logo': 'Použít namísto loga profilový obrázek uživatele'
"Use the avatar instead of the logo": "Použít namísto loga profilový obrázek uživatele" 'You do not have permission to change the settings.': 'Nemáte oprávnění měnit nastavení.'
"You do not have permission to change the settings.": "Nemáte oprávnění měnit nastavení." 'Force this configuration to users': 'Vynutit uplatnění těchto nastavení uživatelům'
"Force this configuration to users": "Vynutit uplatnění těchto nastavení uživatelům" 'Export the configuration': 'Exportovat nastavení'
"Export the configuration": "Exportovat nastavení" 'Purge the cache': 'Vyprázdnit mezipaměť'
"Purge the cache": "Vyprázdnit mezipaměť" 'Show the link to settings': 'Zobrazit odkaz na nastavení'
"Show the link to settings": "Zobrazit odkaz na nastavení" 'The menu is enabled by default for users': 'Nabídka je ve výchozím stavu pro uživatele zapnutá'
"The menu is enabled by default for users": "Nabídka je ve výchozím stavu pro uživatele zapnutá" 'Except when the configuration is forced.': 'S výjimkou, kdy je nastavení vynuceno.'
"Except when the configuration is forced.": "S výjimkou, kdy je nastavení vynuceno." 'Apps that should not be displayed in the menu': 'Aplikace, které by neměly být v nabídce zobrazeny'
"Apps that should not be displayed in the menu": "Aplikace, které by neměly být v nabídce zobrazeny" 'This feature is only compatible with the <code>big menu</code> display.': 'Tato funkce je kompatibilní pouze s <code>velkou nabídkou</code>.'
"This feature is only compatible with the <code>big menu</code> display.": "Tato funkce je kompatibilní pouze s <code>velkou nabídkou</code>." 'The logo is a link to the default app': 'Logo je odkaz na výchozí aplikaci'
"The logo is a link to the default app": "Logo je odkaz na výchozí aplikaci" 'Others': 'Ostatní'
"Others": "Ostatní" 'Categories': 'Kategorie'
"Categories": "Kategorie" 'Customize sorting': 'Přizpůsobit si řazení'
"Customize sorting": "Přizpůsobit si řazení" 'Order by': 'Řadit podle'
"Order by": "Řadit podle" 'Name': 'Název'
"Name": "Název" 'Customed': 'Přizpůsobeno'
"Customed": "Přizpůsobeno" 'Show and hide the list of categories': 'Zobrazit/skrýt seznam kategorií'
"Show and hide the list of categories": "Zobrazit/skrýt seznam kategorií" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Tyto parametry jsou použity v případě, že je zapnutý (Breeze) tmavý motiv vzhledu.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Tyto parametry jsou použity v případě, že je zapnutý (Breeze) tmavý motiv vzhledu." 'Dark mode colors': 'Barvy tmavého režimu'
"Dark mode colors": "Barvy tmavého režimu" 'With categories': 'S kategoriemi'
"With categories": "S kategoriemi" 'Custom categories': 'Vlastní kategorie'
"Custom categories": "Vlastní kategorie" 'Customize application categories': 'Přizpůsobte kategorie aplikací'
"Customize application categories": "Přizpůsobte kategorie aplikací" 'Reset to default': 'Vrátit zpět na výchozí hodnoty'
"Reset to default": "Vrátit zpět na výchozí hodnoty" 'Hidden icon': 'Skrytá ikona'
"Hidden icon": "Skrytá ikona" 'Small icon': 'Malá ikona'
"Small icon": "Malá ikona" 'Normal icon': 'Normální ikona'
"Normal icon": "Normální ikona" 'Big icon': 'Velká ikona'
"Big icon": "Velká ikona" 'Hidden text': 'Skrytý text'
"Hidden text": "Skrytý text" 'Small text': 'Malý text'
"Small text": "Malý text" 'Normal text': 'Normální text'
"Normal text": "Normální text" 'Big text': 'Velký text'
"Big text": "Velký text" 'Applications': 'Aplikace'
"Applications": "Aplikace" 'Applications kept in the top menu': 'Aplikace ponechané v horní nabídce'
"Applications kept in the top menu": "Aplikace ponechané v horní nabídce" 'Applications kept in the top menu but also shown in side menu': 'Aplikace ponechané v horní nabídce ale také zobrazené v té boční'
"Applications kept in the top menu but also shown in side menu": "Aplikace ponechané v horní nabídce ale také zobrazené v té boční" 'These applications must be selected in the previous option.': 'Tyto aplikace je třeba vybrat v předchozí volbě.'
"These applications must be selected in the previous option.": "Tyto aplikace je třeba vybrat v předchozí volbě." 'Hide labels on mouse over': 'Skrýt popisky při najetím ukazatele myši'
"Hide labels on mouse over": "Skrýt popisky při najetím ukazatele myši" 'Except the hovered app': 'S výjimkou nadnášené aplikace'
"Except the hovered app": "Except the hovered app" 'Search': 'Hledat'
"Search": "Search" 'Toggle the menu': 'Vyp/zap nabídku'
"Toggle the menu": "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'
'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,96 +1,111 @@
"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 <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.' '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.'
: '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.' 'Top menu': 'Obere Navigationsleiste'
"Top menu": "Obere Navigationsleiste" 'Apps that not must be moved in the side menu': 'Apps, die nicht ins Seitenmenü verschoben werden sollen'
"Apps that not must be moved in the side menu": "Apps, die nicht ins Seitenmenü verschoben werden sollen" 'If there is no selection then the global configuration is applied.': 'Wenn keine Auswahl vorhanden ist, wird die globale Konfiguration angewendet.'
"If there is no selection then the global configuration is applied.": "Wenn keine Auswahl vorhanden ist, wird die globale Konfiguration angewendet." 'Experimental': 'Experimentell'
"Experimental": "Experimentell" 'Save': 'Speichern'
"Save": "Speichern" 'You like this app and you want to support me?': 'Du magst diese App und möchtest mich unterstützen?'
"You like this app and you want to support me?": "Du magst diese App und möchtest mich unterstützen?" 'Buy me a coffee ☕': 'Gib mir einen Kaffee aus ☕'
"Buy me a coffee ☕": "Gib mir einen Kaffee aus ☕" 'Hidden': 'Ausblenden'
"Hidden": "Ausblenden" 'Small': 'Klein'
"Small": "Klein" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Groß'
"Big": "Groß" 'Colors': 'Farben'
"Colors": "Farben" 'Background color': 'Hintergrundfarbe'
"Background color": "Hintergrundfarbe" 'Background color of current app': 'Hintergrundfarbe der aktuellen App'
"Background color of current app": "Hintergrundfarbe der aktuellen App" 'Text color': 'Textfarbe'
"Text color": "Textfarbe" 'Loader': 'Fortschrittsbalken'
"Loader": "Fortschrittsbalken" 'Icon': 'Symbol'
"Icon": "Symbol" 'Same color': 'Selbe Farbe'
"Same color": "Selbe Farbe" 'Opposite color': 'Gegenfarbe'
"Opposite color": "Gegenfarbe" 'Transparent': 'Transparent'
"Transparent": "Transparent" 'Opaque': 'Nicht transparent'
"Opaque": "Nicht transparent" 'Opener': 'Menü-Symbol'
"Opener": "Menü-Symbol" 'Default': 'Standard'
"Default": "Standard" 'Default (dark)': 'Standard (dunkel)'
"Default (dark)": "Standard (dunkel)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (dunkel)'
"Hamburger (dark)": "Hamburger (dunkel)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (dunkel)'
"Hamburger 2 (dark)": "Hamburger 2 (dunkel)" 'Before the logo': 'Vor dem Logo'
"Before the logo": "Vor dem Logo" 'After the logo': 'Nach dem Logo'
"After the logo": "Nach dem Logo" 'Position': 'Position'
"Position": "Position" 'Show only the opener (hidden logo)': 'Nur das Menü-Symbol anzeigen (Logo wird ausgeblendet)'
"Show only the opener (hidden logo)": "Nur das Menü-Symbol anzeigen (Logo wird ausgeblendet)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Zeige das Seitenmenü und das Menü-Symbol nicht an, wenn keine App vorhanden ist (z.B. bei öffentlichen Seiten).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Zeige das Seitenmenü und das Menü-Symbol nicht an, wenn keine App vorhanden ist (z.B. bei öffentlichen Seiten)." 'Panel': 'Panel'
"Panel": "Panel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Öffne das Menü, wenn die Maus über das Menü-Symbol bewegt wird (auf Touchscreens automatisch deaktiviert)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Öffne das Menü, wenn die Maus über das Menü-Symbol bewegt wird (auf Touchscreens automatisch deaktiviert)" 'Display the big menu': 'Großes Menü anzeigen'
"Display the big menu": "Großes Menü anzeigen" 'Display the logo': 'Logo anzeigen'
"Display the logo": "Logo anzeigen" 'Icons and texts': 'Symbole und Texte'
"Icons and texts": "Symbole und Texte" 'Loader enabled': 'Fortschrittsbalken anzeigen'
"Loader enabled": "Fortschrittsbalken anzeigen" 'Tips': 'Tipps'
"Tips": "Tipps" 'Always displayed': 'Immer anzeigen'
"Always displayed": "Immer anzeigen" 'This is the automatic behavior when the menu is always displayed.': 'Dies ist das automatische Verhalten, wenn das Menü immer angezeigt wird.'
"This is the automatic behavior when the menu is always displayed.": "Dies ist das automatische Verhalten, wenn das Menü immer angezeigt wird." 'Not compatible with touch screens.': 'Nicht kompatibel mit Touchscreens.'
"Not compatible with touch screens.": "Nicht kompatibel mit Touchscreens." 'Big menu': 'Großes Menü'
"Big menu": "Großes Menü" 'Live preview': 'Live-Vorschau'
"Live preview": "Live-Vorschau" 'Open apps in new tab': 'Öffne Apps in einem neuen Tab'
"Open apps in new tab": "Öffne Apps in einem neuen Tab" 'Use the global setting': 'Verwende die globale Einstellung'
"Use the global setting": "Verwende die globale Einstellung" 'Use my selection': 'Verwende meine Auswahl'
"Use my selection": "Verwende meine Auswahl" 'Show and hide the list of applications': 'Ein- und Ausblenden der Appliste'
"Show and hide the list of applications": "Ein- und Ausblenden der Appliste" 'Use the avatar instead of the logo': 'Avatar anstelle des Logos anzeigen'
"Use the avatar instead of the logo": "Avatar anstelle des Logos anzeigen" 'You do not have permission to change the settings.': 'Du hast keine Berechtigung, die Einstellungen dieser App zu ändern.'
"You do not have permission to change the settings.": "Du hast keine Berechtigung, die Einstellungen dieser App zu ändern." 'Force this configuration to users': 'Konfiguration für alle Benutzer erzwingen'
"Force this configuration to users": "Konfiguration für alle Benutzer erzwingen" 'Export the configuration': 'Konfiguration exportieren'
"Export the configuration": "Konfiguration exportieren" 'Purge the cache': 'Cache leeren'
"Purge the cache": "Cache leeren" 'Show the link to settings': 'Link zu den Einstellungen anzeigen'
"Show the link to settings": "Link zu den Einstellungen anzeigen" 'The menu is enabled by default for users': 'Das Menü ist standardmäßig für alle Benutzer aktiviert'
"The menu is enabled by default for users": "Das Menü ist standardmäßig für alle Benutzer aktiviert" 'Except when the configuration is forced.': 'Gilt nicht, wenn die Konfiguration erzwungen wird.'
"Except when the configuration is forced.": "Gilt nicht, wenn die Konfiguration erzwungen wird." 'Apps that should not be displayed in the menu': 'Apps, die nicht im Menü angezeigt werden sollen'
"Apps that should not be displayed in the menu": "Apps, die nicht im Menü angezeigt werden sollen" 'This feature is only compatible with the <code>big menu</code> display.': 'Kompatibel mit dem <code>großen Menü</code>.'
"This feature is only compatible with the <code>big menu</code> display.": "Kompatibel mit dem <code>großen Menü</code>." 'The logo is a link to the default app': 'Das Logo ist ein Link zur Standard-App'
"The logo is a link to the default app": "Das Logo ist ein Link zur Standard-App" 'Others': 'Andere'
"Others": "Andere" 'Categories': 'Kategorien'
"Categories": "Kategorien" 'Customize sorting': 'Sortierung anpassen'
"Customize sorting": "Sortierung anpassen" 'Order by': 'Sortieren nach'
"Order by": "Sortieren nach" 'Name': 'Name'
"Name": "Name" 'Customed': 'Benutzerdefiniert'
"Customed": "Benutzerdefiniert" 'Show and hide the list of categories': 'Liste der Kategorien ein- und ausblenden'
"Show and hide the list of categories": "Liste der Kategorien ein- und ausblenden" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Diese Optionen werden auf <code>Dark Theme</code> oder <code>Breeze Dark Theme</code> angewendet.'
"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." 'Dark mode colors': 'Farben für den dunklen Modus'
"Dark mode colors": "Farben für den dunklen Modus" 'With categories': 'Mit Kategorien'
"With categories": "Mit Kategorien" 'Custom categories': 'Benutzerdefinierte Kategorien'
"Custom categories": "Benutzerdefinierte Kategorien" 'Customize application categories': 'App-Kategorien anpassen'
"Customize application categories": "App-Kategorien anpassen" 'Reset to default': 'Auf Standard zurücksetzen'
"Reset to default": "Auf Standard zurücksetzen" 'Hidden icon': 'Verstecktes Symbol'
"Hidden icon": "Verstecktes Symbol" 'Small icon': 'Kleines Symbol'
"Small icon": "Kleines Symbol" 'Normal icon': 'Normales Symbol'
"Normal icon": "Normales Symbol" 'Big icon': 'Großes Icon'
"Big icon": "Großes Icon" 'Hidden text': 'Versteckter Text'
"Hidden text": "Versteckter Text" 'Small text': 'Kleiner Text'
"Small text": "Kleiner Text" 'Normal text': 'Normaler Text'
"Normal text": "Normaler Text" 'Big text': 'Großer Text'
"Big text": "Großer Text" 'Applications': 'Apps'
"Applications": "Apps" 'Applications kept in the top menu': 'Apps in der oberen Navigationsleiste'
"Applications kept in the top menu": "Apps in der oberen Navigationsleiste" 'Applications kept in the top menu but also shown in side menu': 'Apps in der oberen Navigationsleiste, die auch im Seitenmenü angezeigt werden sollen'
"Applications kept in the top menu but also shown in side menu": "Apps in der oberen Navigationsleiste, die auch im Seitenmenü angezeigt werden sollen" 'These applications must be selected in the previous option.': 'Diese Apps müssen auch in der vorherigen Einstellung ausgewählt werden.'
"These applications must be selected in the previous option.": "Diese Apps müssen auch in der vorherigen Einstellung ausgewählt werden." 'Hide labels on mouse over': 'Labels ausblenden, wenn sich die Maus darüber befindet (Hover)'
"Hide labels on mouse over": "Labels ausblenden, wenn sich die Maus darüber befindet (Hover)" 'Except the hovered app': 'Außer die markierte App'
"Except the hovered app": "Except the hovered app" 'Search': 'Suche'
"Search": "Search" 'Toggle the menu': 'Menü ein- und ausblenden'
"Toggle the menu": "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'
'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,96 +1,111 @@
"Custom menu": "Menú personalizado" 'Custom menu': 'Menú personalizado'
"Enable the custom menu": "Habilitar el menú personalizado" 'Enable the custom menu': 'Activar el menú personalizado'
"No": "No" 'No': 'No'
"Yes": "Sí" 'Yes': 'Sí'
"Menu": "Menú" 'Menu': 'Menú'
? '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.' '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.'
: '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.' 'Top menu': 'Menu principal'
"Top menu": "Menu principal" 'Apps that not must be moved in the side menu': 'Aplicaciones que no se deben mover al menú lateral'
"Apps that not must be moved in the side menu": "Aplicaciones que no se deben mover al menú lateral" 'If there is no selection then the global configuration is applied.': 'Si no hay selección, se aplica la configuración global.'
"If there is no selection then the global configuration is applied.": "Si no hay selección, se aplica la configuración global." 'Experimental': 'En pruebas'
"Experimental": "En pruebas" 'Save': 'Guardar'
"Save": "Guardar" 'You like this app and you want to support me?': '¿Te gusta esta aplicación y quieres apoyarme?'
"You like this app and you want to support me?": "¿Te gusta esta aplicación y quieres apoyarme?" 'Buy me a coffee ☕': 'Cómprame un café ☕'
"Buy me a coffee ☕": "Cómprame un café ☕" 'Hidden': 'Oculto'
"Hidden": "Oculto" 'Small': 'Pequeño'
"Small": "Pequeño" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Grande'
"Big": "Grande" 'Hidden icon': 'Ocultar Icono'
"Hidden icon": "Ocultar Icono" 'Small icon': 'Icono pequeño'
"Small icon": "Icono pequeño" 'Normal icon': 'Icono normal'
"Normal icon": "Icono normal" 'Big icon': 'Icono grande'
"Big icon": "Icono grande" 'Hidden text': 'Texto oculto'
"Hidden text": "Texto oculto" 'Small text': 'Texto pequeño'
"Small text": "Texto pequeño" 'Normal text': 'Texto normal'
"Normal text": "Texto normal" 'Big text': 'Texto grande'
"Big text": "Texto grande" 'Colors': 'Colores'
"Colors": "Colores" 'Background color': 'Color de fondo'
"Background color": "Color de fondo" 'Background color of current app': 'Color de fondo de la aplicación actual'
"Background color of current app": "Color de fondo de la aplicación actual" 'Text color': 'Color del texto'
"Text color": "Color del texto" 'Loader': 'Cargador'
"Loader": "Cargador" 'Icon': 'Icono'
"Icon": "Icono" 'Same color': 'El mismo color'
"Same color": "El mismo color" 'Opposite color': 'Color opuesto'
"Opposite color": "Color opuesto" 'Transparent': 'Transparente'
"Transparent": "Transparente" 'Opaque': 'Opaco'
"Opaque": "Opaco" 'Opener': 'Abrir'
"Opener": "Abrir" 'Default': 'Por defecto'
"Default": "Por defecto" 'Default (dark)': 'Por defecto (oscuro)'
"Default (dark)": "Por defecto (oscuro)" 'Hamburger': 'Hamburguesa'
"Hamburger": "Hamburguesa" 'Hamburger (dark)': 'Hamburger (negro)'
"Hamburger (dark)": "Hamburger (negro)" 'Hamburger 2': 'Hamburguesa 2'
"Hamburger 2": "Hamburguesa 2" 'Hamburger 2 (dark)': 'Hamburger 2 (negro)'
"Hamburger 2 (dark)": "Hamburger 2 (negro)" 'Before the logo': 'Antes del logotipo'
"Before the logo": "Antes del logotipo" 'After the logo': 'Después del logotipo'
"After the logo": "Después del logotipo" 'Position': 'Posición'
"Position": "Posición" 'Show only the opener (hidden logo)': 'Mostrar solo abrir (ocultar logotipo)'
"Show only the opener (hidden logo)": "Mostrar solo abrir (ocultar logotipo)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'No mostrar el menú lateral y el abridor si no hay aplicación (por ejemplo: páginas públicas).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "No mostrar el menú lateral y el abridor si no hay aplicación (por ejemplo: páginas públicas)." 'Panel': 'Panel'
"Panel": "Panel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Abra el menú cuando el ratón esté sobre el icono (se desactiva automáticamente en las pantallas táctiles)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Abra el menú cuando el ratón esté sobre el icono (se desactiva automáticamente en las pantallas táctiles)" 'Display the big menu': 'Mostrar el menú grande'
"Display the big menu": "Mostrar el menú grande" 'Display the logo': 'Mostrar el logotipo'
"Display the logo": "Mostrar el logotipo" 'Icons and texts': 'Iconos y textos'
"Icons and texts": "Iconos y textos" 'Loader enabled': 'Cargador activado'
"Loader enabled": "Cargador activado" 'Tips': 'Consejos'
"Tips": "Consejos" 'Always displayed': 'Siempre se muestra'
"Always displayed": "Siempre se muestra" 'This is the automatic behavior when the menu is always displayed.': 'Este es el comportamiento automático cuando aún se muestra el menú.'
"This is the automatic behavior when the menu is always displayed.": "Este es el comportamiento automático cuando aún se muestra el menú." 'Not compatible with touch screens.': 'No es compatible con las pantallas táctiles.'
"Not compatible with touch screens.": "No es compatible con las pantallas táctiles." 'Big menu': 'Menú grande'
"Big menu": "Menú grande" 'Live preview': 'Previsualización en directo'
"Live preview": "Previsualización en directo" 'Open apps in new tab': 'Abrir las aplicaciones en una nueva pestaña'
"Open apps in new tab": "Abrir las aplicaciones en una nueva pestaña" 'Use the global setting': 'Utilizar la configuración global'
"Use the global setting": "Utilizar la configuración global" 'Use my selection': 'Utilizar mi selección'
"Use my selection": "Utilizar mi selección" 'Show and hide the list of applications': 'Mostrar y ocultar la lista de aplicaciones'
"Show and hide the list of applications": "Mostrar y ocultar la lista de aplicaciones" 'Use the avatar instead of the logo': 'Utilizar un avatar en lugar de un logotipo'
"Use the avatar instead of the logo": "Utilizar un avatar en lugar de un logotipo" 'You do not have permission to change the settings.': 'No tienes permiso para cambiar la configuración.'
"You do not have permission to change the settings.": "No tienes permiso para cambiar la configuración." 'Force this configuration to users': 'Forzar esta configuración a todos los usuarios'
"Force this configuration to users": "Forzar esta configuración a todos los usuarios" 'Export the configuration': 'Exportar la configuración'
"Export the configuration": "Exportar la configuración" 'Purge the cache': 'Vaciar la caché'
"Purge the cache": "Vaciar la caché" 'Show the link to settings': 'Mostrar un enlace a la configuración'
"Show the link to settings": "Mostrar un enlace a la configuración" 'The menu is enabled by default for users': 'El menú está activado por defecto para los usuarios'
"The menu is enabled by default for users": "El menú está activado por defecto para los usuarios" 'Except when the configuration is forced.': 'Excepto cuando la configuración es forzada.'
"Except when the configuration is forced.": "Excepto cuando la configuración es forzada." 'Apps that should not be displayed in the menu': 'Aplicaciones que no deben aparecer en el menú'
"Apps that should not be displayed in the menu": "Aplicaciones que no deben aparecer en el menú" '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>.'
"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>." 'The logo is a link to the default app': 'El logotipo es un enlace a la aplicación por defecto'
"The logo is a link to the default app": "El logotipo es un enlace a la aplicación por defecto" 'Others': 'Otros'
"Others": "Otros" 'Categories': 'Categorías'
"Categories": "Categorías" 'Customize sorting': 'Personalizar la clasificación'
"Customize sorting": "Personalizar la clasificación" 'Order by': 'Ordenar por'
"Order by": "Ordenar por" 'Name': 'Nombre'
"Name": "Nombre" 'Customed': 'Personalizado'
"Customed": "Personalizado" 'Show and hide the list of categories': 'Mostrar y ocultar la lista de categorías'
"Show and hide the list of categories": "Mostrar y ocultar la lista de categorías" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Estos parámetros se utilizan cuando el tema oscuro o el tema oscuro de Breeze están activados.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Estos parámetros se utilizan cuando el tema oscuro o el tema oscuro de Breeze están activados." 'Dark mode colors': 'Colores del modo oscuro'
"Dark mode colors": "Colores del modo oscuro" 'With categories': 'Con categorías'
"With categories": "Con categorías" 'Custom categories': 'Categorías personalizadas'
"Custom categories": "Categorías personalizadas" 'Customize application categories': 'Personalizar las categorías de las aplicaciones'
"Customize application categories": "Personalizar las categorías de las aplicaciones" 'Reset to default': 'Restablecer los valores por defecto'
"Reset to default": "Restablecer los valores por defecto" 'Applications': 'Aplicaciones'
"Applications": "Aplicaciones" 'Applications kept in the top menu': 'Aplicaciones guardadas en el menú superior'
"Applications kept in the top menu": "Aplicaciones guardadas en el menú superior" 'Applications kept in the top menu but also shown in side menu': 'Las aplicaciones se mantienen en el menú superior pero también se muestran en el menú lateral'
"Applications kept in the top menu but also shown in side menu": "Las aplicaciones se mantienen en el menú superior pero también se muestran en el menú lateral" 'These applications must be selected in the previous option.': 'Estas aplicaciones deben ser seleccionadas en las opciones anteriores.'
"These applications must be selected in the previous option.": "Estas aplicaciones deben ser seleccionadas en las opciones anteriores." 'Hide labels on mouse over': 'Ocultar las etiquetas al pasar el ratón'
"Hide labels on mouse over": "Ocultar las etiquetas al pasar el ratón" 'Except the hovered app': 'Excepto la aplicación sobre la que se pasa el cursor'
"Except the hovered app": "Except the hovered app" 'Search': 'Buscar'
"Search": "Search" 'Toggle the menu': 'Alternar el menú'
"Toggle the menu": "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'
'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,96 +1,111 @@
"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 <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.' '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.'
: '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.' '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 être affichées dans le menu latéral" 'If there is no selection then the global configuration is applied.': "Si il n'y a aucune sélection alors la configuration globale sera appliquée."
"If there is no selection then the global configuration is applied.": "Si il n'y a aucune sélection alors la configuration globale sera appliquée." 'Experimental': 'Expérimental'
"Experimental": "Expérimental" 'Save': 'Sauvegarder'
"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é ☕'
"Buy me a coffee ☕": "Offrez moi un café ☕" 'Hidden': 'Caché'
"Hidden": "Caché" 'Small': 'Petit'
"Small": "Petit" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Gros'
"Big": "Gros" 'Hidden icon': 'Icône masqué'
"Hidden icon": "Icône masqué" 'Small icon': 'Petit icône'
"Small icon": "Petit icône" 'Normal icon': 'Icône normal'
"Normal icon": "Icône normal" 'Big icon': 'Gros icône'
"Big icon": "Gros icône" 'Hidden text': 'Text masqué'
"Hidden text": "Text masqué" 'Small text': 'Texte petit'
"Small text": "Texte petit" 'Normal text': 'Texte normal'
"Normal text": "Texte normal" 'Big text': 'Gros texte'
"Big text": "Gros texte" 'Colors': 'Couleurs'
"Colors": "Couleurs" 'Background color': 'Couleur de fond'
"Background color": "Couleur de fond" 'Background color of current app': "Couleur de fond de l'application en cours"
"Background color of current app": "Couleur de fond de l'application en cours" 'Text color': 'Couleur du texte'
"Text color": "Couleur du texte" 'Loader': 'Indicateur de chargement'
"Loader": "Indicateur de chargement" 'Icon': 'Icône'
"Icon": "Icône" 'Same color': 'Même couleur'
"Same color": "Même couleur" 'Opposite color': 'Couleur opposée'
"Opposite color": "Couleur opposée" 'Transparent': 'Transparent'
"Transparent": "Transparent" 'Opaque': 'Opaque'
"Opaque": "Opaque" 'Opener': "Bouton d'ouverture"
"Opener": "Bouton d'ouverture" 'Default': 'Par défaut'
"Default": "Par défaut" 'Default (dark)': 'Par défaut (sombre)'
"Default (dark)": "Par défaut (sombre)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (sombre)'
"Hamburger (dark)": "Hamburger (sombre)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (sombre)'
"Hamburger 2 (dark)": "Hamburger 2 (sombre)" 'Before the logo': 'Avant le logo'
"Before the logo": "Avant le logo" 'After the logo': 'Après le logo'
"After the logo": "Après le logo" 'Position': 'Position'
"Position": "Position" 'Show only the opener (hidden logo)': "Afficher uniquement le bouton d'ouverture (masquer le logo)"
"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)."
"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'
"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 big menu": "Afficher le menu large" 'Display the logo': 'Afficher le logo'
"Display the logo": "Afficher le logo" 'Icons and texts': 'Icônes et textes'
"Icons and texts": "Icônes et textes" 'Loader enabled': "Activation de l'indicateur de chargement"
"Loader enabled": "Activation de l'indicateur de chargement" 'Tips': 'Astuces'
"Tips": "Astuces" 'Always displayed': 'Toujours affiché'
"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.'
"Not compatible with touch screens.": "Incompatible avec les écrans tactiles." 'Big menu': 'Menu large'
"Big menu": "Menu large" 'Live preview': 'Aperçu en direct'
"Live preview": "Aperçu en direct" 'Open apps in new tab': 'Ouvrir les applications dans un nouvel onglet'
"Open apps in new tab": "Ouvrir les applications dans un nouvel onglet" 'Use the global setting': 'Utiliser la configuration globale'
"Use the global setting": "Utiliser la configuration globale" 'Use my selection': 'Utiliser ma sélection'
"Use my selection": "Utiliser ma sélection" 'Show and hide the list of applications': 'Afficher et masquer la liste des applications'
"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"
"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'
"Force this configuration to users": "Forcer cette configuration aux utilisateurs" 'Export the configuration': 'Exporter la configuration'
"Export the configuration": "Exporter la configuration" 'Purge the cache': 'Purger le cache'
"Purge the cache": "Purger le cache" 'Show the link to settings': 'Afficher le lien vers les paramètres'
"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.'
"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'
"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>."
"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"
"The logo is a link to the default app": "Le logo est un lien vers l'application par défaut" 'Others': 'Autres'
"Others": "Autres" 'Categories': 'Catégories'
"Categories": "Catégories" 'Customize sorting': 'Personnaliser le tri'
"Customize sorting": "Personnaliser le tri" 'Order by': 'Trier par'
"Order by": "Trier par" 'Name': 'Nom'
"Name": "Nom" 'Customed': 'Personnalisé'
"Customed": "Personnalisé" 'Show and hide the list of categories': 'Afficher et masquer la liste des catégories'
"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'
"Dark mode colors": "Couleurs du mode sombre" 'With categories': 'Avec les catégories'
"With categories": "Avec les catégories" 'Custom categories': 'Catégories personnalisées'
"Custom categories": "Catégories personnalisées" 'Customize application categories': 'Personnaliser les catégories des applications'
"Customize application categories": "Personnaliser les catégories des applications" 'Reset to default': 'Restaurer les valeurs par défaut'
"Reset to default": "Restaurer les valeurs par défaut" 'Applications': 'Applications'
"Applications": "Applications" 'Applications kept in the top menu': 'Applications conservées dans le menu supérieur'
"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'
"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."
"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'
"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"
"Except the hovered app": "À l'exception de l'application survolée" 'Search': 'Rechercher'
"Search": "Rechercher" 'Toggle the menu': 'Basculer le menu'
"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,96 +1,111 @@
"Custom menu": "Custom menu" 'Custom menu': 'Menú personalizado'
"Enable the custom menu": "Enable the custom menu" 'Enable the custom menu': 'Activar o menú personalizado'
"No": "No" 'No': 'Non'
"Yes": "Yes" 'Yes': 'Si'
"Menu": "Menu" 'Menu': 'Menú'
? '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.' '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'
"Top menu": "Top menu" 'Apps that not must be moved in the side menu': 'As aplicacións que non deben moverse no menú lateral'
"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" 'If there is no selection then the global configuration is applied.': 'Se non hai selección, aplícase a configuración global.'
"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." 'Experimental': 'Experimental'
"Experimental": "Experimental" 'Save': 'Gardar'
"Save": "Save" 'You like this app and you want to support me?': 'Gústalle esta aplicación e quere axudarme?'
"You like this app and you want to support me?": "You like this app and you want to support me?" 'Buy me a coffee ☕': 'Convídeme a un café ☕'
"Buy me a coffee ☕": "Buy me a coffee ☕" 'Hidden': 'Agochado'
"Hidden": "Hidden" 'Small': 'Pequeno'
"Small": "Small" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Grande'
"Big": "Big" 'Hidden icon': 'Icona agochada'
"Hidden icon": "Hidden icon" 'Small icon': 'Icona pequena'
"Small icon": "Small icon" 'Normal icon': 'Icona normal'
"Normal icon": "Normal icon" 'Big icon': 'Icona grande'
"Big icon": "Big icon" 'Hidden text': 'Texto agochado'
"Hidden text": "Hidden text" 'Small text': 'Texto pequeno'
"Small text": "Small text" 'Normal text': 'Texto normal'
"Normal text": "Normal text" 'Big text': 'Texto grande'
"Big text": "Big text" 'Colors': 'Cores'
"Colors": "Colors" 'Background color': 'Cor do fondo'
"Background color": "Background color" 'Background color of current app': 'Cor do fondo da aplicación actual'
"Background color of current app": "Background color of current app" 'Text color': 'Cor do texto'
"Text color": "Text color" 'Loader': 'Cargador'
"Loader": "Loader" 'Icon': 'Icona'
"Icon": "Icon" 'Same color': 'A mesma cor'
"Same color": "Same color" 'Opposite color': 'A cor oposta'
"Opposite color": "Opposite color" 'Transparent': 'Transparente'
"Transparent": "Transparent" 'Opaque': 'Opaco'
"Opaque": "Opaque" 'Opener': 'Abrir'
"Opener": "Opener" 'Default': 'Predeterminado'
"Default": "Default" 'Default (dark)': 'Predeterminado (escuro)'
"Default (dark)": "Default (dark)" 'Hamburger': 'Hamburguesa'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburguesa (escuro)'
"Hamburger (dark)": "Hamburger (dark)" 'Hamburger 2': 'Hamburguesa 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburguesa 2 (escuro)'
"Hamburger 2 (dark)": "Hamburger 2 (dark)" 'Before the logo': 'Antes do logotipo'
"Before the logo": "Before the logo" 'After the logo': 'Após o logotipo'
"After the logo": "After the logo" 'Position': 'Posición'
"Position": "Position" 'Show only the opener (hidden logo)': 'Amosar só a icona de abrir (agochar o logotipo)'
"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Non amosar o menú lateral e a icona de abrir se non hai ningunha aplicación (por exemplo: páxinas públicas).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Do not display the side menu and the opener if there is no application (eg: public pages)." 'Panel': 'Panel'
"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)'
"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': 'Amosar o menú en grande'
"Display the big menu": "Display the big menu" 'Display the logo': 'Amosar o logotipo'
"Display the logo": "Display the logo" 'Icons and texts': 'Iconas e textos'
"Icons and texts": "Icons and texts" 'Loader enabled': 'Cargador activado'
"Loader enabled": "Loader enabled" 'Tips': 'Consellos'
"Tips": "Tips" 'Always displayed': 'Amosado sempre'
"Always displayed": "Always displayed" 'This is the automatic behavior when the menu is always displayed.': 'Este é o comportamento automático cando se amosa sempre o menú.'
"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." 'Not compatible with touch screens.': 'Non é compatíbel coas pantallas táctiles.'
"Not compatible with touch screens.": "Not compatible with touch screens." 'Big menu': 'Menú grande'
"Big menu": "Big menu" 'Live preview': 'Vista previa en directo'
"Live preview": "Live preview" 'Open apps in new tab': 'Abrir as aplicacións nunha nova lapela'
"Open apps in new tab": "Open apps in new tab" 'Use the global setting': 'Usar o axuste global'
"Use the global setting": "Use the global setting" 'Use my selection': 'Usar a miña selección'
"Use my selection": "Use my selection" 'Show and hide the list of applications': 'Amosar e agochar a lista de aplicacións'
"Show and hide the list of applications": "Show and hide the list of applications" 'Use the avatar instead of the logo': 'Usar o avatar no canto do logotipo'
"Use the avatar instead of the logo": "Use the avatar instead of the logo" 'You do not have permission to change the settings.': 'Non ten permiso para cambiar os axustes.'
"You do not have permission to change the settings.": "You do not have permission to change the settings." 'Force this configuration to users': 'Forzar esta configuración para os usuarios'
"Force this configuration to users": "Force this configuration to users" 'Export the configuration': 'Exportar a configuración'
"Export the configuration": "Export the configuration" 'Purge the cache': 'Limpar a caché'
"Purge the cache": "Purge the cache" 'Show the link to settings': 'Amosar a ligazón aos axustes'
"Show the link to settings": "Show the link to settings" 'The menu is enabled by default for users': 'De xeito predeterminado o menú está activado para os usuarios'
"The menu is enabled by default for users": "The menu is enabled by default for users" 'Except when the configuration is forced.': 'Agás cando a configuración é forzada.'
"Except when the configuration is forced.": "Except when the configuration is forced." 'Apps that should not be displayed in the menu': 'Aplicacións que non deben amosarse no menú'
"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" 'This feature is only compatible with the <code>big menu</code> display.': 'Esta función só é compatíbel coa presentación do <code>menú grande</code>.'
"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 logo is a link to the default app': 'O logotipo é unha ligazón á aplicación predeterminada'
"The logo is a link to the default app": "The logo is a link to the default app" 'Others': 'Outros'
"Others": "Others" 'Categories': 'Categorías'
"Categories": "Categories" 'Customize sorting': 'Personalizar a ordenación'
"Customize sorting": "Customize sorting" 'Order by': 'Ordenar por'
"Order by": "Order by" 'Name': 'Nome'
"Name": "Name" 'Customed': 'Personalizado'
"Customed": "Customed" 'Show and hide the list of categories': 'Amosar e agochar a lista de categorías'
"Show and hide the list of categories": "Show and hide the list of categories" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Estes parámetros úsanse cando o tema escuro ou o tema escuro de Breeze están activados.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." 'Dark mode colors': 'Cores do modo escuro'
"Dark mode colors": "Dark mode colors" 'With categories': 'Con categorías'
"With categories": "With categories" 'Custom categories': 'Categorías personalizadas'
"Custom categories": "Custom categories" 'Customize application categories': 'Personalizar as categorías das aplicacións'
"Customize application categories": "Customize application categories" 'Reset to default': 'Restabelecer os valores predeterminados'
"Reset to default": "Reset to default" 'Applications': 'Aplicacións'
"Applications": "Applications" 'Applications kept in the top menu': 'As aplicacións mantéñense no menú superior'
"Applications kept in the top menu": "Applications kept in the top menu" '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'
"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.': 'Estas aplicacións deben ser seleccionadas na opción anterior.'
"These applications must be selected in the previous option.": "These applications must be selected in the previous option." 'Hide labels on mouse over': 'Agochar as etiquetas ao pasar o rato'
"Hide labels on mouse over": "Hide labels on mouse over" 'Except the hovered app': 'Agás a aplicación que pasa o rato'
"Except the hovered app": "Except the hovered app" 'Search': 'Buscar'
"Search": "Search" 'Toggle the menu': 'Alternar o menú'
"Toggle the menu": "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'
'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,96 +1,111 @@
"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 <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.' '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.'
: '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.' 'Top menu': 'Bovenste menu'
"Top menu": "Bovenste menu" 'Apps that not must be moved in the side menu': 'Apps die niet moeten worden verplaatst in het zijmenu'
"Apps that not must be moved in the side menu": "Apps die niet moeten worden verplaatst in het zijmenu" 'If there is no selection then the global configuration is applied.': 'Als er geen keuze is, wordt de globale configuratie toegepast.'
"If there is no selection then the global configuration is applied.": "Als er geen keuze is, wordt de globale configuratie toegepast." 'Experimental': 'Experimenteel'
"Experimental": "Experimenteel" 'Save': 'Opslaan'
"Save": "Opslaan" 'You like this app and you want to support me?': 'Vind je deze app leuk en wil je me steunen?'
"You like this app and you want to support me?": "Vind je deze app leuk en wil je me steunen?" 'Buy me a coffee ☕': 'Koop een koffie voor me ☕'
"Buy me a coffee ☕": "Koop een koffie voor me ☕" 'Hidden': 'Verborgen'
"Hidden": "Verborgen" 'Small': 'Klein'
"Small": "Klein" 'Normal': 'Normaal'
"Normal": "Normaal" 'Big': 'Groot'
"Big": "Groot" 'Hidden icon': 'Verborgen icoon'
"Hidden icon": "Verborgen icoon" 'Small icon': 'Klein icoon'
"Small icon": "Klein icoon" 'Normal icon': 'Normaal icoon'
"Normal icon": "Normaal icoon" 'Big icon': 'Groot icoon'
"Big icon": "Groot icoon" 'Hidden text': 'Verborgen tekst'
"Hidden text": "Verborgen tekst" 'Small text': 'Kleine tekst'
"Small text": "Kleine tekst" 'Normal text': 'Normale tekst'
"Normal text": "Normale tekst" 'Big text': 'Grote tekst'
"Big text": "Grote tekst" 'Colors': 'Kleuren'
"Colors": "Kleuren" 'Background color': 'Achtergrond kleur'
"Background color": "Achtergrond kleur" 'Background color of current app': 'Achtergrondkleur van huidige app'
"Background color of current app": "Achtergrondkleur van huidige app" 'Text color': 'Tekst kleur'
"Text color": "Tekst kleur" 'Loader': 'Lader'
"Loader": "Lader" 'Icon': 'Icoon'
"Icon": "Icoon" 'Same color': 'Zelfde kleur'
"Same color": "Zelfde kleur" 'Opposite color': 'Tegenovergestelde kleur'
"Opposite color": "Tegenovergestelde kleur" 'Transparent': 'Transparant'
"Transparent": "Transparant" 'Opaque': 'Ondoorzichtig'
"Opaque": "Ondoorzichtig" 'Opener': 'Opener'
"Opener": "Opener" 'Default': 'Standaard'
"Default": "Standaard" 'Default (dark)': 'Standaard (donker)'
"Default (dark)": "Standaard (donker)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (donker)'
"Hamburger (dark)": "Hamburger (donker)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (donker)'
"Hamburger 2 (dark)": "Hamburger 2 (donker)" 'Before the logo': 'Voor het logo'
"Before the logo": "Voor het logo" 'After the logo': 'Na het logo'
"After the logo": "Na het logo" 'Position': 'Positie'
"Position": "Positie" 'Show only the opener (hidden logo)': 'Toon alleen de opener (verborgen logo)'
"Show only the opener (hidden logo)": "Toon alleen de opener (verborgen logo)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Geef het zijmenu en de opener niet weer als er geen toepassing is (bijv. openbare pagina''s).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Geef het zijmenu en de opener niet weer als er geen toepassing is (bijv. openbare pagina's)." 'Panel': 'Paneel'
"Panel": "Paneel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Open het menu wanneer de muis over de opener gaat (automatisch uitgeschakeld op aanraakschermen)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open het menu wanneer de muis over de opener gaat (automatisch uitgeschakeld op aanraakschermen)" 'Display the big menu': 'Toon het grote menu'
"Display the big menu": "Toon het grote menu" 'Display the logo': 'Toon het logo'
"Display the logo": "Toon het logo" 'Icons and texts': 'Iconen en teksten'
"Icons and texts": "Iconen en teksten" 'Loader enabled': 'Lader ingeschakeld'
"Loader enabled": "Lader ingeschakeld" 'Tips': 'Tips'
"Tips": "Tips" 'Always displayed': 'Altijd weergegeven'
"Always displayed": "Altijd weergegeven" 'This is the automatic behavior when the menu is always displayed.': 'Dit is het automatische gedrag wanneer het menu altijd wordt weergegeven.'
"This is the automatic behavior when the menu is always displayed.": "Dit is het automatische gedrag wanneer het menu altijd wordt weergegeven." 'Not compatible with touch screens.': 'Niet compatibel met aanraakschermen.'
"Not compatible with touch screens.": "Niet compatibel met aanraakschermen." 'Big menu': 'Groot menu'
"Big menu": "Groot menu" 'Live preview': 'Live voorbeeld'
"Live preview": "Live voorbeeld" 'Open apps in new tab': 'Open apps in nieuwe tab'
"Open apps in new tab": "Open apps in nieuwe tab" 'Use the global setting': 'Gebruik de globale instellingen'
"Use the global setting": "Gebruik de globale instellingen" 'Use my selection': 'Gebruik mijn selectie'
"Use my selection": "Gebruik mijn selectie" 'Show and hide the list of applications': 'De lijst met toepassingen tonen en verbergen'
"Show and hide the list of applications": "De lijst met toepassingen tonen en verbergen" 'Use the avatar instead of the logo': 'Gebruik avatar in plaats van het logo'
"Use the avatar instead of the logo": "Gebruik avatar in plaats van het logo" 'You do not have permission to change the settings.': 'Je hebt geen toestemming om de instellingen te veranderen.'
"You do not have permission to change the settings.": "Je hebt geen toestemming om de instellingen te veranderen." 'Force this configuration to users': 'Forceer deze configuratie aan gebruikers'
"Force this configuration to users": "Forceer deze configuratie aan gebruikers" 'Export the configuration': 'Exporteer de configuratie'
"Export the configuration": "Exporteer de configuratie" 'Purge the cache': 'De cache wissen'
"Purge the cache": "De cache wissen" 'Show the link to settings': 'Toon de link naar de instellingen'
"Show the link to settings": "Toon de link naar de instellingen" 'The menu is enabled by default for users': 'Het menu is standaard ingeschakeld voor gebruikers'
"The menu is enabled by default for users": "Het menu is standaard ingeschakeld voor gebruikers" 'Except when the configuration is forced.': 'Behalve als de configuratie geforceerd is.'
"Except when the configuration is forced.": "Behalve als de configuratie geforceerd is." 'Apps that should not be displayed in the menu': 'Apps die niet in het menu weergegeven mogen worden'
"Apps that should not be displayed in the menu": "Apps die niet in het menu weergegeven mogen worden" 'This feature is only compatible with the <code>big menu</code> display.': 'Deze functie is alleen compatibel met het <code>grote menu</code> scherm.'
"This feature is only compatible with the <code>big menu</code> display.": "Deze functie is alleen compatibel met het <code>grote menu</code> scherm." 'The logo is a link to the default app': 'Het logo is een link naar de standaard app'
"The logo is a link to the default app": "Het logo is een link naar de standaard app" 'Others': 'Overige'
"Others": "Overige" 'Categories': 'Categorieën'
"Categories": "Categorieën" 'Customize sorting': 'Sortering aanpassen'
"Customize sorting": "Sortering aanpassen" 'Order by': 'Sorteer op'
"Order by": "Sorteer op" 'Name': 'Naam'
"Name": "Naam" 'Customed': 'Aangepast'
"Customed": "Aangepast" 'Show and hide the list of categories': 'De lijst met categorieën tonen en verbergen'
"Show and hide the list of categories": "De lijst met categorieën tonen en verbergen" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Deze parameters worden gebruikt wanneer Dark theme of Breeze Dark Theme zijn ingeschakeld.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Deze parameters worden gebruikt wanneer Dark theme of Breeze Dark Theme zijn ingeschakeld." 'Dark mode colors': 'Donkere modus kleuren'
"Dark mode colors": "Donkere modus kleuren" 'With categories': 'Met categorieën'
"With categories": "Met categorieën" 'Custom categories': 'Aangepaste categorieën'
"Custom categories": "Aangepaste categorieën" 'Customize application categories': 'Toepassingscategorieën aanpassen'
"Customize application categories": "Toepassingscategorieën aanpassen" 'Reset to default': 'Terugzetten naar standaard'
"Reset to default": "Terugzetten naar standaard" 'Applications': 'Applicaties'
"Applications": "Applicaties" 'Applications kept in the top menu': 'Applicaties bewaard in het bovenste menu'
"Applications kept in the top menu": "Applicaties bewaard in het bovenste menu" 'Applications kept in the top menu but also shown in side menu': 'Applicaties blijven in het topmenu maar worden ook in het zijmenu getoond'
"Applications kept in the top menu but also shown in side menu": "Applicaties blijven in het topmenu maar worden ook in het zijmenu getoond" 'These applications must be selected in the previous option.': 'Deze toepassingen moeten bij de vorige optie zijn geselecteerd.'
"These applications must be selected in the previous option.": "Deze toepassingen moeten bij de vorige optie zijn geselecteerd." 'Hide labels on mouse over': 'Hide labels on mouse over'
"Hide labels on mouse over": "Hide labels on mouse over" 'Except the hovered app': 'Except the hovered app'
"Except the hovered app": "Except the hovered app" 'Search': 'Search'
"Search": "Search" 'Toggle the menu': 'Toggle the menu'
"Toggle the menu": "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'
'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,94 +1,109 @@
"Custom menu": "Menu personalizado" 'Custom menu': 'Menú personalizado'
"Enable the custom menu": "Habilitar o menu personalizado" 'Enable the custom menu': 'Activar o menu personalizado'
"No": "Não" 'No': 'Não'
"Yes": "Sim" 'Yes': 'Sim'
"Menu": "Menu" 'Menu': '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.' '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 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.' 'Top menu': 'Menu superior'
"Top menu": "Menu superior" 'Apps that not must be moved in the side menu': 'Apps que não devem ser movidos para o menu lateral'
"Apps that not must be moved in the side menu": "Apps que não devem ser movidos para o menu lateral" 'If there is no selection then the global configuration is applied.': 'Se não houver seleção, a configuração global será aplicada.'
"If there is no selection then the global configuration is applied.": "Se não houver seleção, a configuração global será aplicada." 'Experimental': 'Experimental'
"Experimental": "Experimental" 'Save': 'Salvar'
"Save": "Salvar" 'You like this app and you want to support me?': 'Você gosta deste aplicativo e quer me apoiar?'
"You like this app and you want to support me?": "Você gosta deste aplicativo e quer me apoiar?" 'Buy me a coffee ☕': 'Me pague um café ☕'
"Buy me a coffee ☕": "Me pague um café ☕" 'Hidden': 'Oculto'
"Hidden": "Oculto" 'Small': 'Pequeno'
"Small": "Pequeno" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Grande'
"Big": "Grande" 'Hidden icon': 'Ícone oculto'
"Hidden icon": "Ícone oculto" 'Small icon': 'Ícone pequeno'
"Small icon": "Ícone pequeno" 'Normal icon': 'Ícone normal'
"Normal icon": "Ícone normal" 'Big icon': 'Ícone grance'
"Big icon": "Ícone grance" 'Hidden text': 'Texto oculto'
"Hidden text": "Texto oculto" 'Small text': 'Texto pequeno'
"Small text": "Texto pequeno" 'Normal text': 'Texto normal'
"Normal text": "Texto normal" 'Big text': 'Texto grande'
"Big text": "Texto grande" 'Colors': 'Cores'
"Colors": "Cores" 'Background color': 'Cor de fundo'
"Background color": "Cor de fundo" 'Background color of current app': 'Cor de fundo do app atual'
"Background color of current app": "Cor de fundo do app atual" 'Text color': 'Cor do texto'
"Text color": "Cor do texto" 'Loader': 'Progresso'
"Loader": "Progresso" 'Icon': 'Ícone'
"Icon": "Ícone" 'Same color': 'Mesma cor'
"Same color": "Mesma cor" 'Opposite color': 'Cor oposta'
"Opposite color": "Cor oposta" 'Transparent': 'Transparente'
"Transparent": "Transparente" 'Opaque': 'Opaco'
"Opaque": "Opaco" 'Opener': 'Abrir'
"Opener": "Abrir" 'Default': 'Padrão'
"Default": "Padrão" 'Default (dark)': 'Padrão (escuro)'
"Default (dark)": "Padrão (escuro)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (escuro)'
"Hamburger (dark)": "Hamburger (escuro)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (escuro)'
"Hamburger 2 (dark)": "Hamburger 2 (escuro)" 'Before the logo': 'Antes da logo'
"Before the logo": "Antes da logo" 'After the logo': 'Depois da logo'
"After the logo": "Depois da logo" 'Position': 'Posição'
"Position": "Posição" 'Show only the opener (hidden logo)': 'Mostrar apenas o Abrir (ocultar logo)'
"Show only the opener (hidden logo)": "Mostrar apenas o Abrir (ocultar logo)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Não mostrar o menu lateral e o Abrir se não houver aplicação (p.ex. páginas públicas).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Não mostrar o menu lateral e o Abrir se não houver aplicação (p.ex. páginas públicas)." 'Panel': 'Painel'
"Panel": "Painel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Abrir o menu quando o mouse passar sobre o Abrir (desativado automaticamente em telas de toque)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Abrir o menu quando o mouse passar sobre o Abrir (desativado automaticamente em telas de toque)" 'Display the big menu': 'Mostrar o menu grande'
"Display the big menu": "Mostrar o menu grande" 'Display the logo': 'Mostrar a logo'
"Display the logo": "Mostrar a logo" 'Icons and texts': 'Ícones e textos'
"Icons and texts": "Ícones e textos" 'Loader enabled': 'Progresso ativado'
"Loader enabled": "Progresso ativado" 'Tips': 'Dicas'
"Tips": "Dicas" 'Always displayed': 'Sempre visível'
"Always displayed": "Sempre visível" 'This is the automatic behavior when the menu is always displayed.': 'Este é o comportamento automático quando o menu está sempre visível.'
"This is the automatic behavior when the menu is always displayed.": "Este é o comportamento automático quando o menu está sempre visível." 'Not compatible with touch screens.': 'Não compatível com telas de toque.'
"Not compatible with touch screens.": "Não compatível com telas de toque." 'Big menu': 'Menu grande'
"Big menu": "Menu grande" 'Live preview': 'Visualização ao vivo'
"Live preview": "Visualização ao vivo" 'Open apps in new tab': 'Abrir apps em nova aba'
"Open apps in new tab": "Abrir apps em nova aba" 'Use the global setting': 'Usar configurações globais'
"Use the global setting": "Usar configurações globais" 'Use my selection': 'Usar minha seleção'
"Use my selection": "Usar minha seleção" 'Show and hide the list of applications': 'Mostrar e ocultar a lista de aplicativos'
"Show and hide the list of applications": "Mostrar e ocultar a lista de aplicativos" 'Use the avatar instead of the logo': 'Use o avatar ao invés da logo'
"Use the avatar instead of the logo": "Use o avatar ao invés da logo" 'You do not have permission to change the settings.': 'Você não tem permissão para alterar as configurações.'
"You do not have permission to change the settings.": "Você não tem permissão para alterar as configurações." 'Force this configuration to users': 'Forçar esta configuração para os usuários'
"Force this configuration to users": "Forçar esta configuração para os usuários" 'Export the configuration': 'Exportar a configuração'
"Export the configuration": "Exportar a configuração" 'Purge the cache': 'Limpar o cache'
"Purge the cache": "Limpar o cache" 'Show the link to settings': 'Mostrar o link para configurações'
"Show the link to settings": "Mostrar o link para configurações" 'The menu is enabled by default for users': 'O menu é habilitado por padrão para os usuários'
"The menu is enabled by default for users": "O menu é habilitado por padrão para os usuários" 'Except when the configuration is forced.': 'Exceto quando a configuração é forçada.'
"Except when the configuration is forced.": "Exceto quando a configuração é forçada." 'Apps that should not be displayed in the menu': 'Apps que não devem ser mostrados no menu'
"Apps that should not be displayed in the menu": "Apps que não devem ser mostrados no menu" 'This feature is only compatible with the <code>big menu</code> display.': 'Este recurso só é compatível com a exibição do <code>menu grande</code>.'
"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>." 'The logo is a link to the default app': 'A logo é um link para o app padrão'
"The logo is a link to the default app": "A logo é um link para o app padrão" 'Others': 'Outros'
"Others": "Outros" 'Categories': 'Categorias'
"Categories": "Categorias" 'Customize sorting': 'Personalizar classificação'
"Customize sorting": "Personalizar classificação" 'Order by': 'Ordenar por'
"Order by": "Ordenar por" 'Name': 'Nome'
"Name": "Nome" 'Customed': 'Personalizado'
"Customed": "Personalizado" 'Show and hide the list of categories': 'Mostrar e esconder a lista de categorias'
"Show and hide the list of categories": "Mostrar e esconder a lista de categorias" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Estes parâmetros são usados quando o tema escuro ou o tema Dark Breeze está ativo.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Estes parâmetros são usados quando o tema escuro ou o tema Dark Breeze está ativo." 'Dark mode colors': 'Cores do modo escuro'
"Dark mode colors": "Cores do modo escuro" 'With categories': 'Com categorias'
"With categories": "Com categorias" 'Custom categories': 'Categorias personalizadas'
"Custom categories": "Categorias personalizadas" 'Customize application categories': 'Personalizar categorias de apps'
"Customize application categories": "Personalizar categorias de apps" 'Reset to default': 'Restaurar padrão'
"Reset to default": "Restaurar padrão" 'Applications': 'Aplicativos'
"Applications": "Aplicativos" 'Applications kept in the top menu': 'Aplicativos mantidos no menu superior'
"Applications kept in the top menu": "Aplicativos mantidos no menu superior" 'Applications kept in the top menu but also shown in side menu': 'Aplicativos mantidos no menu superior, mas também mostrados no menu lateral'
"Applications kept in the top menu but also shown in side menu": "Aplicativos mantidos no menu superior, mas também mostrados no menu lateral" 'These applications must be selected in the previous option.': 'Estes aplicativos devem ser selecionados na opção anterior.'
"These applications must be selected in the previous option.": "Estes aplicativos devem ser selecionados na opção anterior." 'Hide labels on mouse over': 'Ocultar descrição ao passar o mouse'
"Hide labels on mouse over": "Ocultar descrição ao passar o mouse" 'Toggle the menu': 'Toggle the menu'
"Toggle the menu": "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'
'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,96 +1,111 @@
"Custom menu": "Custom menu" 'Custom menu': 'Custom menu'
"Enable the custom menu": "Включить пользовательское меню" 'Enable the custom menu': 'Включить пользовательское меню'
"No": "Нет" 'No': 'Нет'
"Yes": "Да" 'Yes': 'Да'
"Menu": "Меню" '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.' 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.': 'Используйте сочетание клавиш Ctrl+o, чтобы открыть или скрыть боковое меню. Используйте tab key для навигации.'
: 'Используйте сочетание клавиш <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span>, чтобы открыть или скрыть боковое меню. Используйте <span class="keyboard-key">Tab</span> для навигации.' 'Top menu': 'Верхнее меню'
"Top menu": "Верхнее меню" 'Apps that not must be moved in the side menu': 'Приложения не перемещаемые в боковое меню'
"Apps that not must be moved in the side menu": "Приложения не перемещаемые в боковое меню" 'If there is no selection then the global configuration is applied.': 'Если тут ничего не отмечено, применяются глобальные настройки.'
"If there is no selection then the global configuration is applied.": "Если тут ничего не отмечено, применяются глобальные настройки." 'Experimental': 'Экспериментальный'
"Experimental": "Экспериментальный" 'Save': 'Сохранить'
"Save": "Сохранить" 'You like this app and you want to support me?': 'Вам нравится приложение или вы хотите поддержать меня?'
"You like this app and you want to support me?": "Вам нравится приложение или вы хотите поддержать меня?" 'Buy me a coffee ☕': 'Купить мне чашку кофе ☕'
"Buy me a coffee ☕": "Купить мне чашку кофе ☕" 'Hidden': 'Скрыто'
"Hidden": "Скрыто" 'Small': 'Маленький'
"Small": "Маленький" 'Normal': 'Средний'
"Normal": "Средний" 'Big': 'Большой'
"Big": "Большой" 'Hidden icon': 'Без иконки'
"Hidden icon": "Без иконки" 'Small icon': 'Маленькая иконка'
"Small icon": "Маленькая иконка" 'Normal icon': 'Средняя иконка'
"Normal icon": "Средняя иконка" 'Big icon': 'Большая иконка'
"Big icon": "Большая иконка" 'Hidden text': 'Без текста'
"Hidden text": "Без текста" 'Small text': 'Маленький текст'
"Small text": "Маленький текст" 'Normal text': 'Средний текст'
"Normal text": "Средний текст" 'Big text': 'Большой текст'
"Big text": "Большой текст" 'Colors': 'Цвета'
"Colors": "Цвета" 'Background color': 'Цвет фона'
"Background color": "Цвет фона" 'Background color of current app': 'Цвет фона выбранного приложения'
"Background color of current app": "Цвет фона выбранного приложения" 'Text color': 'Цвет текста'
"Text color": "Цвет текста" 'Loader': 'Загрузчик'
"Loader": "Загрузчик" 'Icon': 'Иконка'
"Icon": "Иконка" 'Same color': 'Такой же цвет'
"Same color": "Такой же цвет" 'Opposite color': 'Противоположный цвет'
"Opposite color": "Противоположный цвет" 'Transparent': 'Прозрачный'
"Transparent": "Прозрачный" 'Opaque': 'Непрозрачный'
"Opaque": "Непрозрачный" 'Opener': 'Открывалка'
"Opener": "Открывалка" 'Default': 'По умолчанию'
"Default": "По умолчанию" 'Default (dark)': 'По умолчанию (тёмный)'
"Default (dark)": "По умолчанию (тёмный)" 'Hamburger': 'Гамбургер'
"Hamburger": "Гамбургер" 'Hamburger (dark)': 'Гамбургер (тёмный)'
"Hamburger (dark)": "Гамбургер (тёмный)" 'Hamburger 2': 'Гамбургер 2'
"Hamburger 2": "Гамбургер 2" 'Hamburger 2 (dark)': 'Гамбургер 2 (тёмный)'
"Hamburger 2 (dark)": "Гамбургер 2 (тёмный)" 'Before the logo': 'Перед логотипом'
"Before the logo": "Перед логотипом" 'After the logo': 'После логотипа'
"After the logo": "После логотипа" 'Position': 'Положение'
"Position": "Положение" 'Show only the opener (hidden logo)': 'Показать только открывающую часть (скрытый логотип)'
"Show only the opener (hidden logo)": "Показать только открывающую часть (скрытый логотип)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Не отображать боковое меню и открывалку, если нет приложения (например, публичные страницы).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Не отображать боковое меню и открывалку, если нет приложения (например, публичные страницы)." 'Panel': 'Панель'
"Panel": "Панель" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Открывать меню при наведении мыши на открывалку (автоматически отключается на сенсорных экранах)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Открывать меню при наведении мыши на экран (автоматически отключается на сенсорных экранах)" 'Display the big menu': 'Отобразить большое меню'
"Display the big menu": "Отобразить большое меню" 'Display the logo': 'Показать логотип'
"Display the logo": "Показать логотип" 'Icons and texts': 'Иконки и текст'
"Icons and texts": "Иконки и текст" 'Loader enabled': 'Загрузчик включен'
"Loader enabled": "Загрузчик включен" 'Tips': 'Советы'
"Tips": "Советы" 'Always displayed': 'Всегда отображается'
"Always displayed": "Всегда отображается" 'This is the automatic behavior when the menu is always displayed.': 'Это автоматическое поведение, когда меню отображается всегда.'
"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." 'Not compatible with touch screens.': 'Не совместимо с сенсорными экранами.'
"Not compatible with touch screens.": "Не совместимо с сенсорными экранами." 'Big menu': 'Большое меню'
"Big menu": "Большое меню" 'Live preview': 'Предпросмотр в реальном времени'
"Live preview": "Live preview" 'Open apps in new tab': 'Открывать приложения в новой вкладке'
"Open apps in new tab": "Открывать приложения в новой вкладке" 'Use the global setting': 'Использовать глобальные настройки'
"Use the global setting": "Использовать глобальные настройки" 'Use my selection': 'Использовать мои настройки'
"Use my selection": "Использовать мои настройки" 'Show and hide the list of applications': 'Показать или скрыть список приложений'
"Show and hide the list of applications": "Показать или скрыть список приложений" 'Use the avatar instead of the logo': 'Использовать аватар вместо логотипа'
"Use the avatar instead of the logo": "Использовать аватар вместо логотипа" 'You do not have permission to change the settings.': 'У вас нет разрешения изменять настройки.'
"You do not have permission to change the settings.": "У вас нет разрешения изменять настройки." 'Force this configuration to users': 'Принудительно предоставить эту конфигурацию пользователям'
"Force this configuration to users": "Force this configuration to users" 'Export the configuration': 'Экспортировать конфигурацию'
"Export the configuration": "Экспортировать конфигурацию" 'Purge the cache': 'Очистить кэш'
"Purge the cache": "Очистить кэш" 'Show the link to settings': 'Показать ссылку на настройки'
"Show the link to settings": "Показать ссылку на настройки" 'The menu is enabled by default for users': 'Это меню включено по умолчанию для пользователей'
"The menu is enabled by default for users": "Это меню включено по умолчанию для пользователей" 'Except when the configuration is forced.': 'За исключением случаев, когда конфигурация является принудительной.'
"Except when the configuration is forced.": "Except when the configuration is forced." 'Apps that should not be displayed in the menu': 'Ппрограммы, скрытые из меню'
"Apps that should not be displayed in the menu": "Ппрограммы, скрытые из меню" 'This feature is only compatible with the <code>big menu</code> display.': 'Эта функция совместима только с отображением <code>большого меню</code>.'
"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 logo is a link to the default app': 'Логотип открывает приложение по умолчанию'
"The logo is a link to the default app": "Логотип открывает приложение по умолчанию" 'Others': 'Прочие'
"Others": "Прочие" 'Categories': 'Категории'
"Categories": "Категории" 'Customize sorting': 'Настроить сортировку'
"Customize sorting": "Настроить сортировку" 'Order by': 'В порядке'
"Order by": "В порядке" 'Name': 'Название'
"Name": "Название" 'Customed': 'Customed'
"Customed": "Customed" 'Show and hide the list of categories': 'Показать или скрыть список категорий'
"Show and hide the list of categories": "Показать или скрыть список категорий" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'Эти настройки используются темами Тёмная и Тёмная Breeze.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "Эти настройки используются темами Тёмная и Тёмная Breeze." 'Dark mode colors': 'Цвета тёмной темы'
"Dark mode colors": "Цвета тёмной темы" 'With categories': 'С категориями'
"With categories": "С категориями" 'Custom categories': 'Пользовательские категории'
"Custom categories": "Пользовательские категории" 'Customize application categories': 'Изменить категории приложений'
"Customize application categories": "Изменить категории приложений" 'Reset to default': 'Сбросить к значениям по умолчанию'
"Reset to default": "Сбросить к значениям по умолчанию" 'Applications': 'Приложения'
"Applications": "Приложения" 'Applications kept in the top menu': 'Приложения, хранящиеся в верхнем меню'
"Applications kept in the top menu": "Applications kept in the top menu" '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 but also shown in side menu" 'These applications must be selected in the previous option.': 'Эти приложения необходимо выбрать в предыдущем выборе.'
"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": "Except the hovered app" 'Search': 'Поиск'
"Search": "Search" 'Toggle the menu': 'Переключить меню'
"Toggle the menu": "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'
'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,94 +1,109 @@
"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 <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.' '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'
"Top menu": "Top menu" 'Apps that not must be moved in the side menu': 'Apps that not must be moved in the side menu'
"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" 'If there is no selection then the global configuration is applied.': 'If there is no selection then the global configuration is applied.'
"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." 'Experimental': 'Experimental'
"Experimental": "Experimental" 'Save': 'Save'
"Save": "Save" 'You like this app and you want to support me?': 'You like this app and you want to support me?'
"You like this app and you want to support me?": "You like this app and you want to support me?" 'Buy me a coffee ☕': 'Buy me a coffee ☕'
"Buy me a coffee ☕": "Buy me a coffee ☕" 'Hidden': 'Hidden'
"Hidden": "Hidden" 'Small': 'Small'
"Small": "Small" 'Normal': 'Normal'
"Normal": "Normal" 'Big': 'Big'
"Big": "Big" 'Hidden icon': 'Hidden icon'
"Hidden icon": "Hidden icon" 'Small icon': 'Small icon'
"Small icon": "Small icon" 'Normal icon': 'Normal icon'
"Normal icon": "Normal icon" 'Big icon': 'Big icon'
"Big icon": "Big icon" 'Hidden text': 'Hidden text'
"Hidden text": "Hidden text" 'Small text': 'Small text'
"Small text": "Small text" 'Normal text': 'Normal text'
"Normal text": "Normal text" 'Big text': 'Big text'
"Big text": "Big text" 'Colors': 'Colors'
"Colors": "Colors" 'Background color': 'Background color'
"Background color": "Background color" 'Background color of current app': 'Background color of current app'
"Background color of current app": "Background color of current app" 'Text color': 'Text color'
"Text color": "Text color" 'Loader': 'Loader'
"Loader": "Loader" 'Icon': 'Icon'
"Icon": "Icon" 'Same color': 'Same color'
"Same color": "Same color" 'Opposite color': 'Opposite color'
"Opposite color": "Opposite color" 'Transparent': 'Transparent'
"Transparent": "Transparent" 'Opaque': 'Opaque'
"Opaque": "Opaque" 'Opener': 'Opener'
"Opener": "Opener" 'Default': 'Default'
"Default": "Default" 'Default (dark)': 'Default (dark)'
"Default (dark)": "Default (dark)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (dark)'
"Hamburger (dark)": "Hamburger (dark)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (dark)'
"Hamburger 2 (dark)": "Hamburger 2 (dark)" 'Before the logo': 'Before the logo'
"Before the logo": "Before the logo" 'After the logo': 'After the logo'
"After the logo": "After the logo" 'Position': 'Position'
"Position": "Position" 'Show only the opener (hidden logo)': 'Show only the opener (hidden logo)'
"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" 'Do not display the side menu and the opener if there is no application (eg: public pages).': 'Do not display the side menu and the opener if there is no application (eg: public pages).'
"Do not display the side menu and the opener if there is no application (eg: public pages).": "Do not display the side menu and the opener if there is no application (eg: public pages)." 'Panel': 'Panel'
"Panel": "Panel" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)" 'Display the big menu': 'Display the big menu'
"Display the big menu": "Display the big menu" 'Display the logo': 'Display the logo'
"Display the logo": "Display the logo" 'Icons and texts': 'Icons and texts'
"Icons and texts": "Icons and texts" 'Loader enabled': 'Loader enabled'
"Loader enabled": "Loader enabled" 'Tips': 'Tips'
"Tips": "Tips" 'Always displayed': 'Always displayed'
"Always displayed": "Always displayed" 'This is the automatic behavior when the menu is always displayed.': 'This is the automatic behavior when the menu is always displayed.'
"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." 'Not compatible with touch screens.': 'Not compatible with touch screens.'
"Not compatible with touch screens.": "Not compatible with touch screens." 'Big menu': 'Big menu'
"Big menu": "Big menu" 'Live preview': 'Live preview'
"Live preview": "Live preview" 'Open apps in new tab': 'Open apps in new tab'
"Open apps in new tab": "Open apps in new tab" 'Use the global setting': 'Use the global setting'
"Use the global setting": "Use the global setting" 'Use my selection': 'Use my selection'
"Use my selection": "Use my selection" 'Show and hide the list of applications': 'Show and hide the list of applications'
"Show and hide the list of applications": "Show and hide the list of applications" 'Use the avatar instead of the logo': 'Use the avatar instead of the logo'
"Use the avatar instead of the logo": "Use the avatar instead of the logo" 'You do not have permission to change the settings.': 'You do not have permission to change the settings.'
"You do not have permission to change the settings.": "You do not have permission to change the settings." 'Force this configuration to users': 'Force this configuration to users'
"Force this configuration to users": "Force this configuration to users" 'Export the configuration': 'Export the configuration'
"Export the configuration": "Export the configuration" 'Purge the cache': 'Purge the cache'
"Purge the cache": "Purge the cache" 'Show the link to settings': 'Show the link to settings'
"Show the link to settings": "Show the link to settings" 'The menu is enabled by default for users': 'The menu is enabled by default for users'
"The menu is enabled by default for users": "The menu is enabled by default for users" 'Except when the configuration is forced.': 'Except when the configuration is forced.'
"Except when the configuration is forced.": "Except when the configuration is forced." 'Apps that should not be displayed in the menu': 'Apps that should not be displayed in the menu'
"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" 'This feature is only compatible with the <code>big menu</code> display.': 'This feature is only compatible with the <code>big menu</code> display.'
"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 logo is a link to the default app': 'The logo is a link to the default app'
"The logo is a link to the default app": "The logo is a link to the default app" 'Others': 'Others'
"Others": "Others" 'Categories': 'Categories'
"Categories": "Categories" 'Customize sorting': 'Customize sorting'
"Customize sorting": "Customize sorting" 'Order by': 'Order by'
"Order by": "Order by" 'Name': 'Name'
"Name": "Name" 'Customed': 'Customed'
"Customed": "Customed" 'Show and hide the list of categories': 'Show and hide the list of categories'
"Show and hide the list of categories": "Show and hide the list of categories" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." 'Dark mode colors': 'Dark mode colors'
"Dark mode colors": "Dark mode colors" 'With categories': 'With categories'
"With categories": "With categories" 'Custom categories': 'Custom categories'
"Custom categories": "Custom categories" 'Customize application categories': 'Customize application categories'
"Customize application categories": "Customize application categories" 'Reset to default': 'Reset to default'
"Reset to default": "Reset to default" 'Applications': 'Applications'
"Applications": "Applications" 'Applications kept in the top menu': 'Applications kept in the top menu'
"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'
"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.'
"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'
"Hide labels on mouse over": "Hide labels on mouse over" 'Toggle the menu': 'Toggle the menu'
"Toggle the menu": "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'
'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,100 +1,111 @@
"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 <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> '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.'
to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to 'Top menu': 'Top menu'
navigate.' 'Apps that not must be moved in the side menu': 'Apps that not must be moved in the side menu'
: 'Use the shortcut <span class="keyboard-key">Ctrl</span>+<span class="keyboard-key">o</span> 'If there is no selection then the global configuration is applied.': 'If there is no selection then the global configuration is applied.'
to open and to hide the side menu. Use <span class="keyboard-key">tab</span> to 'Experimental': 'Experimental'
navigate.' 'Save': 'Save'
"Top menu": "Top menu" 'You like this app and you want to support me?': 'You like this app and you want to support me?'
"Apps that not must be moved in the side menu": "Apps that not must be moved in the side menu" 'Buy me a coffee ☕': 'Buy me a coffee ☕'
"If there is no selection then the global configuration is applied.": "If there is no selection then the global configuration is applied." 'Hidden': 'Hidden'
"Experimental": "Experimental" 'Small': 'Small'
"Save": "Save" 'Normal': 'Normal'
"You like this app and you want to support me?": "You like this app and you want to support me?" 'Big': 'Big'
"Buy me a coffee ☕": "Buy me a coffee ☕" 'Hidden icon': 'Hidden icon'
"Hidden": "Hidden" 'Small icon': 'Small icon'
"Small": "Small" 'Normal icon': 'Normal icon'
"Normal": "Normal" 'Big icon': 'Big icon'
"Big": "Big" 'Hidden text': 'Hidden text'
"Hidden icon": "Hidden icon" 'Small text': 'Small text'
"Small icon": "Small icon" 'Normal text': 'Normal text'
"Normal icon": "Normal icon" 'Big text': 'Big text'
"Big icon": "Big icon" 'Colors': 'Colors'
"Hidden text": "Hidden text" 'Background color': 'Background color'
"Small text": "Small text" 'Background color of current app': 'Background color of current app'
"Normal text": "Normal text" 'Text color': 'Text color'
"Big text": "Big text" 'Loader': 'Loader'
"Colors": "Colors" 'Icon': 'Icon'
"Background color": "Background color" 'Same color': 'Same color'
"Background color of current app": "Background color of current app" 'Opposite color': 'Opposite color'
"Text color": "Text color" 'Transparent': 'Transparent'
"Loader": "Loader" 'Opaque': 'Opaque'
"Icon": "Icon" 'Opener': 'Opener'
"Same color": "Same color" 'Default': 'Default'
"Opposite color": "Opposite color" 'Default (dark)': 'Default (dark)'
"Transparent": "Transparent" 'Hamburger': 'Hamburger'
"Opaque": "Opaque" 'Hamburger (dark)': 'Hamburger (dark)'
"Opener": "Opener" 'Hamburger 2': 'Hamburger 2'
"Default": "Default" 'Hamburger 2 (dark)': 'Hamburger 2 (dark)'
"Default (dark)": "Default (dark)" 'Before the logo': 'Before the logo'
"Hamburger": "Hamburger" 'After the logo': 'After the logo'
"Hamburger (dark)": "Hamburger (dark)" 'Position': 'Position'
"Hamburger 2": "Hamburger 2" 'Show only the opener (hidden logo)': 'Show only the opener (hidden logo)'
"Hamburger 2 (dark)": "Hamburger 2 (dark)" '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).'
"Before the logo": "Before the logo" 'Panel': 'Panel'
"After the logo": "After the logo" '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)'
"Position": "Position" 'Display the big menu': 'Display the big menu'
"Show only the opener (hidden logo)": "Show only the opener (hidden logo)" 'Display the logo': 'Display 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)." 'Icons and texts': 'Icons and texts'
"Panel": "Panel" 'Loader enabled': 'Loader enabled'
"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)" 'Tips': 'Tips'
"Display the big menu": "Display the big menu" 'Always displayed': 'Always displayed'
"Display the logo": "Display the logo" 'This is the automatic behavior when the menu is always displayed.': 'This is the automatic behavior when the menu is always displayed.'
"Icons and texts": "Icons and texts" 'Not compatible with touch screens.': 'Not compatible with touch screens.'
"Loader enabled": "Loader enabled" 'Big menu': 'Big menu'
"Tips": "Tips" 'Live preview': 'Live preview'
"Always displayed": "Always displayed" 'Open apps in new tab': 'Open apps in new tab'
"This is the automatic behavior when the menu is always displayed.": "This is the automatic behavior when the menu is always displayed." 'Use the global setting': 'Use the global setting'
"Not compatible with touch screens.": "Not compatible with touch screens." 'Use my selection': 'Use my selection'
"Big menu": "Big menu" 'Show and hide the list of applications': 'Show and hide the list of applications'
"Live preview": "Live preview" 'Use the avatar instead of the logo': 'Use the avatar instead of the logo'
"Open apps in new tab": "Open apps in new tab" 'You do not have permission to change the settings.': 'You do not have permission to change the settings.'
"Use the global setting": "Use the global setting" 'Force this configuration to users': 'Force this configuration to users'
"Use my selection": "Use my selection" 'Export the configuration': 'Export the configuration'
"Show and hide the list of applications": "Show and hide the list of applications" 'Purge the cache': 'Purge the cache'
"Use the avatar instead of the logo": "Use the avatar instead of the logo" 'Show the link to settings': 'Show the link to settings'
"You do not have permission to change the settings.": "You do not have permission to change the settings." 'The menu is enabled by default for users': 'The menu is enabled by default for users'
"Force this configuration to users": "Force this configuration to users" 'Except when the configuration is forced.': 'Except when the configuration is forced.'
"Export the configuration": "Export the configuration" 'Apps that should not be displayed in the menu': 'Apps that should not be displayed in the menu'
"Purge the cache": "Purge the cache" 'This feature is only compatible with the <code>big menu</code> display.': 'This feature is only compatible with the <code>big menu</code> display.'
"Show the link to settings": "Show the link to settings" 'The logo is a link to the default app': 'The logo is a link to the default app'
"The menu is enabled by default for users": "The menu is enabled by default for users" 'Others': 'Others'
"Except when the configuration is forced.": "Except when the configuration is forced." 'Categories': 'Categories'
"Apps that should not be displayed in the menu": "Apps that should not be displayed in the menu" 'Customize sorting': 'Customize sorting'
"This feature is only compatible with the <code>big menu</code> display.": "This feature is only compatible with the <code>big menu</code> display." 'Order by': 'Order by'
"The logo is a link to the default app": "The logo is a link to the default app" 'Name': 'Name'
"Others": "Others" 'Customed': 'Customed'
"Categories": "Categories" 'Show and hide the list of categories': 'Show and hide the list of categories'
"Customize sorting": "Customize sorting" '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.'
"Order by": "Order by" 'Dark mode colors': 'Dark mode colors'
"Name": "Name" 'With categories': 'With categories'
"Customed": "Customed" 'Custom categories': 'Custom categories'
"Show and hide the list of categories": "Show and hide the list of categories" 'Customize application categories': 'Customize application categories'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "This parameters are used when Dark theme or Breeze Dark Theme are enabled." 'Reset to default': 'Reset to default'
"Dark mode colors": "Dark mode colors" 'Applications': 'Applications'
"With categories": "With categories" 'Applications kept in the top menu': 'Applications kept in the top menu'
"Custom categories": "Custom 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'
"Customize application categories": "Customize application categories" 'These applications must be selected in the previous option.': 'These applications must be selected in the previous option.'
"Reset to default": "Reset to default" 'Hide labels on mouse over': 'Hide labels on mouse over'
"Applications": "Applications" 'Except the hovered app': 'Except the hovered app'
"Applications kept in the top menu": "Applications kept in the top menu" 'Search': 'Search'
"Applications kept in the top menu but also shown in side menu": "Applications kept in the top menu but also shown in side menu" 'Toggle the menu': 'Toggle the menu'
"These applications must be selected in the previous option.": "These applications must be selected in the previous option." 'Open the documentation': 'Open the documentation'
"Hide labels on mouse over": "Hide labels on mouse over" 'Ask the developer': 'Ask the developer'
"Except the hovered app": "Except the hovered app" 'New request': 'New request'
"Search": "Search" 'Report a bug': 'Report a bug'
"Toggle the menu": "Toggle the menu" '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,96 +1,111 @@
"Custom menu": "自定义菜单" 'Custom menu': '自定义菜单'
"Enable the custom menu": "激活自定义菜单" 'Enable the custom menu': '启用自定义菜单'
"No": "取消" 'No': '取消'
"Yes": "确定" 'Yes': '确定'
"Menu": "菜单" '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." '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> 来导航。'
: "使用快捷键 <span class=\"keyboard-key\">Ctrl</span>+<span class=\"keyboard-key\">o</span> 打开或隐藏侧边栏菜单。使用<span class=\"keyboard-key\">tab</span> 来导航。" 'Top menu': '顶部菜单'
"Top menu": "顶部菜单" 'Apps that not must be moved in the side menu': '禁止在侧边栏菜单移动的应用'
"Apps that not must be moved in the side menu": "禁止在侧边栏菜单移动的应用" 'If there is no selection then the global configuration is applied.': '如果没有选择,则应用全局配置。'
"If there is no selection then the global configuration is applied.": "如不选择,将应用全局设定。" 'Experimental': '实验性'
"Experimental": "实验性" 'Save': '保存'
"Save": "保存" 'You like this app and you want to support me?': '喜欢本应用并支持我一下?'
"You like this app and you want to support me?": "喜欢本应用并支持我一下?" 'Buy me a coffee ☕': '赏一杯咖啡 ☕ 给我'
"Buy me a coffee ☕": "赏一杯咖啡 ☕ 给我" 'Hidden': '隐藏'
"Hidden": "隐藏" 'Small': '小型'
"Small": "小型" 'Normal': '标准'
"Normal": "标准" 'Big': '大型'
"Big": "大型" 'Colors': '颜色'
"Colors": "颜色" 'Background color': '背景颜色'
"Background color": "背景颜色" 'Background color of current app': '当前应用的背景色'
"Background color of current app": "当前应用的背景色" 'Text color': '文本颜色'
"Text color": "文字颜色" 'Loader': '菜单指示器'
"Loader": "菜单指示器" 'Icon': '图标'
"Icon": "图标" 'Same color': '相同颜色'
"Same color": "相同颜色" 'Opposite color': '相反颜色'
"Opposite color": "相反颜色" 'Transparent': '透明'
"Transparent": "透明" 'Opaque': '不透明'
"Opaque": "不透明" 'Opener': '容器'
"Opener": "容器" 'Default': '默认'
"Default": "默认" 'Default (dark)': '默认(深色)'
"Default (dark)": "默认(深色)" 'Hamburger': 'Hamburger'
"Hamburger": "Hamburger" 'Hamburger (dark)': 'Hamburger (深色)'
"Hamburger (dark)": "Hamburger (深色)" 'Hamburger 2': 'Hamburger 2'
"Hamburger 2": "Hamburger 2" 'Hamburger 2 (dark)': 'Hamburger 2 (深色)'
"Hamburger 2 (dark)": "Hamburger 2 (深色)" 'Before the logo': '在徽标之前'
"Before the logo": "在logo前" 'After the logo': '在徽标之后'
"After the logo": "在logo后" 'Position': '位置'
"Position": "位置" 'Show only the opener (hidden logo)': '只显示容器(隐藏徽标)'
"Show only the opener (hidden logo)": "只显示容器 (隐藏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).": "N如果没有应用不显示侧边栏菜单和容器 (例如 : 公共页面)。" 'Panel': '面板'
"Panel": "面板" 'Open the menu when the mouse is hover the opener (automatically disabled on touch screens)': '鼠标悬停时打开菜单 (触摸屏时将自动禁用)'
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)" 'Display the big menu': '显示大型菜单'
"Display the big menu": "显示大型菜单" 'Display the logo': '显示徽标'
"Display the logo": "显示logo" 'Icons and texts': '图标和文本'
"Icons and texts": "图标与文字" 'Loader enabled': '菜单指示器已启用'
"Loader enabled": "菜单指示器已激活" 'Tips': '技巧'
"Tips": "技巧" 'Always displayed': '始终显示'
"Always displayed": "一直显示" 'This is the automatic behavior when the menu is always displayed.': '这是菜单始终显示时的自动行为。'
"This is the automatic behavior when the menu is always displayed.": "一直显示菜单时的自动动作。" 'Not compatible with touch screens.': '与触摸屏不兼容。'
"Not compatible with touch screens.": "与触屏不兼容。" 'Big menu': '大型菜单'
"Big menu": "大型菜单" 'Live preview': '实时预览'
"Live preview": "实时预览" 'Open apps in new tab': '在新标签页中打开应用'
"Open apps in new tab": "在新标签中打开应用" 'Use the global setting': '使用全局设置'
"Use the global setting": "使用全局设定" 'Use my selection': '使用自定义设置'
"Use my selection": "使用自定义设定" 'Show and hide the list of applications': '显示和隐藏应用程序列表'
"Show and hide the list of applications": "显示或隐藏应用列表" 'Use the avatar instead of the logo': '使用头像代替徽标'
"Use the avatar instead of the logo": "使用头像代替logo" 'You do not have permission to change the settings.': '您没有更改设置的权限。'
"You do not have permission to change the settings.": "没有更改设置的权限。" 'Force this configuration to users': '强制用户使用此配置'
"Force this configuration to users": "强制用户使用此设置" 'Export the configuration': '导出配置'
"Export the configuration": "导出设置" 'Purge the cache': '清除缓存'
"Purge the cache": "清除缓存" 'Show the link to settings': '显示设置链接'
"Show the link to settings": "显示设置链接" 'The menu is enabled by default for users': '默认情况下为用户启用菜单'
"The menu is enabled by default for users": "用户的默认菜单已激活" 'Except when the configuration is forced.': '除非强制配置。'
"Except when the configuration is forced.": "除非设置被强制使用。" 'Apps that should not be displayed in the menu': '禁止在菜单中显示的应用'
"Apps that should not be displayed in the menu": "禁止在菜单中显示的应用" 'This feature is only compatible with the <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': 'logo链接到默认应用'
"The logo is a link to the default app": "logo链接到默认应用" 'Others': '其他'
"Others": "其他" 'Categories': '类别'
"Categories": "类别" 'Customize sorting': '自定义排序'
"Customize sorting": "自定义顺序" 'Order by': '排序方式'
"Order by": "排序规则" 'Name': '名称'
"Name": "名称" 'Customed': '自定义'
"Customed": "自定义" 'Show and hide the list of categories': '显示或隐藏类别列表'
"Show and hide the list of categories": "显示或隐藏类别列表" 'This parameters are used when Dark theme or Breeze Dark Theme are enabled.': '启用深色主题时使用此参数。'
"This parameters are used when Dark theme or Breeze Dark Theme are enabled.": "此参数将应用于暗黑主题激活时。" 'Dark mode colors': '深色模式颜色'
"Dark mode colors": "暗黑模式颜色" 'With categories': '按类别'
"With categories": "有类别" 'Custom categories': '自定义类别'
"Custom categories": "自定义类别" 'Customize application categories': '自定义应用程序类别'
"Customize application categories": "自定义应用程序类别" 'Reset to default': '重置为默认设置'
"Reset to default": "重置为默认设置" 'Hidden icon': '隐藏图标'
"Hidden icon": "隐藏图标" 'Small icon': '小图标'
"Small icon": "小图标" 'Normal icon': '正常图标'
"Normal icon": "正常图标" 'Big icon': '大图标'
"Big icon": "大图标" 'Hidden text': '隐藏文本'
"Hidden text": "隐藏文字" 'Small text': '小文本'
"Small text": "小文本" 'Normal text': '普通文本'
"Normal text": "普通文本" 'Big text': '大文本'
"Big text": "大文本" 'Applications': 'Applications'
"Applications": "Applications" 'Applications kept in the top menu': 'Applications kept in the top menu'
"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'
"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.'
"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'
"Hide labels on mouse over": "Hide labels on mouse over" 'Except the hovered app': 'Except the hovered app'
"Except the hovered app": "Except the hovered app" 'Search': 'Search'
"Search": "Search" 'Toggle menu': 'Toggle menu'
"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'

32
src/lib/app.js Normal file
View file

@ -0,0 +1,32 @@
/**
* @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 { loadState } from '@nextcloud/initial-state'
const getActiveAppId = () => {
const apps = loadState('core', 'apps', {})
for (let id in apps) {
if (apps[id].active) {
return apps[id].id
}
}
return null
}
export { getActiveAppId }

View file

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

54
src/lib/dom.js Normal file
View file

@ -0,0 +1,54 @@
/**
* @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 }

28
src/lib/menu.js Normal file
View file

@ -0,0 +1,28 @@
/**
* @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 }

40
src/lib/search.js Normal file
View file

@ -0,0 +1,40 @@
/**
* @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 }

26
src/lib/setting.js Normal file
View file

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

52
src/menu.js Normal file
View file

@ -0,0 +1,52 @@
/**
* @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 { 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)
})

139
src/menus/MenuContainer.vue Normal file
View file

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

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

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

185
src/menus/StandardMenu.vue Normal file
View file

@ -0,0 +1,185 @@
<!--
@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.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="app.name"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.active ? '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.active ? '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'
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([])
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.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)
}
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']
ready.value = true
setApps(await navStore.getCoreApps())
window.addEventListener('resize', () => {
window.clearTimeout(resizeTimeout)
resizeTimeout = window.setTimeout(computeLists, 100)
})
})
</script>
<script>
export default {
compatConfig: {
GLOBAL_MOUNT_CONTAINER: false,
},
}
</script>

137
src/menus/TopWideMenu.vue Normal file
View file

@ -0,0 +1,137 @@
<!--
@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--topwidemenu"
:class="{ open: open }"
>
<div class="cm-header">
<CloserButton
v-if="!openerHover"
@click="$emit('close')"
/>
<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 CloserButton from '../components/CloserButton'
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>

627
src/pages/AdminSettings.vue Normal file
View file

@ -0,0 +1,627 @@
<!--
@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>
<NcContent
v-if="config"
app-name="side_menu"
>
<NcAppNavigation class="cm-settings-nav">
<NcAppNavigationItem
v-for="item in menu"
:key="item.label"
:name="trans(item.label)"
:active="item.section === section"
@click="setSection(item.section)"
/>
</NcAppNavigation>
<NcAppContent class="cm-settings cm-settings--nav">
<SettingsSection :hidden="section !== 'panel'">
<SectionTitle label="Panel" />
<SettingItem>
<SettingValue>
<FormDisplayPicker
:always-displayed="config['always-displayed']"
:top-wide-menu="config['big-menu']"
:side-menu-with-categories="config['side-with-categories']"
@update:always-displayed="(value) => (config['always-displayed'] = value)"
@update:top-wide-menu="(value) => (config['big-menu'] = value)"
@update:side-menu-with-categories="(value) => (config['side-with-categories'] = value)"
/>
</SettingValue>
</SettingItem>
<SettingItem :disabled="config['big-menu'] || config['always-displayed'] || config['side-with-categories']">
<SettingLabel
label="Display the logo"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['display-logo']" />
</SettingValue>
</SettingItem>
<SettingItem :disabled="config['big-menu'] || config['always-displayed'] || config['side-with-categories']">
<SettingLabel
label="Use the avatar instead of the logo"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['use-avatar']" />
</SettingValue>
</SettingItem>
<SettingItem :disabled="config['big-menu'] || config['always-displayed'] || config['side-with-categories']">
<SettingLabel
label="The logo is a link to the default app"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['add-logo-link']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Show the link to settings"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['show-settings']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Icons"
:middle="true"
/>
<SettingValue>
<FormSize v-model="config['size-icon']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Texts"
:middle="true"
/>
<SettingValue>
<FormSize v-model="config['size-text']" />
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'topMenu'">
<SectionTitle label="Top menu" />
<SettingItem>
<SettingLabel
label="Applications kept in the top menu"
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['top-menu-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Applications kept in the top menu but also shown in side menu"
help="These applications must be selected in the previous option."
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['top-side-menu-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Hide labels on mouse over"
:top="true"
/>
<SettingValue>
<FormSelect
v-model="config['top-menu-mouse-over-hidden-label']"
:expanded="true"
:options="[
{ id: 1, label: 'Yes' },
{ id: 0, label: 'No' },
{ id: 2, label: 'Except the hovered app' },
]"
/>
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'apps'">
<SectionTitle label="Applications" />
<SettingItem>
<SettingLabel
label="Apps that should not be displayed in the menu"
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['big-menu-hidden-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Open apps in new tab"
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['target-blank-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Customize sorting"
:middle="true"
/>
<SettingValue>
<FormAppSort v-model="config['apps-order']" />
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'cats'">
<SectionTitle label="Categories" />
<SettingItem :disabled="!(config['big-menu'] || config['always-displayed'] || config['side-with-categories'])">
<SettingLabel
label="Order by"
:middle="true"
/>
<SettingValue>
<FormSelect
v-model="config['categories-order-type']"
:options="[
{ id: 'default', label: 'Name' },
{ id: 'custom', label: 'Custom' },
]"
/>
</SettingValue>
</SettingItem>
<SettingItem :disabled="!(config['big-menu'] || config['always-displayed'] || config['side-with-categories'])">
<SettingLabel
label="Customize sorting"
:middle="true"
/>
<SettingValue>
<FormCatSort v-model="config['categories-order']" />
</SettingValue>
</SettingItem>
<SettingItem :disabled="!(config['big-menu'] || config['always-displayed'] || config['side-with-categories'])">
<SettingLabel
label="Customize application categories"
:top="true"
>
</SettingLabel>
<SettingValue>
<FormAppCategory
:langs="config['langs']"
:categories-custom="config['categories-custom']"
:apps-categories-custom="config['apps-categories-custom']"
@update:categories-custom="(value) => (config['categories-custom'] = value)"
@update:apps-categories-custom="(value) => (config['apps-categories-custom'] = value)"
/>
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'opener'">
<SectionTitle label="Opener" />
<SettingItem>
<SettingLabel
label="Opener"
:middle="true"
/>
<SettingValue>
<FormOpener v-model="config['opener']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Dark mode opener"
:middle="true"
/>
<SettingValue>
<FormOpener v-model="config['dark-mode-opener']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Position"
:middle="true"
/>
<SettingValue>
<FormSelect
v-model="config['opener-position']"
:options="[
{ id: 'before', label: 'Before the logo' },
{ id: 'after', label: 'After the logo' },
]"
/>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Show only the opener (hidden logo)"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['opener-only']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Open the menu when the mouse is hover the opener (automatically disabled on touch screens)"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['opener-hover']" />
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'colors'">
<SectionTitle label="Colors" />
<SettingItem>
<SettingLabel
label="Background color"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['background-color']" />
<FormColorPicker v-model="config['background-color-to']" />
<FormRange
v-model="config['background-color-opacity']"
prepend="Transparent"
append="Opaque"
/>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Background color of current app"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['current-app-background-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Text color"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['text-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Loader"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['loader-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Icon"
:middle="true"
/>
<SettingValue>
<FormRange
v-model="config['icon-invert-filter']"
prepend="Same color"
append="Opposite color"
/>
<FormRange
v-model="config['icon-opacity']"
prepend="Transparent"
append="Opaque"
/>
</SettingValue>
</SettingItem>
<SectionTitle label="Dark mode colors" />
<SettingItem>
<SettingLabel
label="Background color"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['dark-mode-background-color']" />
<FormColorPicker v-model="config['dark-mode-background-color-to']" />
<FormRange
v-model="config['dark-mode-background-color-opacity']"
prepend="Transparent"
append="Opaque"
/>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Background color of current app"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['dark-mode-current-app-background-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Text color"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['dark-mode-text-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Loader"
:middle="true"
/>
<SettingValue>
<FormColorPicker v-model="config['dark-mode-loader-color']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Icon"
:middle="true"
/>
<SettingValue>
<FormRange
v-model="config['dark-mode-icon-invert-filter']"
prepend="Same color"
append="Opposite color"
/>
<FormRange
v-model="config['dark-mode-icon-opacity']"
prepend="Transparent"
append="Opaque"
/>
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'global'">
<p class="cm-settings-tips">
<em>{{ t('side_menu', 'Use the shortcut Ctrl+o to open and to hide the side menu. Use tab key to navigate.') }}</em>
</p>
<SectionTitle label="Global" />
<SettingItem>
<SettingLabel
label="The menu is enabled by default for users"
help="Except when the configuration is forced."
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['default-enabled']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Force this configuration to users"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['force']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Loader enabled"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['loader-enabled']" />
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
<SettingsSection :hidden="section !== 'support'">
<SectionTitle label="Support" />
<SettingItem>
<SettingLabel
label="You like this app and you want to support me?"
:middle="true"
/>
<SettingValue>
<ExternalLink href="https://www.buymeacoffee.com/deblan">
<NcButton variant="secondary">{{ trans('Buy me a coffee ☕') }}</NcButton>
</ExternalLink>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel label="Need help" />
<SettingValue class="cm-settings-button-inline">
<ExternalLink href="https://deblan.gitnet.page/side_menu_doc/">
<NcButton variant="secondary">{{ trans('Open the documentation') }}</NcButton>
</ExternalLink>
<ExternalLink href="https://gitnet.fr/deblan/side_menu/issues/new?template=.gitea%2fissue_template%2fQUESTION_TEMPLATE.yml">
<NcButton variant="secondary">{{ trans('Ask the developer') }}</NcButton>
</ExternalLink>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="I would like a new feature"
:middle="true"
/>
<SettingValue>
<ExternalLink href="https://gitnet.fr/deblan/side_menu/issues/new?template=.gitea%2fissue_template%2fFEATURE_TEMPLATE.yml">
<NcButton variant="secondary">{{ trans('New request') }}</NcButton>
</ExternalLink>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Something went wrong"
:top="true"
/>
<SettingValue class="cm-settings-button-inline">
<ExternalLink href="https://gitnet.fr/deblan/side_menu/issues/new?template=.gitea%2fissue_template%2fISSUE_TEMPLATE.yml">
<NcButton variant="secondary">{{ trans('Report a bug') }}</NcButton>
</ExternalLink>
<NcButton
variant="secondary"
@click="showConfig = true"
>{{ trans('Show the configuration') }}</NcButton
>
<NcModal
v-if="showConfig"
class="cm-settings-config-modal"
@close="showConfig = false"
>
<div class="modal__content">
<p style="margin-bottom: 5px">{{ trans('Configuration:') }}</p>
<textarea
readonly
v-text="filterConfig(config)"
></textarea>
<div class="modal__footer">
<NcButton
variant="secondary"
@click="copyConfig"
>
<span v-if="configCopied">{{ trans('Done!') }}</span>
<span v-else>{{ trans('Copy') }}</span>
</NcButton>
<NcButton
variant="primary"
@click="showConfig = false"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</SettingValue>
</SettingItem>
<AdminSaveButton :config="config" />
</SettingsSection>
</NcAppContent>
</NcContent>
</template>
<script setup>
import { NcContent, NcAppContent, NcButton, NcModal, NcAppNavigation, NcAppNavigationItem } from '@nextcloud/vue'
import { ref, onMounted } from 'vue'
import { useConfigStore } from '../store/config.js'
import { SettingsSection, SettingItem, SettingLabel, SettingValue, SectionTitle, ExternalLink, AdminSaveButton } from '../components/settings'
import {
FormRange,
FormColorPicker,
FormOpener,
FormSelect,
FormYesNo,
FormSize,
FormAppPicker,
FormAppSort,
FormCatSort,
FormDisplayPicker,
FormAppCategory,
} from '../components/settings/form'
const menu = [
{ label: 'Global', section: 'global', icon: '' },
{ label: 'Panel', section: 'panel', icon: '' },
{ label: 'Colors', section: 'colors', icon: '' },
{ label: 'Opener', section: 'opener', icon: '' },
{ label: 'Applications', section: 'apps', icon: '' },
{ label: 'Categories', section: 'cats', icon: '' },
{ label: 'Top menu', section: 'topMenu', icon: '' },
{ label: 'Support', section: 'support', icon: '' },
]
const config = ref(null)
const showConfig = ref(false)
const configCopied = ref(false)
const configStore = useConfigStore()
const section = ref(null)
const setSection = (value) => {
sessionStorage.setItem('side_menu_section', value)
section.value = value
}
const trans = (value) => {
return t('side_menu', value)
}
const copyConfig = () => {
navigator.clipboard.writeText(JSON.stringify(filterConfig(config.value), null, 2))
configCopied.value = true
window.setTimeout(() => {
configCopied.value = false
}, 2000)
}
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
}
onMounted(async () => {
config.value = await configStore.getAppConfig()
setSection(sessionStorage.getItem('side_menu_section') ?? menu[0].section)
})
</script>

170
src/pages/UserSettings.vue Normal file
View file

@ -0,0 +1,170 @@
<!--
@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>
<NcContent
v-if="config"
app-name="side_menu"
>
<NcAppContent
v-if="config['force']"
class="cm-settings"
>
<SettingsSection>
<SettingLabel label="You do not have permission to change the settings." />
</SettingsSection>
</NcAppContent>
<NcAppContent
v-else
class="cm-settings"
>
<SettingsSection>
<SectionTitle label="Menu" />
<SettingItem>
<SettingLabel
label="Enable the custom menu"
:middle="true"
/>
<SettingValue>
<FormYesNo v-model="config['enabled']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Applications kept in the top menu"
help="If there is no selection then the global configuration is applied."
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['top-menu-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Applications kept in the top menu but also shown in side menu"
help="These applications must be selected in the previous option."
help2="If there is no selection then the global configuration is applied"
:middle="true"
/>
<SettingValue>
<FormAppPicker v-model="config['top-side-menu-apps']" />
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Open apps in new tab"
:middle="true"
/>
<SettingValue class="cm-settings-children-inline">
<FormSelect
v-model="config['target-blank-mode']"
:options="[
{ id: 1, label: 'Use the global setting' },
{ id: 2, label: 'Use my selection' },
]"
/>
<FormAppPicker
v-if="config['target-blank-mode'] === 2"
v-model="config['target-blank-apps']"
/>
</SettingValue>
</SettingItem>
<SettingItem>
<SettingLabel
label="Customize sorting"
:top="true"
/>
<SettingValue>
<FormAppSort v-model="config['apps-order']" />
</SettingValue>
</SettingItem>
</SettingsSection>
<SettingsSection>
<SectionTitle label="Support" />
<SettingItem>
<SettingLabel
label="You like this app and you want to support me?"
:middle="true"
/>
<SettingValue>
<ExternalLink href="https://www.buymeacoffee.com/deblan">
<NcButton variant="secondary">{{ trans('Buy me a coffee ☕') }}</NcButton>
</ExternalLink>
</SettingValue>
</SettingItem>
<UserSaveButton :config="config" />
</SettingsSection>
</NcAppContent>
</NcContent>
</template>
<script setup>
import { NcContent, NcAppContent, NcButton } from '@nextcloud/vue'
import { ref, onMounted } from 'vue'
import { useConfigStore } from '../store/config.js'
import { FormSelect, FormYesNo, FormAppPicker, FormAppSort } from '../components/settings/form'
import { SettingsSection, SettingItem, SettingLabel, SettingValue, SectionTitle, ExternalLink, UserSaveButton } from '../components/settings'
const config = ref(null)
const configStore = useConfigStore()
const trans = (value) => {
return t('side_menu', value)
}
onMounted(async () => {
config.value = await configStore.getUserConfig()
})
</script>
<style scoped>
.hidden {
display: none;
}
.button-inline button {
display: inline-block;
margin-right: 5px;
}
.config {
width: 100%;
height: 30vh;
}
.modal__content {
padding: 20px;
}
.modal__footer {
margin-top: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
margin-right: 5px;
}
.save {
display: block;
float: right;
}
</style>

262
src/scss/admin.scss Normal file
View file

@ -0,0 +1,262 @@
/**
* @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/>.
*/
.cm-settings {
&--nav {
padding-top: 30px;
}
&-nav {
.app-navigation__content {
padding: 20px;
}
.app-navigation-entry-icon {
display: none !important;
}
.app-navigation-entry__name {
padding-left: 10px !important;
}
}
&-tips {
margin-bottom: 15px;
}
&-section {
width: 100%;
padding: 20px;
&--hidden {
display: none;
}
}
&-item {
display: flex;
justify-content: start;
margin-bottom: 10px;
&--disabled {
opacity: 0.5;
}
&-label {
max-width: 350px;
width: 100%;
padding-right: 20px;
&--short {
max-width: 300px;
}
&--top {
vertical-align: top;
}
&--middle {
display: flex;
flex-direction: column;
text-align: left;
}
}
&-form {
}
}
&-form {
&-arrow {
color: var(--color-text-maxcontrast);
display: inline-block;
margin-right: 3px;
}
&-draggable {
cursor: pointer;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
}
&-displaypicker {
img {
padding: 10px 10px 10px 0;
border: 2px solid transparent;
max-width: 100%;
cursor: pointer;
}
}
&-colorpicker {
display: inline-block;
margin-right: 12px;
width: 60px;
height: 30px;
&-value {
cursor: pointer;
width: 60px;
height: 30px;
border-radius: 6px;
border: 1px solid var(--color-border);
}
}
&-range {
input {
min-height: auto;
}
div * {
vertical-align: middle;
}
em + input,
input + em {
margin-left: 10px;
}
}
&-catsort-modal {
.modal__footer {
padding: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
}
}
&-appsort-modal {
.modal__footer {
text-align: right;
padding: 20px;
}
.modal__footer button {
display: inline-block;
}
}
&-apppicker-modal {
.modal__content {
padding: 20px;
}
.modal__footer {
margin-top: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
}
img {
width: 15px;
height: 15px;
}
}
&-appcategory-modal {
.modal__content {
padding: 20px;
}
.menu button {
display: inline-block;
margin-right: 5px;
}
.modal__footer {
margin-top: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
}
td {
padding: 5px 0;
}
tr:hover,
td:hover {
background: none !important;
}
.form {
padding: 10px 0;
}
img {
width: 15px;
height: 15px;
}
.btn-close {
margin-left: 20px;
}
}
}
&-btn {
&--save {
margin-top: 30px;
}
}
&-config-modal {
textarea {
width: 100%;
height: 30vh;
}
.modal__content {
padding: 20px;
}
.modal__footer {
margin-top: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
margin-right: 5px;
}
}
&-children-inline {
> * {
display: inline-block !important;
margin-right: 5px;
margin-bottom: 5px;
}
}
&-button-inline {
.button-vue {
display: inline-block !important;
margin-right: 5px;
margin-bottom: 5px;
}
}
}

601
src/scss/menu.scss Normal file
View file

@ -0,0 +1,601 @@
/**
* @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/>.
*/
#header {
.cm-opener {
margin-left: 0px;
margin-top: 0px;
}
}
.app-menu {
visibility: hidden;
}
.cm {
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;
&-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-top: 1px !important;
margin-left: 5px !important;
margin-left: 3px !important;
overflow: hidden;
span {
position: relative;
left: 50px;
display: block;
width: 1px;
height: 1px;
overflow: hidden;
}
&:active,
&:focus {
background-color: var(--side-menu-current-app-background-color, #444) !important;
}
}
&-closer {
background: url('../../img/side-menu-opener-closer.svg');
display: none;
}
a {
transition: 0.2s;
}
&-categories-wrapper {
padding-bottom: 70px;
}
&-search {
float: right;
input {
background: none;
border: 0;
border-radius: 0;
color: var(--side-menu-text-color);
&::placeholder {
color: var(--side-menu-text-color);
}
}
}
&-categories {
max-height: calc(100vh - 55px);
overflow: auto;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 0 10% 0 10%;
}
&-category {
padding: 10px 20px;
flex: 1 1 auto;
&-title {
padding-left: 10px;
color: var(--side-menu-text-color, #fff);
font-weight: bold;
font-size: 20px;
margin-bottom: 12px;
line-height: 30px;
margin-top: 0;
}
}
&-header {
width: 100%;
z-index: 2300;
max-width: 290px;
padding-top: 2px;
top: 0;
&::after {
content: ' ';
display: block;
clear: both;
}
}
&-loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 3001;
&-bar {
height: 4px;
background: var(--side-menu-loader-color, #0e75ac);
width: 0;
transition-property: width;
}
}
&-apps {
height: calc(100vh - 49px);
top: 49px;
z-index: 2200;
position: fixed;
width: 100%;
max-width: 290px;
overflow: auto;
&.side-menu-apps-list--with-logo {
height: calc(100vh - 160px);
top: 160px;
}
}
&-app {
a {
line-height: 30px;
color: var(--side-menu-text-color, #fff);
display: block;
padding: 7px 0 5px 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
a:hover,
a:focus,
&:active,
&.active {
background: var(--side-menu-current-app-background-color, #444);
}
&-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);
}
}
&-setting {
margin-right: 9px;
margin-top: 2px;
float: right;
line-height: 34px;
height: 42px;
display: none;
a {
color: var(--side-menu-text-color, #fff);
display: block;
padding: 4px 7px;
}
&:hover a,
a:active,
a:focus {
background: var(--side-menu-current-app-background-color, #444);
}
img {
vertical-align: bottom;
margin-left: 3px;
width: 32px;
height: 32px;
}
}
&.open {
display: block;
.cm-setting {
display: block;
}
}
&-logo {
text-align: center;
clear: both;
img {
max-width: 60%;
max-height: 100px;
}
}
&--topwidemenu {
max-width: 100%;
height: auto;
}
&--sidemenuwithcategories {
max-width: 290px;
height: 100vh;
.cm-categories {
display: block;
padding: 0;
width: 100%;
}
.cm-category {
padding: 10px 0;
}
.cm-header {
max-width: 295px;
}
}
&.cm--topwidemenu,
&.cm--sidemenuwithcategories {
.cm-apps {
height: auto !important;
position: static !important;
max-width: 100vw !important;
overflow: auto !important;
}
.cm-app {
a {
padding: 7px 0 7px 7px;
}
&-icon {
vertical-align: middle;
margin-top: -2px;
}
}
}
}
.cm-standardmenu {
visibility: hidden;
&.show {
visibility: visible;
}
}
.cm-always-displayed {
body {
width: calc(100% - 50px) !important;
position: absolute;
left: 50px;
}
#header {
position: absolute !important;
.cm-opener {
display: none;
}
}
.cm {
display: block;
width: 50px;
&-apps {
height: calc(100vh - 49px);
width: 50px;
top: 49px;
&:hover {
overflow: auto;
}
}
&-header {
height: 49px;
width: 50px;
}
&-logo {
display: none;
}
&-app {
&-text {
display: none;
}
}
&.open {
width: 100%;
max-width: 290px;
.cm-apps {
width: 100%;
}
.cm-app {
&-text {
display: inline;
}
}
.cm-header {
width: 100%;
}
}
}
.app-navigation-toggle-wrapper {
right: 0 !important;
margin-left: 0 !important;
}
}
@media screen and (max-width: 1024px) {
.cm {
&--topwidemenu {
max-width: 290px;
height: 100vh;
.cm-header {
max-width: 100%;
}
}
&-categories {
display: block;
padding: 0;
}
&-category {
padding: 10px 0;
}
}
}
@media screen and (min-width: 1024px) {
.cm {
&--topwidemenu {
.cm-header {
max-width: 100%;
}
}
&-closer {
display: block;
float: right;
margin-right: 9px;
}
}
}
$header-icon-size: 20px;
.cm-standardmenu {
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;
&.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);
box-sizing: content-box;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
}
.app-menu-entry--label {
opacity: 0;
position: absolute;
font-size: 12px;
color: var(--color-primary-text);
text-align: center;
left: 50%;
top: 45%;
display: block;
min-width: 100%;
transform: translateX(-50%);
transition: all 0.1s ease-in-out;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
letter-spacing: -0.5px;
}
&: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: bolder;
bottom: 0;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
// 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 {
opacity: 1;
img {
margin-top: -8px;
}
.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: -8px;
}
.app-menu-entry--label {
opacity: 1;
bottom: 0;
}
&::before,
.app-menu-entry::before {
opacity: 0;
}
}
}
.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));
&:not([aria-expanded='true']) {
color: var(--color-main-text);
&:hover {
opacity: 1;
background-color: transparent !important;
}
}
&:focus-visible {
opacity: 1;
outline: none !important;
}
}
&-app-menu-popover-entry {
.app-icon {
position: relative;
height: 35px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
filter: var(--background-invert-if-bright, var(--primary-invert-if-bright));
&.has-unread::after {
background-color: var(--color-main-text);
}
img {
width: $header-icon-size;
height: $header-icon-size;
}
}
}
.has-unread::after {
content: '';
width: 8px;
height: 8px;
background-color: var(--color-primary-element-text);
border-radius: 50%;
position: absolute;
display: block;
top: 10px;
right: 10px;
}
.unread-counter {
display: none;
}
}

56
src/store/config.js Normal file
View file

@ -0,0 +1,56 @@
/**
* @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 { defineStore } from 'pinia'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
export const useConfigStore = defineStore('config', () => {
let config = null
let appConfig = null
let userConfig = null
async function getConfig() {
if (config === null) {
config = await axios.get(generateUrl('/apps/side_menu/js/config')).then((response) => response.data)
}
return config
}
async function getAppConfig() {
if (appConfig === null) {
appConfig = await axios.get(generateUrl('/apps/side_menu/admin/config')).then((response) => response.data)
}
return appConfig
}
async function getUserConfig() {
if (userConfig === null) {
userConfig = await axios.get(generateUrl('/apps/side_menu/user/config')).then((response) => response.data)
}
return userConfig
}
return {
getConfig,
getAppConfig,
getUserConfig,
}
})

66
src/store/nav.js Normal file
View file

@ -0,0 +1,66 @@
/**
* @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 { defineStore } from 'pinia'
import axios from '@nextcloud/axios'
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
export const useNavStore = defineStore('nav', () => {
let categories = null
let apps = null
let coreApps = null
async function getApps() {
if (apps === null) {
apps = []
const cats = await getCategories()
cats.forEach((category) => {
Object.values(category.apps).forEach((app) => {
apps.push(app)
})
})
}
return apps
}
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)
}
return coreApps
}
async function getCategories() {
if (categories === null) {
categories = await axios.get(generateUrl('/apps/side_menu/nav/items')).then((response) => response.data.items)
}
return categories
}
return {
getApps,
getCoreApps,
getCategories,
}
})

32
src/user.js Normal file
View file

@ -0,0 +1,32 @@
/**
* @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/admin.scss'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
import UserSettings from './pages/UserSettings'
waitContainer('#side-menu-user-settings').then((selector) => {
const pinia = createPinia()
const app = createApp(UserSettings)
app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(selector)
})

View file

@ -1,125 +1,124 @@
<?php
function putVars(array $vars)
{
foreach ($vars as $key => $value) {
echo sprintf(
"--side-menu-%s: %s;\n",
$key,
'opener' === $key
? sprintf('url("%s")', image_path('side_menu', $value.'.svg'))
: $value
);
}
}
?>
:root { :root {
<?php foreach ($_['vars'] as $key => $value): ?> <?php putVars($_['vars']['dark']); ?>
<?php if ($key === 'opener'): ?>
--side-menu-<?php echo $key ?>: url('<?php print_unescaped(image_path('side_menu', $value.'.svg')); ?>');
<?php else: ?>
--side-menu-<?php echo $key ?>: <?php echo $value ?>;
<?php endif; ?>
<?php endforeach; ?>
} }
<?php if (empty($_['top-menu-apps']) && empty($_['top-side-menu-apps'])): ?> @media (prefers-color-scheme: light) {
#appmenu { :root {
display: none; <?php putVars($_['vars']['light']); ?>
} }
}
#appmenu + nav { @media (prefers-color-scheme: dark) {
display: none; <?php putVars($_['vars']['dark']); ?>
} }
<?php else: ?>
.app-hidden {
opacity: 0;
}
<?php endif; ?>
<?php if ($_['opener-only']): ?> body[data-theme-dark], body[data-theme-dark-highcontrast] {
<?php putVars($_['vars']['dark']); ?>
}
body[data-theme-light], body[data-theme-light-highcontrast] {
<?php putVars($_['vars']['light']); ?>
}
<?php if ($_['opener-only']) { ?>
#nextcloud { #nextcloud {
display: none; display: none;
} }
<?php endif; ?> <?php } ?>
<?php if (!$_['display-logo']): ?> <?php if ('hidden' === $_['size-text']) { ?>
.side-menu-logo { .cm-apps {
<?php if ('big' === $_['size-icon']) { ?>
width: 55px;
<?php } else { ?>
width: 52px;
<?php } ?>
}
.cm .cm-opener {
<?php if ('big' === $_['size-icon']) { ?>
margin-left: 1px;
<?php } else { ?>
margin-left: 0px;
<?php } ?>
}
<?php } ?>
<?php if ('hidden' === $_['size-icon']) { ?>
.cm-app-icon {
display: none; display: none;
} }
<?php } elseif ('small' === $_['size-icon']) { ?>
.side-menu-header { .cm-app-icon svg {
height: 50px;
}
.side-menu-apps-list {
height: calc(100vh - 49px);
top: 49px;
}
#side-menu.hide-opener .side-menu-header .side-menu-opener.side-menu-closer {
visibility: hidden;
}
#side-menu.hide-opener.side-menu-with-categories .side-menu-search {
float: none;
}
<?php if ($_['size-text'] === 'hidden'): ?>
#side-menu, .side-menu-apps-list {
<?php if ($_['size-icon'] === 'big'): ?>
width: 55px;
<?php else: ?>
width: 52px;
<?php endif; ?>
}
#side-menu .side-menu-opener {
<?php if ($_['size-icon'] === 'big'): ?>
margin-left: 1px;
<?php else: ?>
margin-left: 0px;
<?php endif; ?>
}
<?php endif; ?>
<?php endif; ?>
<?php if ($_['size-icon'] === 'hidden'): ?>
.side-menu-app-icon {
display: none;
}
<?php elseif ($_['size-icon'] === 'small'): ?>
.side-menu-app-icon svg {
width: 15px; width: 15px;
height: 15px; height: 15px;
} }
img.side-menu-app-icon { img.cm-app-icon {
width: 15px; width: 15px;
height: 15px; height: 15px;
} }
<?php elseif ($_['size-icon'] === 'normal'): ?>
.side-menu-app-icon svg { .cm-app a {
padding-left: 16px !important;
}
<?php } elseif ('normal' === $_['size-icon']) { ?>
.cm-app-icon svg {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
img.side-menu-app-icon { img.cm-app-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
<?php elseif ($_['size-icon'] === 'big'): ?> <?php } elseif ('big' === $_['size-icon']) { ?>
.side-menu-app-icon svg { .cm-app-icon svg {
width: 23px; width: 23px;
height: 23px; height: 23px;
} }
img.side-menu-app-icon { img.cm-app-icon {
width: 23px; width: 23px;
height: 23px; height: 23px;
} }
<?php endif; ?>
<?php if ($_['size-text'] === 'hidden'): ?> .cm-app a {
.side-menu-app-text { padding-left: 11px !important;
}
<?php } ?>
<?php if ('hidden' === $_['size-text']) { ?>
.cm-app-text {
display: none; display: none;
} }
<?php elseif ($_['size-text'] === 'small'): ?> <?php } elseif ('small' === $_['size-text']) { ?>
.side-menu-app-text { .cm-app-text {
font-size: 12px; font-size: 12px;
} }
<?php elseif ($_['size-text'] === 'big'): ?> <?php } elseif ('big' === $_['size-text']) { ?>
.side-menu-app-text { .cm-app-text {
font-size: 16px; font-size: 16px;
} }
<?php endif; ?> <?php } ?>
<?php if ($_['always-displayed']): ?> <?php if ($_['always-displayed']) { ?>
#content { #content {
left: 53px; left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px); width: calc(100% - (var(--body-container-margin) * 2) - 62px);
@ -129,4 +128,4 @@
width: calc(100% - (var(--body-container-margin) * 2) - 60px); width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px; margin-left: 11px;
} }
<?php endif; ?> <?php } ?>

View file

@ -1,204 +0,0 @@
<?php
header('Content-type: text/javascript');
$display = 'default';
if ($_['always-displayed']) {
$display = 'always-displayed';
} elseif ($_['big-menu']) {
$display = 'big-menu';
} elseif ($_['side-with-categories']) {
$display = 'side-with-categories';
}
?>
const SMcreateElement = (tagName, attributes) => {
const element = document.createElement(tagName)
if (typeof attributes === 'object') {
for (let i in attributes) {
if (i === 'text') {
element.textContent = attributes[i]
} else if (i === 'html') {
element.innerHTML = attributes[i]
} else {
element.setAttribute(i, attributes[i])
}
}
}
return element
}
(function() {
const sideMenuContainer = SMcreateElement('div', {id: 'side-menu-container'})
const sideMenuOpener = SMcreateElement('button', {
'class': 'side-menu-opener',
'arial-label': t('side_menu', 'Toggle the menu'),
'html': `<span>${t('side_menu', 'Toggle the menu')}</span>`
})
const sideMenu = SMcreateElement('div', {id: 'side-menu'})
const body = document.querySelector('body')
const html = document.querySelector('html')
const nextcloud = document.querySelector('#nextcloud')
const logo = document.querySelector('.header-left .logo')
const isTouchDevice = window.matchMedia("(pointer: coarse)").matches
window.targetBlankApps = <?php echo json_encode($_['target-blank-apps']), "\n" ?>
window.topMenuApps = <?php echo json_encode($_['top-menu-apps']), "\n"; ?>
window.topSideMenuApps = <?php echo json_encode($_['top-side-menu-apps']), "\n"; ?>
window.menuAppsOrder = <?php echo json_encode($_['apps-order']), "\n"; ?>
window.topMenuAppsMouseOverHiddenLabel = <?php echo json_encode($_['top-menu-mouse-over-hidden-label']), "\n"; ?>
<?php if ($display === 'big-menu'): ?>
sideMenu.setAttribute('data-bigmenu', '1')
<?php elseif ($display === 'side-with-categories'): ?>
sideMenu.setAttribute('data-sidewithcategories', '1')
<?php endif; ?>
const sideMenuFocus = () => {
let a = document.querySelector('#side-menu .side-menu-app.active a')
|| document.querySelector('#side-menu .side-menu-app a')
if (a) {
a.focus()
}
}
document.querySelector('body').addEventListener('side-menu.apps', (e) => {
const apps = e.detail.apps;
<?php if ($_['hide-when-no-apps']): ?>
const sideMenu = document.querySelector('#side-menu')
if (apps.length === 0) {
sideMenu.classList.remove('open')
sideMenu.classList.add('hide')
sideMenuOpener.classList.add('hide')
} else {
sideMenu.classList.remove('hide')
sideMenuOpener.classList.remove('hide')
}
<?php if ($display === 'always-displayed'): ?>
if (apps.length === 0) {
html.classList.remove('side-menu-always-displayed')
} else {
html.classList.add('side-menu-always-displayed')
}
<?php endif; ?>
<?php else: ?>
<?php if ($display === 'always-displayed'): ?>
if (apps.length === 0) {
html.classList.remove('side-menu-always-displayed')
} else {
html.classList.add('side-menu-always-displayed')
}
<?php endif; ?>
<?php endif; ?>
})
body.addEventListener('side-menu.ready', () => {
const sideMenu = document.querySelector('#side-menu')
const headerMenuOpener = document.querySelector('#header .side-menu-opener')
const sideMenuOpener = document.querySelectorAll('#side-menu .side-menu-opener')
if (!headerMenuOpener) {
return
}
<?php if ($_['opener-hover']): ?>
const sideMenuMouseLeave = () => {
sideMenu.classList.remove('open')
sideMenu.removeEventListener('mouseleave', sideMenuMouseLeave)
}
const sideMenuMouseEnter = () => {
sideMenu.addEventListener('mouseleave', sideMenuMouseLeave)
}
const sideMenuOpenerMouseEnter = () => {
sideMenu.classList.add('open')
sideMenu.addEventListener('mouseenter', sideMenuMouseEnter)
sideMenuFocus()
}
if (!isTouchDevice) {
<?php if ($_['opener-hover']): ?>
headerMenuOpener.addEventListener('mouseenter', sideMenuOpenerMouseEnter)
sideMenu.classList.add('hide-opener')
<?php endif ?>
sideMenu.addEventListener('mouseleave', sideMenuMouseLeave)
sideMenu.addEventListener('mouseenter', sideMenuOpenerMouseEnter)
}
<?php endif; ?>
headerMenuOpener.addEventListener('click', () => {
sideMenu.classList.add('open')
headerMenuOpener.blur()
sideMenuFocus()
})
for (let opener of sideMenuOpener) {
opener.addEventListener('click', () => {
<?php if ($display === 'always-displayed'): ?>
sideMenu.classList.toggle('open')
<?php else: ?>
sideMenu.classList.remove('open')
<?php endif; ?>
})
}
document.addEventListener('keydown', (e) => {
var key = e.key || e.keyCode
if ((key === 'o' || key === 79) && e.ctrlKey === true) {
e.preventDefault()
sideMenu.classList.toggle('open')
sideMenuFocus()
}
})
const sideMenuObserver = new MutationObserver((e) => {
if (body.getAttribute('id') !== 'body-settings') {
return
}
body.classList.toggle('body-settings-side-menu', sideMenu.classList.contains('open'))
})
sideMenuObserver.observe(sideMenu, {
attributes: true,
attributeFilter: ['class'],
childList: false,
characterData: false
})
})
body.appendChild(sideMenuContainer)
sideMenuContainer.appendChild(sideMenu)
<?php if ($_['loader-enabled'] === true): ?>
PageLoader()
<?php endif; ?>
if (nextcloud) {
if (logo && logo.parentNode !== nextcloud) {
nextcloud.appendChild(logo)
}
<?php if ($_['opener-position'] === 'before'): ?>
nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud)
<?php else: ?>
nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling)
<?php endif; ?>
}
})();

File diff suppressed because it is too large Load diff

View file

@ -16,250 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
vendor_script('side_menu', 'html5sortable.min'); use OCP\IURLGenerator;
script('side_menu', 'admin'); use OCP\IConfig;
style('side_menu', 'admin'); use OCA\SideMenu\AppInfo\Application;
$choicesYesNo = [ script('side_menu', 'side_menu-user');
'No' => '0',
'Yes' => '1',
];
$labelShowHideApps = 'Show and hide the list of applications';
$labelReset = 'Reset to default';
?> ?>
<div id="side-menu-section">
<?php if ($_['force']): ?>
<div class="section">
<h2>
<?php p($l->t('Menu')); ?>
</h2>
<p> <div id="side-menu-user-settings"></div>
<em><?php echo $l->t('You do not have permission to change the settings.'); ?></em>
</p>
</div>
<?php else: ?>
<div class="section">
<p>
<em><?php echo $l->t('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.'); ?></em>
</p>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Enable the custom menu')); ?>
</div>
<div class="side-menu-setting-form">
<select id="side-menu-enabled" name="enabled" class="side-menu-setting" data-personal>
<?php foreach ($choicesYesNo as $label => $value): ?>
<option value="<?php echo $value ?>" <?php if ($value === $_['enabled']): ?>selected<?php endif; ?>>
<?php echo $l->t($label); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Open apps in new tab')); ?>
</div>
<div class="side-menu-setting-form">
<?php $choices = [
'Use the global setting' => '1',
'Use my selection' => '2',
]; ?>
<select id="side-menu-loader-enabled" name="target-blank-mode" class="side-menu-setting" data-personal>
<?php foreach ($choices as $label => $value): ?>
<option value="<?php echo $value ?>" <?php if ($value === $_['target-blank-mode']): ?>selected<?php endif; ?>>
<?php echo $l->t($label); ?>
</option>
<?php endforeach; ?>
</select>
<p>
<a class="side-menu-toggler" data-target="#target-blank-apps" href="#_">
🖱️ <?php p($l->t($labelShowHideApps)); ?>
</a>
</p>
<div class="side-menu-setting" data-name="target-blank-apps" id="target-blank-apps" data-personal data-checkbox style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['apps'] as $app): ?>
<li class="side-menu-setting-list-item">
<input
type="checkbox"
name="target-blank-apps[]"
value="<?php echo $app['id'] ?>"
id="target-blank-app-<?php echo $app['id'] ?>"
<?php if (in_array($app['id'], $_['target-blank-apps'])): ?>checked<?php endif; ?>
/>
<label for="target-blank-app-<?php echo $app['id'] ?>">
<?php echo p($l->t($app['name'])); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>
<?php p($l->t('Top menu')); ?>
</h2>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Applications kept in the top menu')); ?>
<p>
<em>
<?php p($l->t('If there is no selection then the global configuration is applied.')); ?>
</em>
</p>
</div>
<div class="side-menu-setting-form">
<p>
<a class="side-menu-toggler" data-target="#top-menu-apps" href="#_">
🖱️ <?php p($l->t($labelShowHideApps)); ?>
</a>
</p>
<div class="side-menu-setting" data-name="top-menu-apps" data-checkbox data-personal id="top-menu-apps" style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['apps'] as $app): ?>
<li class="side-menu-setting-list-item">
<input
type="checkbox"
name="top-menu-apps[]"
value="<?php echo $app['id'] ?>"
id="top-menu-app-<?php echo $app['id'] ?>"
<?php if (in_array($app['id'], $_['top-menu-apps'])): ?>checked<?php endif; ?>
/>
<label for="top-menu-app-<?php echo $app['id'] ?>">
<?php echo $app['name'] ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</div>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Applications kept in the top menu but also shown in side menu')); ?>
<p>
<em>
<?php p($l->t('These applications must be selected in the previous option.')); ?><br>
<?php p($l->t('If there is no selection then the global configuration is applied.')); ?>
</em>
</p>
</div>
<div class="side-menu-setting-form">
<p>
<a class="side-menu-toggler" data-target="#top-side-menu-apps" href="#_">
🖱️ <?php p($l->t($labelShowHideApps)); ?>
</a>
</p>
<div class="side-menu-setting" data-name="top-side-menu-apps" data-checkbox data-personal id="top-side-menu-apps" style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['apps'] as $app): ?>
<li class="side-menu-setting-list-item">
<input
type="checkbox"
name="top-side-menu-apps[]"
value="<?php echo $app['id'] ?>"
id="top-side-menu-app-<?php echo $app['id'] ?>"
<?php if (in_array($app['id'], $_['top-side-menu-apps'])): ?>checked<?php endif; ?>
/>
<label for="top-side-menu-app-<?php echo $app['id'] ?>">
<?php echo $app['name'] ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>
<?php p($l->t('Applications')); ?>
</h2>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Customize sorting')); ?>
</div>
<div class="side-menu-setting-form">
<a class="side-menu-toggler" data-target="#apps-order-list" href="#_">
🖱️ <?php p($l->t($labelShowHideApps)); ?>
</a>
<div class="theme-undo icon icon-history btn-reset btn-reset--down" data-toggle="tooltip" data-original-title="<?php echo p($l->t($labelReset)); ?>" data-reset="<?php echo htmlentities(json_encode([
'side-menu-apps-order' => '[]',
])) ?>"></div>
<div id="apps-order-list" style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['ordered-apps'] as $key => $app): ?>
<li data-id="<?php echo $app['id']; ?>" class="side-menu-setting-list-item">
<span class="arrow">
</span>
<?php echo p($l->t($app['name'])); ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<input type="hidden" value='<?php echo json_encode($_['apps-order']) ?>' name="apps-order" class="side-menu-setting" id="side-menu-apps-order" data-personal>
</div>
</div>
</div>
</div>
<?php endif ?>
<div class="section">
<?php if (!$_['force']): ?>
<button id="side-menu-save" class="btn btn-info" arial-label="<?php p($l->t('Save')); ?>">
<?php p($l->t('Save')); ?>
<progress max="100" value="0" id="side-menu-save-progress"></progress>
</button>
<span id="side-menu-message" class="msg"></span>
<div style="height: 30px"></div>
<?php endif ?>
<div>
<span for="side-menu-opener">
<?php p($l->t('You like this app and you want to support me?')); ?>
<a style="margin-left: 10px" target="_blank" href="https://www.buymeacoffee.com/deblan" rel="noopener">
<button arial-label="<?php p($l->t('Buy me a coffee ☕')); ?>">
<?php p($l->t('Buy me a coffee ☕')); ?>
</button>
</a>
</span>
</div>
</div>
</div>

View file

@ -1,33 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./src/**/*.js"],
"compilerOptions": {
"types": ["node", "vue", "vue-router"],
"outDir": "./js/",
"target": "ESNext",
"module": "ESNext",
// Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
// Allow ts to import js files
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"noImplicitAny": false,
"resolveJsonModule": true,
"strict": true,
},
"vueCompilerOptions": {
"target": 2.7
},
"ts-node": {
// these options are overrides used only by ts-node
// same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"verbatimModuleSyntax": false
}
}
}

93
webpack.config.js Normal file
View file

@ -0,0 +1,93 @@
const path = require('path')
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')
const rules = require('./webpack.rules.js')
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const appName = 'side_menu'
const buildMode = process.env.NODE_ENV
const isDev = buildMode === 'development'
module.exports = {
target: 'web',
mode: buildMode,
devtool: false,
entry: {
menu: path.resolve(path.join('src', 'menu.js')),
admin: path.resolve(path.join('src', 'admin.js')),
user: path.resolve(path.join('src', 'user.js')),
},
output: {
path: path.resolve('./js'),
publicPath: path.join('/apps/', appName, '/js/'),
// Output file names
filename: `${appName}-[name].js?v=[contenthash]`,
chunkFilename: `${appName}-[name].js?v=[contenthash]`,
// Clean output before each build
clean: true,
},
optimization: {
chunkIds: 'named',
splitChunks: {
automaticNameDelimiter: '-',
minSize: 10000,
maxSize: 250000,
},
minimize: !isDev,
minimizer: [
new TerserPlugin({
terserOptions: {
output: {
comments: false,
}
},
extractComments: true,
}),
],
},
module: {
rules: Object.values(rules),
},
plugins: [
new VueLoaderPlugin(),
// Make sure we auto-inject node polyfills on demand
// https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed
new NodePolyfillPlugin({
// Console is available in the web-browser
excludeAliases: ['console'],
}),
// @nextcloud/moment since v1.3.0 uses `moment/min/moment-with-locales.js`
// Which works only in Node.js and is not compatible with Webpack bundling
// It has an unused function `localLocale` that requires locales by invalid relative path `./locale`
// Though it is not used, Webpack tries to resolve it with `require.context` and fails
new webpack.IgnorePlugin({
resourceRegExp: /^\.[/\\]locale$/,
contextRegExp: /moment[/\\]min$/,
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
],
resolve: {
extensions: ['.*', '.mjs', '.js', '.vue'],
symlinks: false,
// Ensure npm does not duplicate vue dependency, and that npm link works for vue 3
// See https://github.com/vuejs/core/issues/1503
// See https://github.com/nextcloud/nextcloud-vue/issues/3281
alias: {
'vue$': path.resolve('./node_modules/vue')
},
},
}

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