feat(files): move userconfig to dedicated store and fix crop previews

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
pull/36534/head
John Molakvoæ 2023-03-31 12:46:46 +07:00
parent 044e824260
commit c7c9ee1ebd
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
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({

@ -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 {
},
},
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() {

@ -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,