diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-03-31 12:46:46 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-06 14:49:32 +0200 |
commit | c7c9ee1ebdd07fdd5cf295835d5cf5e061fc40af (patch) | |
tree | 2f1b19981435e221717ff42e8215553a319c7bad | |
parent | 044e8242602907c8fa73989aa8149cf879de8881 (diff) | |
download | nextcloud-server-c7c9ee1ebdd07fdd5cf295835d5cf5e061fc40af.tar.gz nextcloud-server-c7c9ee1ebdd07fdd5cf295835d5cf5e061fc40af.zip |
feat(files): move userconfig to dedicated store and fix crop previews
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 76 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeader.vue | 6 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderActions.vue (renamed from apps/files/src/components/FilesListActionsHeader.vue) | 2 | ||||
-rw-r--r-- | apps/files/src/main.js | 10 | ||||
-rw-r--r-- | apps/files/src/store/userconfig.ts | 76 | ||||
-rw-r--r-- | apps/files/src/types.ts | 8 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 6 | ||||
-rw-r--r-- | apps/files/src/views/Settings.vue | 34 | ||||
-rw-r--r-- | apps/files_trashbin/src/services/trashbin.ts | 2 |
9 files changed, 171 insertions, 49 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index fa4c67cb553..71a0c4e2a65 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -89,7 +89,10 @@ :key="column.id" :class="`files-list__row-${currentView?.id}-${column.id}`" class="files-list__row-column-custom"> - <CustomElementRender v-if="active" :current-view="currentView" :render="column.render" :source="source" /> + <CustomElementRender v-if="active" + :current-view="currentView" + :render="column.render" + :source="source" /> </td> </Fragment> </template> @@ -99,27 +102,25 @@ import { debounce } from 'debounce' import { Folder, File, getFileActions, formatFileSize } from '@nextcloud/files' import { Fragment } from 'vue-fragment' import { join } from 'path' -import { loadState } from '@nextcloud/initial-state' +import { mapState } from 'pinia' +import { showError } from '@nextcloud/dialogs' import { translate } from '@nextcloud/l10n' import FileIcon from 'vue-material-design-icons/File.vue' import FolderIcon from 'vue-material-design-icons/Folder.vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import Vue from 'vue' -import { showError } from '@nextcloud/dialogs' import { useFilesStore } from '../store/files' import { useSelectionStore } from '../store/selection' +import { useUserConfigStore } from '../store/userconfig' import CustomElementRender from './CustomElementRender.vue' import CustomSvgIconRender from './CustomSvgIconRender.vue' import logger from '../logger.js' +import { UserConfig } from '../types' -// TODO: move to store -// TODO: watch 'files:config:updated' event -const userConfig = loadState('files', 'config', {}) // The preview service worker cache name (see webpack config) const SWCacheName = 'previews' @@ -160,9 +161,11 @@ export default Vue.extend({ setup() { const filesStore = useFilesStore() const selectionStore = useSelectionStore() + const userConfigStore = useUserConfigStore() return { filesStore, selectionStore, + userConfigStore, } }, @@ -171,11 +174,15 @@ export default Vue.extend({ backgroundFailed: false, backgroundImage: '', loading: '', - userConfig, } }, computed: { + /** @return {UserConfig} */ + userConfig() { + return this.userConfigStore.userConfig + }, + /** @return {Navigation} */ currentView() { return this.$navigation.active @@ -244,11 +251,18 @@ export default Vue.extend({ }, }, + cropPreviews() { + return this.userConfig.crop_image_previews + }, + previewUrl() { try { const url = new URL(window.location.origin + this.source.attributes.previewUrl) - const cropping = this.userConfig?.crop_image_previews === true - url.searchParams.set('a', cropping ? '1' : '0') + // Request tiny previews + url.searchParams.set('x', '32') + url.searchParams.set('y', '32') + // Handle cropping + url.searchParams.set('a', this.cropPreviews === true ? '1' : '0') return url.href } catch (e) { return null @@ -287,8 +301,8 @@ export default Vue.extend({ }, watch: { - active(active) { - if (active === false) { + active(active, before) { + if (active === false && before === true) { this.resetState() // When the row is not active anymore @@ -296,6 +310,7 @@ export default Vue.extend({ this.$el.parentNode.style.display = 'none' return } + // Restore default tabindex this.$el.parentNode.style.display = '' }, @@ -303,8 +318,8 @@ export default Vue.extend({ * When the source changes, reset the preview * and fetch the new one. */ - source() { - this.resetState() + previewUrl() { + this.clearImg() this.debounceIfNotCached() }, }, @@ -313,12 +328,18 @@ export default Vue.extend({ * The row is mounted once and reused as we scroll. */ mounted() { - // Init the debounce function on mount and - // not when the module is imported ⚠ + // ⚠ Init the debounce function on mount and + // not when the module is imported to + // avoid sharing between recycled components this.debounceGetPreview = debounce(function() { this.fetchAndApplyPreview() }, 150, false) + // ⚠ Init img on mount and + // not when the module is imported to + // avoid sharing between recycled components + this.img = null + this.debounceIfNotCached() }, @@ -345,7 +366,18 @@ export default Vue.extend({ }, fetchAndApplyPreview() { + // Ignore if no preview + if (!this.previewUrl) { + return + } + + // If any image is being processed, reset it + if (this.img) { + this.clearImg() + } + this.img = new Image() + this.img.fetchpriority = this.active ? 'high' : 'auto' this.img.onload = () => { this.backgroundImage = `url(${this.previewUrl})` } @@ -360,19 +392,23 @@ export default Vue.extend({ this.loading = '' // Reset the preview + this.clearImg() + + // Close menu + this.$refs.actionsMenu.closeMenu() + }, + + clearImg() { this.backgroundImage = '' this.backgroundFailed = false - // If we're already fetching a preview, cancel it if (this.img) { // Do not fail on cancel this.img.onerror = null this.img.src = '' - delete this.img } - // Close menu - this.$refs.actionsMenu.closeMenu() + this.img = null }, isCachedPreview(previewUrl) { diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue index 66b2845ea11..184ca7aa30e 100644 --- a/apps/files/src/components/FilesListHeader.vue +++ b/apps/files/src/components/FilesListHeader.vue @@ -26,7 +26,7 @@ </th> <!-- Actions multiple if some are selected --> - <FilesListActionsHeader v-if="!isNoneSelected" + <FilesListHeaderActions v-if="!isNoneSelected" :current-view="currentView" :selected-nodes="selectedNodes" /> @@ -74,7 +74,7 @@ import Vue from 'vue' import { useFilesStore } from '../store/files' import { useSelectionStore } from '../store/selection' import { useSortingStore } from '../store/sorting' -import FilesListActionsHeader from './FilesListActionsHeader.vue' +import FilesListHeaderActions from './FilesListHeaderActions.vue' import FilesListHeaderButton from './FilesListHeaderButton.vue' import logger from '../logger.js' import Navigation from '../services/Navigation' @@ -85,7 +85,7 @@ export default Vue.extend({ components: { FilesListHeaderButton, NcCheckboxRadioSwitch, - FilesListActionsHeader, + FilesListHeaderActions, }, props: { diff --git a/apps/files/src/components/FilesListActionsHeader.vue b/apps/files/src/components/FilesListHeaderActions.vue index 9abb30c6c2e..8f519734284 100644 --- a/apps/files/src/components/FilesListActionsHeader.vue +++ b/apps/files/src/components/FilesListHeaderActions.vue @@ -57,7 +57,7 @@ import logger from '../logger.js' const actions = getFileActions() export default Vue.extend({ - name: 'FilesListActionsHeader', + name: 'FilesListHeaderActions', components: { CustomSvgIconRender, diff --git a/apps/files/src/main.js b/apps/files/src/main.js index 5aca12754b4..e516275ba28 100644 --- a/apps/files/src/main.js +++ b/apps/files/src/main.js @@ -18,11 +18,14 @@ import SettingsModel from './models/Setting.js' import router from './router/router.js' - // Init private and public Files namespace window.OCA.Files = window.OCA.Files ?? {} window.OCP.Files = window.OCP.Files ?? {} +// Init Pinia store +Vue.use(PiniaVuePlugin) +const pinia = createPinia() + // Init Navigation Service const Navigation = new NavigationService() Object.assign(window.OCP.Files, { Navigation }) @@ -41,13 +44,10 @@ const FilesNavigationRoot = new View({ Navigation, }, router, + pinia, }) FilesNavigationRoot.$mount('#app-navigation-files') -// Init Pinia store -Vue.use(PiniaVuePlugin) -const pinia = createPinia() - // Init content list view const ListView = Vue.extend(FilesListView) const FilesList = new ListView({ diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts new file mode 100644 index 00000000000..d432b502acf --- /dev/null +++ b/apps/files/src/store/userconfig.ts @@ -0,0 +1,76 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license AGPL-3.0-or-later + * + * 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/>. + * + */ +/* eslint-disable */ +import { loadState } from '@nextcloud/initial-state' +import { generateUrl } from '@nextcloud/router' +import { defineStore } from 'pinia' +import Vue from 'vue' +import axios from '@nextcloud/axios' +import type { UserConfig, UserConfigStore } from '../types' +import { emit, subscribe } from '@nextcloud/event-bus' +import type { update } from 'cypress/types/lodash' + +const userConfig = loadState('files', 'config', { + show_hidden: false, + crop_image_previews: true, +}) as UserConfig + +export const useUserConfigStore = () => { + const store = defineStore('userconfig', { + state: () => ({ + userConfig, + } as UserConfigStore), + + actions: { + /** + * Update the user config local store + */ + onUpdate(key: string, value: boolean) { + Vue.set(this.userConfig, key, value) + }, + + /** + * Update the user config local store AND on server side + */ + async update(key: string, value: boolean) { + await axios.post(generateUrl('/apps/files/api/v1/config/' + key), { + value, + }) + + emit('files:config:updated', { key, value }) + } + } + }) + + const userConfigStore = store() + + // Make sure we only register the listeners once + if (!userConfigStore.initialized) { + subscribe('files:config:updated', function({ key, value }: { key: string, value: boolean }) { + userConfigStore.onUpdate(key, value) + }) + userConfigStore.initialized = true + } + + return userConfigStore +} + diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 207a45c080b..28e1bad28a6 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -71,3 +71,11 @@ export interface SortingConfig { export interface SortingStore { [key: string]: SortingConfig } + +// User config store +export interface UserConfig { + [key: string]: boolean +} +export interface UserConfigStore { + userConfig: UserConfig +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 8b615e2cd95..065749adaf5 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -75,7 +75,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import TrashCan from 'vue-material-design-icons/TrashCan.vue' import Vue from 'vue' -import { ContentsWithRoot } from '../services/Navigation' +import Navigation, { ContentsWithRoot } from '../services/Navigation' import { useFilesStore } from '../store/files' import { usePathsStore } from '../store/paths' import { useSelectionStore } from '../store/selection' @@ -83,7 +83,6 @@ import { useSortingStore } from '../store/sorting' import BreadCrumbs from '../components/BreadCrumbs.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' import logger from '../logger.js' -import Navigation from '../services/Navigation' export default Vue.extend({ name: 'FilesList', @@ -127,6 +126,7 @@ export default Vue.extend({ /** * The current directory query. + * * @return {string} */ dir() { @@ -136,6 +136,7 @@ export default Vue.extend({ /** * The current folder. + * * @return {Folder|undefined} */ currentFolder() { @@ -161,6 +162,7 @@ export default Vue.extend({ /** * The current directory contents. + * * @return {Node[]} */ dirContents() { diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index 9a117b70e22..b1c544f8ecf 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -26,11 +26,11 @@ @update:open="onClose"> <!-- Settings API--> <NcAppSettingsSection id="settings" :title="t('files', 'Files settings')"> - <NcCheckboxRadioSwitch :checked.sync="show_hidden" + <NcCheckboxRadioSwitch :checked="userConfig.show_hidden" @update:checked="setConfig('show_hidden', $event)"> {{ t('files', 'Show hidden files') }} </NcCheckboxRadioSwitch> - <NcCheckboxRadioSwitch :checked.sync="crop_image_previews" + <NcCheckboxRadioSwitch :checked="userConfig.crop_image_previews" @update:checked="setConfig('crop_image_previews', $event)"> {{ t('files', 'Crop image previews') }} </NcCheckboxRadioSwitch> @@ -86,18 +86,11 @@ import Clipboard from 'vue-material-design-icons/Clipboard.vue' import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' import Setting from '../components/Setting.vue' -import { emit } from '@nextcloud/event-bus' import { generateRemoteUrl, generateUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import { loadState } from '@nextcloud/initial-state' import { showError, showSuccess } from '@nextcloud/dialogs' import { translate } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' - -const userConfig = loadState('files', 'config', { - show_hidden: false, - crop_image_previews: true, -}) +import { useUserConfigStore } from '../store/userconfig' export default { name: 'Settings', @@ -117,11 +110,15 @@ export default { }, }, - data() { + setup() { + const userConfigStore = useUserConfigStore() return { + userConfigStore, + } + }, - ...userConfig, - + data() { + return { // Settings API settings: window.OCA?.Files?.Settings?.settings || [], @@ -133,6 +130,12 @@ export default { } }, + computed: { + userConfig() { + return this.userConfigStore.userConfig + }, + }, + beforeMount() { // Update the settings API entries state this.settings.forEach(setting => setting.open()) @@ -149,10 +152,7 @@ export default { }, setConfig(key, value) { - emit('files:config:updated', { key, value }) - axios.post(generateUrl('/apps/files/api/v1/config/' + key), { - value, - }) + this.userConfigStore.update(key, value) }, async copyCloudId() { diff --git a/apps/files_trashbin/src/services/trashbin.ts b/apps/files_trashbin/src/services/trashbin.ts index 2be4c39dbfc..6592857b2a3 100644 --- a/apps/files_trashbin/src/services/trashbin.ts +++ b/apps/files_trashbin/src/services/trashbin.ts @@ -53,7 +53,7 @@ const data = `<?xml version="1.0"?> const resultToNode = function(node: FileStat): File | Folder { const permissions = parseWebdavPermissions(node.props?.permissions) const owner = getCurrentUser()?.uid as string - const previewUrl = generateUrl('/apps/files_trashbin/preview?fileId={fileid}', node.props) + const previewUrl = generateUrl('/apps/files_trashbin/preview?fileId={fileid}x=32&y=32', node.props) const nodeData = { id: node.props?.fileid as number || 0, |