mirror of
https://github.com/nextcloud/server.git
synced 2026-04-20 22:00:39 -04:00
feat(files): move userconfig to dedicated store and fix crop previews
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
044e824260
commit
c7c9ee1ebd
9 changed files with 171 additions and 49 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import logger from '../logger.js'
|
|||
const actions = getFileActions()
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListActionsHeader',
|
||||
name: 'FilesListHeaderActions',
|
||||
|
||||
components: {
|
||||
CustomSvgIconRender,
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
76
apps/files/src/store/userconfig.ts
Normal file
76
apps/files/src/store/userconfig.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const userConfigStore = useUserConfigStore()
|
||||
return {
|
||||
userConfigStore,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
...userConfig,
|
||||
|
||||
// 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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue