Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot d0b3a8e178 chore(deps): update dependency css-loader to v7.1.4 2026-05-25 08:02:00 +00:00
33 changed files with 816 additions and 2611 deletions
+251 -646
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -49,8 +49,6 @@ 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`:
+2 -7
View File
@@ -30,7 +30,7 @@ Notice
Because I believe in a free and decentralized Internet, [Gitnet](https://gitnet.fr) is **self-hosted at home**. 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.2.0</version> <version>5.3.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,13 +53,8 @@ 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="35"/> <nextcloud min-version="31" max-version="33"/>
</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>
+8 -19
View File
@@ -6,29 +6,21 @@ use OC\AllConfig;
use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\AppFramework\Http\Request; use OC\AppFramework\Http\Request;
use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OC\Security\CSRF\CsrfTokenManager;
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\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\IURLGenerator;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Util; use OCP\Util;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@@ -42,9 +34,9 @@ class Application extends App implements IBootstrap
public const APP_ID = 'side_menu'; public const APP_ID = 'side_menu';
public const APP_NAME = 'Custom menu'; public const APP_NAME = 'Custom menu';
protected IConfig $config; protected AllConfig $config;
protected ContentSecurityPolicyNonceManager $cspnm; protected ContentSecurityPolicyNonceManager $cspnm;
protected IRequest $request; protected Request $request;
protected ?User $user = null; protected ?User $user = null;
public function __construct(array $urlParams = []) public function __construct(array $urlParams = [])
@@ -60,11 +52,7 @@ 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)
); );
}); });
@@ -95,9 +83,10 @@ class Application extends App implements IBootstrap
public function boot(IBootContext $context): void public function boot(IBootContext $context): void
{ {
$this->config = Server::get(IConfig::class); $this->config = \OC::$server->getConfig();
$this->user = Server::get(IUserSession::class)->getUser(); $this->cspnm = \OC::$server->getContentSecurityPolicyNonceManager();
$this->request = Server::get(IRequest::class); $this->user = \OC::$server[IUserSession::class]->getUser();
$this->request = \OC::$server->getRequest();
if (!$this->isEnabled()) { if (!$this->isEnabled()) {
return; return;
@@ -149,7 +138,7 @@ class Application extends App implements IBootstrap
$cache = $this->config->getAppValue(self::APP_ID, 'cache', '0'); $cache = $this->config->getAppValue(self::APP_ID, 'cache', '0');
foreach ($assets as $value) { foreach ($assets as $value) {
$route = Server::get(IURLGenerator::class)->linkToRoute( $route = \OC::$server->getURLGenerator()->linkToRoute(
$value['route'], $value['route'],
['v' => $cache] ['v' => $cache]
); );
+30 -83
View File
@@ -20,14 +20,12 @@
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;
@@ -37,12 +35,6 @@ 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,
@@ -51,7 +43,6 @@ 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);
} }
@@ -60,7 +51,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->categoryRepository->clearCache(); $this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]');
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,
@@ -106,7 +97,6 @@ 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',
@@ -140,84 +130,13 @@ class AdminSettingController extends Controller
'top-menu-mouse-over-hidden-label', 'top-menu-mouse-over-hidden-label',
]; ];
$defaults = $this->getDefaults(); $defaults = [
$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',
@@ -267,5 +186,33 @@ 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);
} }
} }
-1
View File
@@ -135,7 +135,6 @@ 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'),
-46
View File
@@ -1,46 +0,0 @@
<?php
/**
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\SideMenu\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();
}
}
+1 -2
View File
@@ -40,8 +40,7 @@ class AppRepository
{ {
$navigation = $this->navigationManager->getAll(); $navigation = $this->navigationManager->getAll();
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]'); $appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
// Only needed to check custom categories, skip loading them otherwise $categories = $this->categoryRepository->getOrderedCategories();
$categories = empty(array_filter($appCategoriesCustom)) ? [] : $this->categoryRepository->getOrderedCategories();
$apps = $this->ocApp->listAllApps(); $apps = $this->ocApp->listAllApps();
$visibleApps = []; $visibleApps = [];
+7 -122
View File
@@ -4,11 +4,6 @@ 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;
@@ -20,23 +15,12 @@ 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,
) { ) {
} }
@@ -48,9 +32,15 @@ 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->getCategories(); $categoriesLabels = $this->config->getAppValueArray('cache-categories', '[]');
$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'];
@@ -95,109 +85,4 @@ 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;
}
} }
+422 -1534
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@
"@babel/core": ">=7.12.0 <8.0.0", "@babel/core": ">=7.12.0 <8.0.0",
"@formatjs/intl-segmenter": "^12.0.8", "@formatjs/intl-segmenter": "^12.0.8",
"@nextcloud/router": "^3.0.1", "@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^9.9.0", "@nextcloud/vue": "^9.0.0-alpha.8",
"node-polyfill-webpack-plugin": "^4.1.0", "node-polyfill-webpack-plugin": "^4.1.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"postcss": "^7.0.0 || ^8.0.1", "postcss": "^7.0.0 || ^8.0.1",
+40 -19
View File
@@ -20,9 +20,6 @@
</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'
@@ -49,28 +46,52 @@ 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
try { const update = () => {
await waitPasswordConfirmation() ++counter
} catch {
loading.value = false if (counter === size) {
return loading.value = false
if (!error.value) {
location.reload()
}
}
} }
try { waitPasswordConfirmation()
// One request for all settings: separate requests per setting could .then(() => {
// leave a half-saved configuration in Nextcloud's config cache when for (let key in data) {
// the page reloads. let value = data[key]
await axios.post(generateUrl('/apps/side_menu/admin/config'), {
config: filterConfig(config), if (Array.isArray(value) || typeof value === 'object') {
value = JSON.stringify(value)
} else if (typeof value === 'boolean') {
value = value ? '1' : '0'
}
OCP.AppConfig.setValue('side_menu', key, value.toString(), {
success() {
update()
},
error() {
error.value = `Error while saving ${key}`
update()
},
})
}
})
.catch(() => {
counter = 0
loading.value = false
error.value = null
}) })
location.reload()
} catch {
error.value = t('side_menu', 'Error while saving the settings')
loading.value = false
}
} }
</script> </script>
-1
View File
@@ -109,4 +109,3 @@
'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'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Customize' 'Customize': 'Customize'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Close' 'Close': 'Close'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Personalizar' 'Customize': 'Personalizar'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Cerrar' 'Close': 'Cerrar'
'Show the search form': 'Show the search form'
-1
View File
@@ -128,4 +128,3 @@
'Customize': 'Personnaliser' 'Customize': 'Personnaliser'
'Custom': 'Personnalisé' 'Custom': 'Personnalisé'
'Close': 'Fermer' 'Close': 'Fermer'
'Show the search form': 'Afficher le formulaire de recherche'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Personalizar' 'Customize': 'Personalizar'
'Custom': 'Personalizado' 'Custom': 'Personalizado'
'Close': 'Pechar' 'Close': 'Pechar'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Customize' 'Customize': 'Customize'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Close' 'Close': 'Close'
'Show the search form': 'Show the search form'
-1
View File
@@ -107,4 +107,3 @@
'Customize': 'Customize' 'Customize': 'Customize'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Close' 'Close': 'Close'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Приспособить' 'Customize': 'Приспособить'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Закрыть' 'Close': 'Закрыть'
'Show the search form': 'Show the search form'
-1
View File
@@ -107,4 +107,3 @@
'Customize': 'Customize' 'Customize': 'Customize'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Close' 'Close': 'Close'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Anpassa' 'Customize': 'Anpassa'
'Custom': 'Anpassad' 'Custom': 'Anpassad'
'Close': 'Stäng' 'Close': 'Stäng'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': 'Customize' 'Customize': 'Customize'
'Custom': 'Custom' 'Custom': 'Custom'
'Close': 'Close' 'Close': 'Close'
'Show the search form': 'Show the search form'
-1
View File
@@ -109,4 +109,3 @@
'Customize': '自定义' 'Customize': '自定义'
'Custom': '自定义' 'Custom': '自定义'
'Close': '关闭' 'Close': '关闭'
'Show the search form': 'Show the search form'
+1 -3
View File
@@ -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-if="showSearchForm" v-model="search" /> <AppSearch v-model="search" />
<OpenerButton <OpenerButton
v-if="!openerHover" v-if="!openerHover"
@click="$emit('close')" @click="$emit('close')"
@@ -102,7 +102,6 @@ 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
@@ -121,7 +120,6 @@ 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()
+1 -3
View File
@@ -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="showSearchForm && open" v-if="open"
v-model="search" v-model="search"
/> />
<OpenerButton <OpenerButton
@@ -103,7 +103,6 @@ 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')
@@ -152,7 +151,6 @@ 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']
+12 -17
View File
@@ -62,9 +62,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</li> </li>
</ul> </ul>
<NcActions <NcActions
class="cm-standardmenu-app-menu-more" class="cm-standardmenu-app-menu-more app-menu-more"
:aria-label="t('core', 'More apps')" :aria-label="t('core', 'More apps')"
:forceMenu="true"
> >
<NcActionLink <NcActionLink
v-for="app in popoverAppList" v-for="app in popoverAppList"
@@ -133,27 +132,19 @@ const setApps = (value) => {
const appLimit = () => { const appLimit = () => {
const headerStart = document.querySelector('#header .header-start') const headerStart = document.querySelector('#header .header-start')
const headerEnd = document.querySelector('#header .header-end') const headerEnd = document.querySelector('#header .header-end')
const unifiedSearch = document.querySelector('#header .unified-search-input')
const body = document.querySelector('body') const body = document.querySelector('body')
let totalWidth = body.offsetWidth let size = (headerEnd ? headerEnd.offsetWidth : 0) + 70
let otherElementsWidth = headerEnd ? headerEnd.offsetWidth : 0
if (unifiedSearch && headerEnd.offsetWidth < unifiedSearch.offsetWidth) {
otherElementsWidth += unifiedSearch.offsetWidth
}
let maxWidth = body.offsetWidth - otherElementsWidth
if (headerStart) { if (headerStart) {
Array.from(headerStart.children).forEach((child) => { Array.from(headerStart.children).forEach((child) => {
if (child.id !== 'app-menu-container') { if (child.id !== 'app-menu-container') {
maxWidth -= child.offsetWidth size += child.offsetWidth
} }
}) })
} }
return Math.max(0, Math.floor(maxWidth / 60)) return Math.max(0, Math.floor((body.offsetWidth - size) / 70))
} }
const makeStyle = (app) => { const makeStyle = (app) => {
@@ -165,9 +156,13 @@ const makeStyle = (app) => {
} }
const computeLists = () => { const computeLists = () => {
const limit = appLimit() mainAppList.value = appList.value.slice(0, appLimit())
mainAppList.value = appList.value.slice(0, limit) popoverAppList.value = appList.value.slice(appLimit()).sort((a, b) => a.order - b.order)
popoverAppList.value = appList.value.slice(limit).sort((a, b) => a.order - b.order) }
const reComputeLists = (delay) => {
window.clearTimeout(resizeTimeout)
resizeTimeout = window.setTimeout(computeLists, delay || 100)
} }
onMounted(async () => { onMounted(async () => {
@@ -182,7 +177,7 @@ onMounted(async () => {
setApps(await navStore.getCoreApps()) setApps(await navStore.getCoreApps())
window.setInterval(computeLists, 100) window.addEventListener('resize', reComputeLists)
}) })
</script> </script>
+1 -3
View File
@@ -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-if="showSearchForm" v-model="search" /> <AppSearch v-model="search" />
<OpenerButton <OpenerButton
v-if="!openerHover" v-if="!openerHover"
@click="$emit('close')" @click="$emit('close')"
@@ -107,7 +107,6 @@ 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
@@ -126,7 +125,6 @@ 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()
-15
View File
@@ -29,8 +29,6 @@ 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" />
@@ -82,15 +80,6 @@ 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"
@@ -630,10 +619,6 @@ 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()
+5 -16
View File
@@ -15,25 +15,14 @@
* 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-nav {
padding-top: 30px !important;
}
.cm-settings-nav-toggle {
margin-top: 40px !important;
margin-left: 15px !important;
}
.cm-settings-nav .app-navigation-toggle-wrapper > button {
display: none;
}
.cm-settings { .cm-settings {
&-nav { &--nav {
max-width: 50vw !important; padding-top: 30px;
}
&-nav {
.app-navigation__content { .app-navigation__content {
padding: 20px 10px 20px 10px; padding: 20px;
} }
.app-navigation-entry-icon { .app-navigation-entry-icon {
+22 -58
View File
@@ -287,6 +287,14 @@
} }
} }
.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;
@@ -355,16 +363,6 @@
right: 0 !important; right: 0 !important;
margin-left: 0 !important; margin-left: 0 !important;
} }
#content {
left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px);
}
#content-vue {
width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px;
}
} }
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
@@ -387,16 +385,6 @@
padding: 10px 0; padding: 10px 0;
} }
} }
#header {
.unified-search-input {
width: 80px !important;
.unified-search-input__label {
display: none;
}
}
}
} }
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
@@ -418,25 +406,11 @@
$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;
@@ -496,7 +470,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: 55%; top: 45%;
display: block; display: block;
min-width: 100%; min-width: 100%;
transform: translateX(-50%); transform: translateX(-50%);
@@ -568,28 +542,24 @@ $header-icon-size: 20px;
} }
} }
&-app-menu-more { .app-menu-more .button-vue--vue-tertiary {
padding-top: 9px !important; opacity: 0.7;
margin: 8px 3px 3px 3px;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
.button-vue--vue-tertiary { &:not([aria-expanded='true']) {
opacity: 0.7; color: var(--color-main-text);
margin: 8px 3px 3px 3px;
filter: var(--background-image-invert-if-bright, var(--primary-invert-if-bright));
&:not([aria-expanded='true']) { &:hover {
color: var(--color-main-text);
&:hover {
opacity: 1;
background-color: transparent !important;
}
}
&:focus-visible {
opacity: 1; opacity: 1;
outline: none !important; background-color: transparent !important;
} }
} }
&:focus-visible {
opacity: 1;
outline: none !important;
}
} }
&-app-menu-popover-entry { &-app-menu-popover-entry {
@@ -629,9 +599,3 @@ $header-icon-size: 20px;
display: none; display: none;
} }
} }
#header {
.unified-search-input:not(.unified-search-input--mobile) {
position: unset !important;
}
}
+12
View File
@@ -117,3 +117,15 @@ body[data-theme-light], body[data-theme-light-highcontrast] {
font-size: 16px; font-size: 16px;
} }
<?php } ?> <?php } ?>
<?php if ($_['always-displayed']) { ?>
#content {
left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px);
}
#content-vue {
width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px;
}
<?php } ?>
-2
View File
@@ -7,7 +7,6 @@ const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin')
const appName = 'side_menu' const appName = 'side_menu'
const appVersion = '6.x'
const buildMode = process.env.NODE_ENV const buildMode = process.env.NODE_ENV
const isDev = buildMode === 'development' const isDev = buildMode === 'development'
@@ -81,7 +80,6 @@ module.exports = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
appName: JSON.stringify(appName), appName: JSON.stringify(appName),
appVersion: JSON.stringify(appVersion),
}), }),
], ],