Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c3e6a245b | |||
| 80f6dcf73a | |||
| 73dea84d89 | |||
| 22e87e12ad | |||
| ccefefa6f7 | |||
| d666cac5be | |||
| 9a71429182 | |||
| f1b495809f | |||
| e44b2c70a7 | |||
| 52de7326ec | |||
| 4975743441 | |||
| be6e9eaced | |||
| 627b9e8bf4 | |||
| 7bc64003a5 | |||
| d078cc68e7 | |||
| 55a2b21ed1 | |||
| f7a8a41f6e | |||
| c2c6399106 | |||
| f4833d1956 | |||
| 4d453676dc | |||
| 0cb14258ad | |||
| 7e7397735b | |||
| ccfb541404 | |||
| f4e35bdc5d |
@@ -1,5 +1,32 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- fix(config): store the app store category cache lazily with only the names in the languages in use, refresh it weekly and when the settings are saved, and load it only when categories are used
|
||||||
|
- fix(admin): save all settings in one request so changes are not lost after the page reloads
|
||||||
|
- fix(config): remove the old category cache from the app config on upgrade
|
||||||
|
|
||||||
|
## 6.2.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- feat(app): add compatibility with Nextcloud 35
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- fix #510 #417: [Bug] Styling of the icon labels and overflow icon is not correct
|
||||||
|
|
||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- feat: add search toggle option (fix #509)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- fix #510: [Bug] Styling of the icon labels and overflow icon is not correct
|
||||||
|
- fix #462: [Bug] Burger menu in settings is hidden with "Always displayed" on mobile device
|
||||||
|
|
||||||
## 6.0.1
|
## 6.0.1
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ Users can disable the menu using the page of personal settings.
|
|||||||
|
|
||||||
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
|
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
|
||||||
|
|
||||||
|
The names of the app store categories are cached for a week, only in the languages that are in use on the instance (and English). If a user switches to a new language and sees English category names, save the settings on the administration page to refresh the cache right away.
|
||||||
|
|
||||||
### Use first top menu app as default app
|
### Use first top menu app as default app
|
||||||
|
|
||||||
You can easily let Custom Menu redirect to the first app in the top menu by changing the following parameter in your `config/config.php`:
|
You can easily let Custom Menu redirect to the first app in the top menu by changing the following parameter in your `config/config.php`:
|
||||||
|
|||||||
+7
-2
@@ -30,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>6.0.1</version>
|
<version>6.2.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>
|
||||||
@@ -53,8 +53,13 @@ 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.5" />
|
<php min-version="8.1" max-version="8.5" />
|
||||||
<nextcloud min-version="34" max-version="34"/>
|
<nextcloud min-version="34" max-version="35"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
<repair-steps>
|
||||||
|
<post-migration>
|
||||||
|
<step>OCA\SideMenu\Migration\ClearCategoryCache</step>
|
||||||
|
</post-migration>
|
||||||
|
</repair-steps>
|
||||||
<settings>
|
<settings>
|
||||||
<admin>OCA\SideMenu\Settings\Admin</admin>
|
<admin>OCA\SideMenu\Settings\Admin</admin>
|
||||||
<admin-section>OCA\SideMenu\Settings\AdminSection</admin-section>
|
<admin-section>OCA\SideMenu\Settings\AdminSection</admin-section>
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ use OCA\SideMenu\Service\AppRepository;
|
|||||||
use OCA\SideMenu\Service\CategoryRepository;
|
use OCA\SideMenu\Service\CategoryRepository;
|
||||||
use OCA\SideMenu\Service\Color;
|
use OCA\SideMenu\Service\Color;
|
||||||
use OCA\SideMenu\Service\ConfigProxy;
|
use OCA\SideMenu\Service\ConfigProxy;
|
||||||
|
use OCA\SideMenu\Service\LangRepository;
|
||||||
use OCA\Theming\ThemingDefaults;
|
use OCA\Theming\ThemingDefaults;
|
||||||
use OCP\AppFramework\App;
|
use OCP\AppFramework\App;
|
||||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
use OCP\EventDispatcher\IEventDispatcher;
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
|
use OCP\IAppConfig;
|
||||||
|
use OCP\ICacheFactory;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\INavigationManager;
|
use OCP\INavigationManager;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
@@ -56,7 +60,11 @@ class Application extends App implements IBootstrap
|
|||||||
$c->get(ConfigProxy::class),
|
$c->get(ConfigProxy::class),
|
||||||
$c->get(IConfig::class),
|
$c->get(IConfig::class),
|
||||||
$c->get(IFactory::class),
|
$c->get(IFactory::class),
|
||||||
$c->get(IUserSession::class)
|
$c->get(IUserSession::class),
|
||||||
|
$c->get(IAppConfig::class),
|
||||||
|
$c->get(ITimeFactory::class),
|
||||||
|
$c->get(LangRepository::class),
|
||||||
|
$c->get(ICacheFactory::class)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
namespace OCA\SideMenu\Controller;
|
namespace OCA\SideMenu\Controller;
|
||||||
|
|
||||||
use OCA\SideMenu\AppInfo\Application;
|
use OCA\SideMenu\AppInfo\Application;
|
||||||
|
use OCA\SideMenu\Service\CategoryRepository;
|
||||||
use OCA\SideMenu\Service\Color;
|
use OCA\SideMenu\Service\Color;
|
||||||
use OCA\SideMenu\Service\ConfigProxy;
|
use OCA\SideMenu\Service\ConfigProxy;
|
||||||
use OCA\SideMenu\Service\LangRepository;
|
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\Attribute\PasswordConfirmationRequired;
|
||||||
use OCP\AppFramework\Http\DataDownloadResponse;
|
use OCP\AppFramework\Http\DataDownloadResponse;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
@@ -35,6 +37,12 @@ use OCP\IURLGenerator;
|
|||||||
|
|
||||||
class AdminSettingController extends Controller
|
class AdminSettingController extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Keys that are managed by the app or by Nextcloud itself ("enabled")
|
||||||
|
* and must not be written from the settings form.
|
||||||
|
*/
|
||||||
|
protected const UNSAVED_KEYS = ['cache', 'cache-categories', 'enabled'];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$appName,
|
$appName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
@@ -43,6 +51,7 @@ class AdminSettingController extends Controller
|
|||||||
protected IURLGenerator $urlGenerator,
|
protected IURLGenerator $urlGenerator,
|
||||||
protected Color $color,
|
protected Color $color,
|
||||||
protected LangRepository $langRepository,
|
protected LangRepository $langRepository,
|
||||||
|
protected CategoryRepository $categoryRepository,
|
||||||
) {
|
) {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
}
|
}
|
||||||
@@ -51,7 +60,7 @@ class AdminSettingController extends Controller
|
|||||||
#[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')]
|
#[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')]
|
||||||
public function removeCache(): RedirectResponse
|
public function removeCache(): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]');
|
$this->categoryRepository->clearCache();
|
||||||
|
|
||||||
return new RedirectResponse($this->urlGenerator->linkToRoute('settings.AdminSettings.index', [
|
return new RedirectResponse($this->urlGenerator->linkToRoute('settings.AdminSettings.index', [
|
||||||
'section' => Application::APP_ID,
|
'section' => Application::APP_ID,
|
||||||
@@ -97,6 +106,7 @@ class AdminSettingController extends Controller
|
|||||||
'use-avatar',
|
'use-avatar',
|
||||||
'add-logo-link',
|
'add-logo-link',
|
||||||
'show-settings',
|
'show-settings',
|
||||||
|
'show-search-form',
|
||||||
'loader-enabled',
|
'loader-enabled',
|
||||||
'always-displayed',
|
'always-displayed',
|
||||||
'enabled',
|
'enabled',
|
||||||
@@ -130,13 +140,84 @@ class AdminSettingController extends Controller
|
|||||||
'top-menu-mouse-over-hidden-label',
|
'top-menu-mouse-over-hidden-label',
|
||||||
];
|
];
|
||||||
|
|
||||||
$defaults = [
|
$defaults = $this->getDefaults();
|
||||||
|
|
||||||
|
$config = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!isset($defaults[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($key, $booleans)) {
|
||||||
|
$config[$key] = $this->configProxy->getAppValueBool($key, $defaults[$key]);
|
||||||
|
} elseif (in_array($key, $arrays)) {
|
||||||
|
$config[$key] = $this->configProxy->getAppValueArray($key, $defaults[$key]);
|
||||||
|
} elseif (in_array($key, $integers)) {
|
||||||
|
$config[$key] = $this->configProxy->getAppValueInt($key, $defaults[$key]);
|
||||||
|
} else {
|
||||||
|
$config[$key] = $this->configProxy->getAppValue($key, $defaults[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($defaults as $key => $default) {
|
||||||
|
if (array_key_exists($key, $config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($key, $booleans)) {
|
||||||
|
$config[$key] = $default === '1';
|
||||||
|
} elseif (in_array($key, $arrays)) {
|
||||||
|
$config[$key] = json_decode($default);
|
||||||
|
} elseif (in_array($key, $integers)) {
|
||||||
|
$config[$key] = (int) $default;
|
||||||
|
} else {
|
||||||
|
$config[$key] = $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$config['langs'] = $this->langRepository->getUsedLangs();
|
||||||
|
|
||||||
|
return new JSONResponse($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[PasswordConfirmationRequired]
|
||||||
|
#[FrontpageRoute(verb: 'POST', url: '/admin/config')]
|
||||||
|
public function saveConfiguration(array $config): JSONResponse
|
||||||
|
{
|
||||||
|
$allowed = array_diff(array_keys($this->getDefaults()), self::UNSAVED_KEYS);
|
||||||
|
|
||||||
|
foreach ($config as $key => $value) {
|
||||||
|
if (!in_array($key, $allowed, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$value = json_encode($value);
|
||||||
|
} elseif (is_bool($value)) {
|
||||||
|
$value = $value ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config->setAppValue(Application::APP_ID, $key, (string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New stylesheet URL, and category names that match the new settings
|
||||||
|
$this->config->setAppValue(Application::APP_ID, 'cache', (string) time());
|
||||||
|
$this->categoryRepository->clearCache();
|
||||||
|
|
||||||
|
return new JSONResponse(['status' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaults(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
'opener-only' => '0',
|
'opener-only' => '0',
|
||||||
'opener-hover' => '0',
|
'opener-hover' => '0',
|
||||||
'display-logo' => '1',
|
'display-logo' => '1',
|
||||||
'use-avatar' => '0',
|
'use-avatar' => '0',
|
||||||
'add-logo-link' => '1',
|
'add-logo-link' => '1',
|
||||||
'show-settings' => '0',
|
'show-settings' => '0',
|
||||||
|
'show-search-form' => '1',
|
||||||
'loader-enabled' => '1',
|
'loader-enabled' => '1',
|
||||||
'always-displayed' => '0',
|
'always-displayed' => '0',
|
||||||
'enabled' => '1',
|
'enabled' => '1',
|
||||||
@@ -186,33 +267,5 @@ class AdminSettingController extends Controller
|
|||||||
|
|
||||||
'categories-order-type' => 'default',
|
'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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ class JsController extends Controller
|
|||||||
'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'),
|
'display-logo' => $this->config->getAppValueBool('display-logo', '1'),
|
||||||
'use-avatar' => $this->config->getAppValueBool('use-avatar', '0'),
|
'use-avatar' => $this->config->getAppValueBool('use-avatar', '0'),
|
||||||
|
'show-search-form' => $this->config->getAppValueBool('show-search-form', '1'),
|
||||||
'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'),
|
'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'),
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\SideMenu\Migration;
|
||||||
|
|
||||||
|
use OCA\SideMenu\Service\CategoryRepository;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\IRepairStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earlier versions stored the category cache as a regular app config
|
||||||
|
* value, which Nextcloud loads on every request. Clear it on upgrade so
|
||||||
|
* it does not linger on instances that never request the categories.
|
||||||
|
*/
|
||||||
|
class ClearCategoryCache implements IRepairStep
|
||||||
|
{
|
||||||
|
public function __construct(protected CategoryRepository $categoryRepository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'Clear the category cache';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(IOutput $output): void
|
||||||
|
{
|
||||||
|
$this->categoryRepository->clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,8 @@ class AppRepository
|
|||||||
{
|
{
|
||||||
$navigation = $this->navigationManager->getAll();
|
$navigation = $this->navigationManager->getAll();
|
||||||
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
|
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
|
||||||
$categories = $this->categoryRepository->getOrderedCategories();
|
// Only needed to check custom categories, skip loading them otherwise
|
||||||
|
$categories = empty(array_filter($appCategoriesCustom)) ? [] : $this->categoryRepository->getOrderedCategories();
|
||||||
$apps = $this->ocApp->listAllApps();
|
$apps = $this->ocApp->listAllApps();
|
||||||
$visibleApps = [];
|
$visibleApps = [];
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ namespace OCA\SideMenu\Service;
|
|||||||
|
|
||||||
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
||||||
use OCA\SideMenu\AppInfo\Application;
|
use OCA\SideMenu\AppInfo\Application;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\Exceptions\AppConfigTypeConflictException;
|
||||||
|
use OCP\IAppConfig;
|
||||||
|
use OCP\ICache;
|
||||||
|
use OCP\ICacheFactory;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
use OCP\L10N\IFactory;
|
use OCP\L10N\IFactory;
|
||||||
@@ -15,12 +20,23 @@ use OCP\L10N\IFactory;
|
|||||||
*/
|
*/
|
||||||
class CategoryRepository
|
class CategoryRepository
|
||||||
{
|
{
|
||||||
|
public const CACHE_KEY = 'cache-categories';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app store categories rarely change, refresh them once a week.
|
||||||
|
*/
|
||||||
|
protected const CACHE_TTL = 604800;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected CategoryFetcher $categoryFetcher,
|
protected CategoryFetcher $categoryFetcher,
|
||||||
protected ConfigProxy $config,
|
protected ConfigProxy $config,
|
||||||
protected IConfig $iConfig,
|
protected IConfig $iConfig,
|
||||||
protected IFactory $l10nFactory,
|
protected IFactory $l10nFactory,
|
||||||
protected IUserSession $userSession,
|
protected IUserSession $userSession,
|
||||||
|
protected IAppConfig $appConfig,
|
||||||
|
protected ITimeFactory $timeFactory,
|
||||||
|
protected LangRepository $langRepository,
|
||||||
|
protected ICacheFactory $cacheFactory,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +48,9 @@ class CategoryRepository
|
|||||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||||
$type = $this->config->getAppValue('categories-order-type', 'default');
|
$type = $this->config->getAppValue('categories-order-type', 'default');
|
||||||
$order = $this->config->getAppValueArray('categories-order', '[]');
|
$order = $this->config->getAppValueArray('categories-order', '[]');
|
||||||
$categoriesLabels = $this->config->getAppValueArray('cache-categories', '[]');
|
$categoriesLabels = $this->getCategories();
|
||||||
$customCategories = $this->config->getAppValueArray('categories-custom', '[]');
|
$customCategories = $this->config->getAppValueArray('categories-custom', '[]');
|
||||||
|
|
||||||
if (empty($categoriesLabels)) {
|
|
||||||
$categoriesLabels = $this->categoryFetcher->get();
|
|
||||||
|
|
||||||
$this->iConfig->setAppValue(Application::APP_ID, 'cache-categories', json_encode($categoriesLabels));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($categoriesLabels as $k => $category) {
|
foreach ($categoriesLabels as $k => $category) {
|
||||||
$categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ??
|
$categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ??
|
||||||
$category['translations']['en']['name'];
|
$category['translations']['en']['name'];
|
||||||
@@ -85,4 +95,109 @@ class CategoryRepository
|
|||||||
|
|
||||||
return $categoriesLabels;
|
return $categoriesLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
$this->getDistributedCache()?->remove(self::CACHE_KEY);
|
||||||
|
$this->appConfig->deleteKey(Application::APP_ID, self::CACHE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The languages to keep in the cache: English as fallback, the
|
||||||
|
* default language and the languages of the users, as two-letter
|
||||||
|
* codes like getOrderedCategories() looks them up. A user who picks
|
||||||
|
* a new language sees English names until the cache is refreshed,
|
||||||
|
* which saving the settings does right away.
|
||||||
|
*/
|
||||||
|
protected function getCachedLangs(): array
|
||||||
|
{
|
||||||
|
$langs = $this->langRepository->getUsedLangs();
|
||||||
|
$langs[] = (string) $this->iConfig->getSystemValue('default_language', 'en');
|
||||||
|
$langs[] = (string) $this->iConfig->getSystemValue('force_language', 'en');
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (string $lang) => strtolower(substr($lang, 0, 2)),
|
||||||
|
array_merge(['en'], $langs)
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the app store categories with only their names. They are
|
||||||
|
* cached in the distributed cache (e.g. Redis) when one is configured,
|
||||||
|
* otherwise in a lazy app config value, so they are not loaded on
|
||||||
|
* every request with the other settings.
|
||||||
|
*/
|
||||||
|
protected function getCategories(): array
|
||||||
|
{
|
||||||
|
$cache = $this->getDistributedCache();
|
||||||
|
|
||||||
|
if ($cache !== null) {
|
||||||
|
$categories = $cache->get(self::CACHE_KEY);
|
||||||
|
|
||||||
|
if (is_array($categories)) {
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$stored = $this->appConfig->getValueArray(Application::APP_ID, self::CACHE_KEY, [], lazy: true);
|
||||||
|
} catch (AppConfigTypeConflictException) {
|
||||||
|
// Stored with another type, e.g. set by hand; refreshed below
|
||||||
|
$stored = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($stored['expires'], $stored['categories']) && $stored['expires'] > $this->timeFactory->getTime()) {
|
||||||
|
return $stored['categories'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = $this->fetchCategories();
|
||||||
|
|
||||||
|
// Don't cache an empty list, e.g. when the app store is unreachable
|
||||||
|
if (empty($categories)) {
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->clearCache();
|
||||||
|
|
||||||
|
if ($cache !== null) {
|
||||||
|
$cache->set(self::CACHE_KEY, $categories, self::CACHE_TTL);
|
||||||
|
} else {
|
||||||
|
$this->appConfig->setValueArray(Application::APP_ID, self::CACHE_KEY, [
|
||||||
|
'expires' => $this->timeFactory->getTime() + self::CACHE_TTL,
|
||||||
|
'categories' => $categories,
|
||||||
|
], lazy: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchCategories(): array
|
||||||
|
{
|
||||||
|
$langs = $this->getCachedLangs();
|
||||||
|
$categories = [];
|
||||||
|
|
||||||
|
foreach ($this->categoryFetcher->get() as $category) {
|
||||||
|
$names = [];
|
||||||
|
|
||||||
|
foreach ($category['translations'] ?? [] as $lang => $translation) {
|
||||||
|
if (in_array($lang, $langs, true)) {
|
||||||
|
$names[$lang] = ['name' => $translation['name'] ?? ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories[] = [
|
||||||
|
'id' => $category['id'],
|
||||||
|
'translations' => $names,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDistributedCache(): ?ICache
|
||||||
|
{
|
||||||
|
return $this->cacheFactory->isAvailable()
|
||||||
|
? $this->cacheFactory->createDistributed(Application::APP_ID)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import { translate as t } from '@nextcloud/l10n'
|
||||||
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { waitPasswordConfirmation } from '../../lib/setting.js'
|
import { waitPasswordConfirmation } from '../../lib/setting.js'
|
||||||
@@ -46,52 +49,28 @@ const filterConfig = (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
const data = filterConfig(config)
|
|
||||||
const size = Object.keys(data).length
|
|
||||||
let counter = 0
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
const update = () => {
|
try {
|
||||||
++counter
|
await waitPasswordConfirmation()
|
||||||
|
} catch {
|
||||||
if (counter === size) {
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!error.value) {
|
try {
|
||||||
|
// One request for all settings: separate requests per setting could
|
||||||
|
// leave a half-saved configuration in Nextcloud's config cache when
|
||||||
|
// the page reloads.
|
||||||
|
await axios.post(generateUrl('/apps/side_menu/admin/config'), {
|
||||||
|
config: filterConfig(config),
|
||||||
|
})
|
||||||
location.reload()
|
location.reload()
|
||||||
}
|
} catch {
|
||||||
}
|
error.value = t('side_menu', 'Error while saving the settings')
|
||||||
}
|
|
||||||
|
|
||||||
waitPasswordConfirmation()
|
|
||||||
.then(() => {
|
|
||||||
for (let key in data) {
|
|
||||||
let value = data[key]
|
|
||||||
|
|
||||||
if (Array.isArray(value) || typeof value === 'object') {
|
|
||||||
value = JSON.stringify(value)
|
|
||||||
} else if (typeof value === 'boolean') {
|
|
||||||
value = value ? '1' : '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
OCP.AppConfig.setValue('side_menu', key, value.toString(), {
|
|
||||||
success() {
|
|
||||||
update()
|
|
||||||
},
|
|
||||||
error() {
|
|
||||||
error.value = `Error while saving ${key}`
|
|
||||||
update()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
counter = 0
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
error.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Přizpůsobit'
|
'Customize': 'Přizpůsobit'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Zavřít'
|
'Close': 'Zavřít'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Customize'
|
'Customize': 'Customize'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Close'
|
'Close': 'Close'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Personalizar'
|
'Customize': 'Personalizar'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Cerrar'
|
'Close': 'Cerrar'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -128,3 +128,4 @@
|
|||||||
'Customize': 'Personnaliser'
|
'Customize': 'Personnaliser'
|
||||||
'Custom': 'Personnalisé'
|
'Custom': 'Personnalisé'
|
||||||
'Close': 'Fermer'
|
'Close': 'Fermer'
|
||||||
|
'Show the search form': 'Afficher le formulaire de recherche'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Personalizar'
|
'Customize': 'Personalizar'
|
||||||
'Custom': 'Personalizado'
|
'Custom': 'Personalizado'
|
||||||
'Close': 'Pechar'
|
'Close': 'Pechar'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Customize'
|
'Customize': 'Customize'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Close'
|
'Close': 'Close'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -107,3 +107,4 @@
|
|||||||
'Customize': 'Customize'
|
'Customize': 'Customize'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Close'
|
'Close': 'Close'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Приспособить'
|
'Customize': 'Приспособить'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Закрыть'
|
'Close': 'Закрыть'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -107,3 +107,4 @@
|
|||||||
'Customize': 'Customize'
|
'Customize': 'Customize'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Close'
|
'Close': 'Close'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Anpassa'
|
'Customize': 'Anpassa'
|
||||||
'Custom': 'Anpassad'
|
'Custom': 'Anpassad'
|
||||||
'Close': 'Stäng'
|
'Close': 'Stäng'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': 'Customize'
|
'Customize': 'Customize'
|
||||||
'Custom': 'Custom'
|
'Custom': 'Custom'
|
||||||
'Close': 'Close'
|
'Close': 'Close'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -109,3 +109,4 @@
|
|||||||
'Customize': '自定义'
|
'Customize': '自定义'
|
||||||
'Custom': '自定义'
|
'Custom': '自定义'
|
||||||
'Close': '关闭'
|
'Close': '关闭'
|
||||||
|
'Show the search form': 'Show the search form'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
:label="settings.name"
|
:label="settings.name"
|
||||||
:avatar="settings.avatar"
|
:avatar="settings.avatar"
|
||||||
/>
|
/>
|
||||||
<AppSearch v-model="search" />
|
<AppSearch v-if="showSearchForm" v-model="search" />
|
||||||
<OpenerButton
|
<OpenerButton
|
||||||
v-if="!openerHover"
|
v-if="!openerHover"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
@@ -102,6 +102,7 @@ const activeApp = ref(null)
|
|||||||
const targetBlankApps = ref([])
|
const targetBlankApps = ref([])
|
||||||
const settings = ref(null)
|
const settings = ref(null)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
const showSearchForm = ref(true)
|
||||||
const openerHover = ref(false)
|
const openerHover = ref(false)
|
||||||
const menu = useTemplateRef('menu')
|
const menu = useTemplateRef('menu')
|
||||||
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
|
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
|
||||||
@@ -120,6 +121,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
targetBlankApps.value = config['target-blank-apps']
|
targetBlankApps.value = config['target-blank-apps']
|
||||||
settings.value = config['settings']
|
settings.value = config['settings']
|
||||||
|
showSearchForm.value = config['show-search-form']
|
||||||
openerHover.value = config['opener-hover'] && !isTouchDevice
|
openerHover.value = config['opener-hover'] && !isTouchDevice
|
||||||
|
|
||||||
items.value = await navStore.getCategories()
|
items.value = await navStore.getCategories()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
:avatar="settings.avatar"
|
:avatar="settings.avatar"
|
||||||
/>
|
/>
|
||||||
<AppSearch
|
<AppSearch
|
||||||
v-if="open"
|
v-if="showSearchForm && open"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
/>
|
/>
|
||||||
<OpenerButton
|
<OpenerButton
|
||||||
@@ -103,6 +103,7 @@ const openerHover = ref(false)
|
|||||||
const alwaysDisplayed = ref(false)
|
const alwaysDisplayed = ref(false)
|
||||||
const displayLogo = ref(false)
|
const displayLogo = ref(false)
|
||||||
const useAvatarAsLogo = ref(false)
|
const useAvatarAsLogo = ref(false)
|
||||||
|
const showSearchForm = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const apps = ref([])
|
const apps = ref([])
|
||||||
const menu = useTemplateRef('menu')
|
const menu = useTemplateRef('menu')
|
||||||
@@ -151,6 +152,7 @@ onMounted(async () => {
|
|||||||
alwaysDisplayed.value = config['always-displayed']
|
alwaysDisplayed.value = config['always-displayed']
|
||||||
targetBlankApps.value = config['target-blank-apps']
|
targetBlankApps.value = config['target-blank-apps']
|
||||||
forceLightIcon.value = config['force-light-icon']
|
forceLightIcon.value = config['force-light-icon']
|
||||||
|
showSearchForm.value = config['show-search-form']
|
||||||
|
|
||||||
avatar.value = config['avatar']
|
avatar.value = config['avatar']
|
||||||
logo.value = config['logo']
|
logo.value = config['logo']
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
:label="settings.name"
|
:label="settings.name"
|
||||||
:avatar="settings.avatar"
|
:avatar="settings.avatar"
|
||||||
/>
|
/>
|
||||||
<AppSearch v-model="search" />
|
<AppSearch v-if="showSearchForm" v-model="search" />
|
||||||
<OpenerButton
|
<OpenerButton
|
||||||
v-if="!openerHover"
|
v-if="!openerHover"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
@@ -107,6 +107,7 @@ const activeApp = ref(null)
|
|||||||
const targetBlankApps = ref([])
|
const targetBlankApps = ref([])
|
||||||
const settings = ref(null)
|
const settings = ref(null)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
const showSearchForm = ref(true)
|
||||||
const openerHover = ref(false)
|
const openerHover = ref(false)
|
||||||
const menu = useTemplateRef('menu')
|
const menu = useTemplateRef('menu')
|
||||||
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
|
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
|
||||||
@@ -125,6 +126,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
targetBlankApps.value = config['target-blank-apps']
|
targetBlankApps.value = config['target-blank-apps']
|
||||||
settings.value = config['settings']
|
settings.value = config['settings']
|
||||||
|
showSearchForm.value = config['show-search-form']
|
||||||
openerHover.value = config['opener-hover'] && !isTouchDevice
|
openerHover.value = config['opener-hover'] && !isTouchDevice
|
||||||
|
|
||||||
items.value = await navStore.getCategories()
|
items.value = await navStore.getCategories()
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
/>
|
/>
|
||||||
</NcAppNavigation>
|
</NcAppNavigation>
|
||||||
<NcAppContent class="cm-settings cm-settings--nav">
|
<NcAppContent class="cm-settings cm-settings--nav">
|
||||||
|
<button @click="toggleNav" class="cm-settings-nav-toggle">{{ trans('Toggle menu') }}</button>
|
||||||
|
|
||||||
<SettingsSection :hidden="section !== 'panel'">
|
<SettingsSection :hidden="section !== 'panel'">
|
||||||
<SectionTitle label="Panel" />
|
<SectionTitle label="Panel" />
|
||||||
|
|
||||||
@@ -80,6 +82,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
<FormYesNo v-model="config['show-settings']" />
|
<FormYesNo v-model="config['show-settings']" />
|
||||||
</SettingValue>
|
</SettingValue>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem :disabled="config['always-displayed']">
|
||||||
|
<SettingLabel
|
||||||
|
label="Show the search form"
|
||||||
|
:middle="true"
|
||||||
|
/>
|
||||||
|
<SettingValue>
|
||||||
|
<FormYesNo v-model="config['show-search-form']" />
|
||||||
|
</SettingValue>
|
||||||
|
</SettingItem>
|
||||||
<SettingItem>
|
<SettingItem>
|
||||||
<SettingLabel
|
<SettingLabel
|
||||||
label="Icons"
|
label="Icons"
|
||||||
@@ -619,6 +630,10 @@ const filterConfig = (value) => {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleNav = () => {
|
||||||
|
document.querySelector('.cm-settings-nav .app-navigation-toggle-wrapper > button').click()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
config.value = await configStore.getAppConfig()
|
config.value = await configStore.getAppConfig()
|
||||||
|
|
||||||
|
|||||||
+15
-4
@@ -15,14 +15,25 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.cm-settings {
|
.cm-settings-nav {
|
||||||
&--nav {
|
padding-top: 30px !important;
|
||||||
padding-top: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-settings-nav-toggle {
|
||||||
|
margin-top: 40px !important;
|
||||||
|
margin-left: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-settings-nav .app-navigation-toggle-wrapper > button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-settings {
|
||||||
&-nav {
|
&-nav {
|
||||||
|
max-width: 50vw !important;
|
||||||
|
|
||||||
.app-navigation__content {
|
.app-navigation__content {
|
||||||
padding: 20px;
|
padding: 20px 10px 20px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-navigation-entry-icon {
|
.app-navigation-entry-icon {
|
||||||
|
|||||||
+20
-10
@@ -287,14 +287,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-standardmenu {
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
&.show {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-always-displayed {
|
.cm-always-displayed {
|
||||||
body {
|
body {
|
||||||
width: calc(100% - 50px) !important;
|
width: calc(100% - 50px) !important;
|
||||||
@@ -426,11 +418,25 @@
|
|||||||
$header-icon-size: 20px;
|
$header-icon-size: 20px;
|
||||||
|
|
||||||
.cm-standardmenu {
|
.cm-standardmenu {
|
||||||
|
visibility: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-background-plain-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-menu-entry--label {
|
||||||
|
color: var(--color-background-plain-text) !important;
|
||||||
|
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.app-menu-main {
|
.app-menu-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -490,7 +496,7 @@ $header-icon-size: 20px;
|
|||||||
color: var(--color-primary-text);
|
color: var(--color-primary-text);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 45%;
|
top: 55%;
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -562,7 +568,10 @@ $header-icon-size: 20px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-menu-more .button-vue--vue-tertiary {
|
&-app-menu-more {
|
||||||
|
padding-top: 9px !important;
|
||||||
|
|
||||||
|
.button-vue--vue-tertiary {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin: 8px 3px 3px 3px;
|
margin: 8px 3px 3px 3px;
|
||||||
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
|
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
|
||||||
@@ -581,6 +590,7 @@ $header-icon-size: 20px;
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-app-menu-popover-entry {
|
&-app-menu-popover-entry {
|
||||||
.app-icon {
|
.app-icon {
|
||||||
|
|||||||
Reference in New Issue
Block a user