add a way to create custom categories and manage application categories

This commit is contained in:
Simon Vieille 2022-01-11 19:59:51 +01:00
parent a9ca55390d
commit 30072500bc
13 changed files with 386 additions and 7 deletions

View File

@ -115,7 +115,7 @@
.side-menu-setting-form { .side-menu-setting-form {
display: table-cell; display: table-cell;
width: 300px; min-width: 300px;
} }
.side-menu-setting-label-short { .side-menu-setting-label-short {

View File

@ -21,10 +21,27 @@ class AppRepository
*/ */
protected $l10nFactory; protected $l10nFactory;
public function __construct(\OC_App $ocApp, IFactory $l10nFactory) /**
* @var ConfigProxy
*/
protected $config;
/**
* @var CategoryRepository
*/
protected $categoryRepository;
public function __construct(
\OC_App $ocApp,
IFactory $l10nFactory,
ConfigProxy $config,
CategoryRepository $categoryRepository
)
{ {
$this->ocApp = $ocApp; $this->ocApp = $ocApp;
$this->l10nFactory = $l10nFactory; $this->l10nFactory = $l10nFactory;
$this->config = $config;
$this->categoryRepository = $categoryRepository;
} }
/** /**
@ -35,6 +52,9 @@ class AppRepository
public function getVisibleApps() public function getVisibleApps()
{ {
$navigation = $this->ocApp->getNavigation(); $navigation = $this->ocApp->getNavigation();
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
$categoriesCustom = $this->config->getAppValueArray('categories-custom', '[]');
$categories = $this->categoryRepository->getOrderedCategories();
$apps = $this->ocApp->listAllApps(); $apps = $this->ocApp->listAllApps();
$visibleApps = []; $visibleApps = [];
@ -74,6 +94,12 @@ class AppRepository
} }
} }
foreach ($visibleApps as $id => $app) {
if (isset($appCategoriesCustom[$id]) && (isset($categoriesCustom[$id]) || isset($categories[$id]))) {
$visibleApps[$id]['category'] = [$appCategoriesCustom[$id]];
}
}
usort($visibleApps, function ($a, $b) { usort($visibleApps, function ($a, $b) {
return ($a['name'] < $b['name']) ? -1 : 1; return ($a['name'] < $b['name']) ? -1 : 1;
}); });

View File

@ -5,6 +5,7 @@ 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\IConfig; use OCP\IConfig;
use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
/** /**
@ -34,16 +35,23 @@ class CategoryRepository
*/ */
protected $iConfig; protected $iConfig;
/**
* @var IUserSession
*/
protected $userSession;
public function __construct( public function __construct(
CategoryFetcher $categoryFetcher, CategoryFetcher $categoryFetcher,
ConfigProxy $config, ConfigProxy $config,
IConfig $iConfig, IConfig $iConfig,
IFactory $l10nFactory IFactory $l10nFactory,
IUserSession $userSession
) { ) {
$this->categoryFetcher = $categoryFetcher; $this->categoryFetcher = $categoryFetcher;
$this->l10nFactory = $l10nFactory; $this->l10nFactory = $l10nFactory;
$this->config = $config; $this->config = $config;
$this->iConfig = $iConfig; $this->iConfig = $iConfig;
$this->userSession = $userSession;
} }
/** /**
@ -56,8 +64,8 @@ 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->config->getAppValueArray('cache-categories', '[]');
$customCategories = $this->config->getAppValueArray('categories-custom', '[]');
if (empty($categoriesLabels)) { if (empty($categoriesLabels)) {
$categoriesLabels = $this->categoryFetcher->get(); $categoriesLabels = $this->categoryFetcher->get();
@ -74,6 +82,18 @@ class CategoryRepository
$categoriesLabels['external_links'] = $this->l10nFactory->get('external')->t('External sites'); $categoriesLabels['external_links'] = $this->l10nFactory->get('external')->t('External sites');
$categoriesLabels['other'] = ''; $categoriesLabels['other'] = '';
$user = $this->userSession->getUser();
if ($user) {
$lang = $this->iConfig->getUserValue($user->getUid(), 'core', 'lang');
} else {
$lang = 'en';
}
foreach ($customCategories as $category) {
$categoriesLabels[$category['id']] = $category[$lang] ?? $category['en'];
}
asort($categoriesLabels); asort($categoriesLabels);
if ('custom' === $type) { if ('custom' === $type) {

View File

@ -0,0 +1,43 @@
<?php
namespace OCA\SideMenu\Service;
use OCP\IDBConnection;
/**
* class LangRepository.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class LangRepository
{
/**
* @var IDBConnection
*/
protected $db;
public function __construct(IDBConnection $db)
{
$this->db = $db;
}
public function getUsedLangs(): array
{
$qb = $this->db->getQueryBuilder();
$qb->select($qb->createFunction('DISTINCT configvalue'))
->where('configkey="lang" and appid="core" and configvalue<>"en"')
->from('preferences')
;
$stmt = $qb->execute();
$langs = ['en'];
foreach ($stmt->fetchAll() as $result) {
$langs[] = $result['configvalue'];
}
return $langs;
}
}

View File

@ -28,6 +28,7 @@ use OCP\ILogger;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
use OCA\Theming\ThemingDefaults; use OCA\Theming\ThemingDefaults;
use OCA\SideMenu\Service\Color; use OCA\SideMenu\Service\Color;
use OCA\SideMenu\Service\LangRepository;
class Admin implements ISettings class Admin implements ISettings
{ {
@ -66,6 +67,11 @@ class Admin implements ISettings
*/ */
protected $color; protected $color;
/**
* @var LangRepository
*/
protected $langRepository;
public function __construct( public function __construct(
IL10N $l, IL10N $l,
ILogger $logger, ILogger $logger,
@ -73,7 +79,8 @@ class Admin implements ISettings
AppRepository $appRepository, AppRepository $appRepository,
CategoryRepository $categoryRepository, CategoryRepository $categoryRepository,
ThemingDefaults $theming, ThemingDefaults $theming,
Color $color Color $color,
LangRepository $langRepository
) { ) {
$this->l = $l; $this->l = $l;
$this->logger = $logger; $this->logger = $logger;
@ -82,6 +89,7 @@ class Admin implements ISettings
$this->categoryRepository = $categoryRepository; $this->categoryRepository = $categoryRepository;
$this->theming = $theming; $this->theming = $theming;
$this->color = $color; $this->color = $color;
$this->langRepository = $langRepository;
} }
/** /**
@ -140,10 +148,13 @@ class Admin implements ISettings
'target-blank-apps' => $this->config->getAppValueArray('target-blank-apps', '[]'), 'target-blank-apps' => $this->config->getAppValueArray('target-blank-apps', '[]'),
'top-menu-apps' => $this->config->getAppValueArray('top-menu-apps', '[]'), 'top-menu-apps' => $this->config->getAppValueArray('top-menu-apps', '[]'),
'default-enabled' => $this->config->getAppValue('default-enabled', '1'), 'default-enabled' => $this->config->getAppValue('default-enabled', '1'),
'apps' => $this->appRepository->getVisibleApps(),
'apps-categories-custom' => $this->config->getAppValueArray('apps-categories-custom', '[]'),
'categories-order-type' => $this->config->getAppValue('categories-order-type', 'default'), 'categories-order-type' => $this->config->getAppValue('categories-order-type', 'default'),
'categories-order' => $this->config->getAppValueArray('categories-order', '[]'), 'categories-order' => $this->config->getAppValueArray('categories-order', '[]'),
'apps' => $this->appRepository->getVisibleApps(), 'categories-custom' => $this->config->getAppValueArray('categories-custom', '[]'),
'categories' => $this->categoryRepository->getOrderedCategories(), 'categories' => $this->categoryRepository->getOrderedCategories(),
'langs' => $this->langRepository->getUsedLangs(),
]; ];
return new TemplateResponse(Application::APP_ID, 'settings/admin-form', $parameters, ''); return new TemplateResponse(Application::APP_ID, 'settings/admin-form', $parameters, '');

View File

@ -12,7 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@nextcloud/axios": "^1.8.0", "@nextcloud/axios": "^1.8.0",
"@nextcloud/vue": "^1.4.0", "@nextcloud/vue": "^1.5.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"trim": "0.0.1", "trim": "0.0.1",
"vue": "^2.6.11" "vue": "^2.6.11"

View File

@ -0,0 +1,182 @@
<!--
@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">
<li v-for="item in values" class="side-menu-setting-list-item" v-on:click="showEditForm(item)">
<span v-html="item.en"></span>
</li>
</ul>
<Actions>
<ActionButton @click="showAddForm" icon="icon-add"></ActionButton>
</Actions>
<Modal v-if="addForm" @close="hideAddForm">
<div class="modal__content">
<div v-for="lang in langs">
<span class="lang" v-html="lang"></span>
<input type="text" v-model="newValue[lang]" required>
</div>
<Actions>
<ActionButton @click="saveAdd" icon="icon-checkmark"></ActionButton>
</Actions>
</div>
</Modal>
<Modal v-if="editForm" @close="hideEditForm">
<div class="modal__content">
<div v-for="lang in langs">
<span class="lang" v-html="lang"></span>
<input type="text" v-model="editValue[lang]" required>
</div>
<div class="pull-right">
<Actions>
<ActionButton @click="removeEdit" icon="icon-delete"></ActionButton>
</Actions>
</div>
<Actions>
<ActionButton @click="saveEdit" icon="icon-checkmark"></ActionButton>
</Actions>
</div>
</Modal>
</div>
</template>
<style scoped>
.modal__content {
width: 200px;
padding: 10px;
}
.modal__content .lang {
width: 60px;
display: inline-block;
padding: 4px;
box-sizing: border-box;
}
.modal__content input[type=text] {
width: calc(100% - 85px);
display: inline-block;
margin: 0;
}
.pull-right {
float: right;
}
</style>
<script>
import Modal from '@nextcloud/vue/dist/Components/Modal'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
export default {
name: 'AdminCategoriesCustom',
components: {
Modal,
Actions,
ActionButton,
},
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

@ -15,6 +15,12 @@
* 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 Vue from 'vue'
Vue.prototype.OC = window.OC
Vue.prototype.OCA = window.OCA
let elements = [] let elements = []
const selector = '#side-menu-message' const selector = '#side-menu-message'
@ -108,7 +114,26 @@ const elementToggler = (element) => {
element.style.display = display 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', () => { document.addEventListener('DOMContentLoaded', () => {
const View = Vue.extend(AdminCategoriesCustom)
const adminCategoriesCustom = new View({})
adminCategoriesCustom.$mount('#side-menu-categories-custom')
elements = document.querySelectorAll('.side-menu-setting') elements = document.querySelectorAll('.side-menu-setting')
document.querySelector('#side-menu-save').addEventListener('click', (event) => { document.querySelector('#side-menu-save').addEventListener('click', (event) => {
@ -134,6 +159,12 @@ document.addEventListener('DOMContentLoaded', () => {
}) })
} }
for (let item of document.querySelectorAll('.apps-categories-custom')) {
item.addEventListener('change', (event) => {
updateAppsCategoriesCustom()
})
}
for (let item of document.querySelectorAll('.side-menu-setting-live')) { for (let item of document.querySelectorAll('.side-menu-setting-live')) {
item.addEventListener('change', (event) => { item.addEventListener('change', (event) => {
const target = event.target const target = event.target
@ -196,9 +227,12 @@ document.addEventListener('DOMContentLoaded', () => {
let value = [] let value = []
for (let item of document.querySelectorAll('#categories-list .side-menu-setting-list-item')) { for (let item of document.querySelectorAll('#categories-list .side-menu-setting-list-item')) {
console.log(item.getAttribute('data-id'))
value.push(item.getAttribute('data-id')) value.push(item.getAttribute('data-id'))
} }
console.log(value)
document.querySelector('input[name="categories-order"]').value = JSON.stringify(value) document.querySelector('input[name="categories-order"]').value = JSON.stringify(value)
}) })
} catch (e) { } catch (e) {

View File

@ -77,3 +77,6 @@
"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"
"Customize application categories": "Personnaliser les catégories des applications"
"Customize application categories": "Přizpůsobte kategorie aplikací"

View File

@ -77,3 +77,5 @@
"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"
"Customize application categories": "Anwendungskategorien anpassen"

View File

@ -77,3 +77,5 @@
"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"
"Customize application categories": "Personnaliser les catégories des applications"

View File

@ -77,3 +77,5 @@
"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": "自定义类别"
"Customize application categories": "自定义应用程序类别"

View File

@ -790,6 +790,60 @@ $choicesSizes = [
</div> </div>
</div> </div>
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Custom categories')); ?>
</div>
<div class="side-menu-setting-form">
<input type="hidden" name="categories-custom" class="side-menu-setting" data-langs="<?php echo htmlentities(json_encode($langs)) ?>" value="<?php echo htmlentities(json_encode($_['categories-custom'])) ?>">
<div id="side-menu-categories-custom">
</div>
</div>
</div>
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Customize application categories')); ?>
</div>
<div class="side-menu-setting-form">
<a class="side-menu-toggler" data-target="#apps-categories-custom-list" href="#_">
🖱️ <?php p($l->t('Show and hide the list of applications')); ?>
</a>
<div id="apps-categories-custom-list" style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['apps'] as $app): ?>
<li class="side-menu-setting-list-item">
<label for="apps-categories-custom-<?php echo $app['id'] ?>">
<?php echo p($l->t($app['name'])); ?>
</label>
<br>
<select data-app="<?php echo $app['id'] ?>" class="apps-categories-custom">
<option value=""></option>
<?php foreach ($_['categories'] as $id => $category): ?>
<?php if ($category): ?>
<option
value="<?php echo $id ?>"
<?php if (($_['apps-categories-custom'][$app['id']] ?? '') === $id): ?>
selected
<?php endif; ?>
><?php echo $category ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</li>
<?php endforeach; ?>
</ul>
</div>
<input type="hidden" class="side-menu-setting" id="apps-categories-custom" name="apps-categories-custom" value="<?php echo htmlentities(json_encode($_['apps-categories-custom'])) ?>">
</div>
</div>
<div class="side-menu-setting-row"> <div class="side-menu-setting-row">
<div class="side-menu-setting-label"> <div class="side-menu-setting-label">
<?php p($l->t('Customize sorting')); ?> <?php p($l->t('Customize sorting')); ?>