Merge pull request #39998 from nextcloud/feat/f2v/dnd

This commit is contained in:
John Molakvoæ 2023-09-26 20:33:11 +02:00 committed by GitHub
commit d395fa862b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 996 additions and 308 deletions

View file

@ -5,8 +5,9 @@
<name>Files</name>
<summary>File Management</summary>
<description>File Management</description>
<version>1.23.0</version>
<version>2.0.0</version>
<licence>agpl</licence>
<author>John Molakvoæ</author>
<author>Robin Appelman</author>
<author>Vincent Petry</author>
<types>

View file

@ -0,0 +1,248 @@
/**
* @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/>.
*
*/
import '@nextcloud/dialogs/style.css'
import type { Folder, Node, View } from '@nextcloud/files'
import type { IFilePickerButton } from '@nextcloud/dialogs'
// eslint-disable-next-line n/no-extraneous-import
import { AxiosError } from 'axios'
import { basename, join } from 'path'
import { emit } from '@nextcloud/event-bus'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { Permission, FileAction, FileType, NodeStatus } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import Vue from 'vue'
import CopyIcon from 'vue-material-design-icons/FileMultiple.vue'
import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw'
import MoveIcon from 'vue-material-design-icons/FolderMove.vue'
import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils'
import logger from '../logger'
/**
* Return the action that is possible for the given nodes
* @param {Node[]} nodes The nodes to check against
* @return {MoveCopyAction} The action that is possible for the given nodes
*/
const getActionForNodes = (nodes: Node[]): MoveCopyAction => {
if (canMove(nodes)) {
if (canCopy(nodes)) {
return MoveCopyAction.MOVE_OR_COPY
}
return MoveCopyAction.MOVE
}
// Assuming we can copy as the enabled checks for copy permissions
return MoveCopyAction.COPY
}
/**
* Handle the copy/move of a node to a destination
* This can be imported and used by other scripts/components on server
* @param {Node} node The node to copy/move
* @param {Folder} destination The destination to copy/move the node to
* @param {MoveCopyAction} method The method to use for the copy/move
* @param {boolean} overwrite Whether to overwrite the destination if it exists
* @return {Promise<void>} A promise that resolves when the copy/move is done
*/
export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, method: MoveCopyAction.COPY | MoveCopyAction.MOVE, overwrite = false) => {
if (!destination) {
return
}
if (destination.type !== FileType.Folder) {
throw new Error(t('files', 'Destination is not a folder'))
}
if (node.dirname === destination.path) {
throw new Error(t('files', 'This file/folder is already in that directory'))
}
if (node.path.startsWith(destination.path)) {
throw new Error(t('files', 'You cannot move a file/folder onto itself or into a subfolder of itself'))
}
const relativePath = join(destination.path, node.basename)
const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`)
logger.debug(`${method} ${node.basename} to ${destinationUrl}`)
// Set loading state
Vue.set(node, 'status', NodeStatus.LOADING)
const queue = getQueue()
return await queue.add(async () => {
try {
await axios({
method: method === MoveCopyAction.COPY ? 'COPY' : 'MOVE',
url: encodeURI(node.source),
headers: {
Destination: encodeURI(destinationUrl),
Overwrite: overwrite ? undefined : 'F',
},
})
// If we're moving, update the node
// if we're copying, we don't need to update the node
// the view will refresh itself
if (method === MoveCopyAction.MOVE) {
// Delete the node as it will be fetched again
// when navigating to the destination folder
emit('files:node:deleted', node)
}
} catch (error) {
if (error instanceof AxiosError) {
if (error?.response?.status === 412) {
throw new Error(t('files', 'A file or folder with that name already exists in this folder'))
} else if (error?.response?.status === 423) {
throw new Error(t('files', 'The files is locked'))
} else if (error?.response?.status === 404) {
throw new Error(t('files', 'The file does not exist anymore'))
} else if (error.message) {
throw new Error(error.message)
}
}
throw new Error()
} finally {
Vue.set(node, 'status', undefined)
}
})
}
/**
* Open a file picker for the given action
* @param {MoveCopyAction} action The action to open the file picker for
* @param {string} dir The directory to start the file picker in
* @param {Node} node The node to move/copy
* @return {Promise<boolean>} A promise that resolves to true if the action was successful
*/
const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: Node): Promise<boolean> => {
const filePicker = getFilePickerBuilder(t('files', 'Chose destination'))
.allowDirectories(true)
.setFilter((n: Node) => {
// We only want to show folders that we can create nodes in
return (n.permissions & Permission.CREATE) !== 0
// We don't want to show the current node in the file picker
&& node.fileid !== n.fileid
})
.setMimeTypeFilter([])
.setMultiSelect(false)
.startAt(dir)
return new Promise((resolve, reject) => {
filePicker.setButtonFactory((nodes: Node[], path: string) => {
const buttons: IFilePickerButton[] = []
const target = basename(path)
if (node.dirname === path) {
// This file/folder is already in that directory
return buttons
}
if (node.path === path) {
// You cannot move a file/folder onto itself
return buttons
}
if (action === MoveCopyAction.COPY || action === MoveCopyAction.MOVE_OR_COPY) {
buttons.push({
label: target ? t('files', 'Copy to {target}', { target }) : t('files', 'Copy'),
type: 'primary',
icon: CopyIcon,
async callback(destination: Node[]) {
try {
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.COPY)
resolve(true)
} catch (error) {
reject(error)
}
},
})
}
if (action === MoveCopyAction.MOVE || action === MoveCopyAction.MOVE_OR_COPY) {
buttons.push({
label: target ? t('files', 'Move to {target}', { target }) : t('files', 'Move'),
type: action === MoveCopyAction.MOVE ? 'primary' : 'secondary',
icon: MoveIcon,
async callback(destination: Node[]) {
try {
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.MOVE)
resolve(true)
} catch (error) {
reject(error)
}
},
})
}
return buttons
})
const picker = filePicker.build()
picker.pick().catch(() => {
reject(new Error(t('files', 'Cancelled move or copy operation')))
})
})
}
export const action = new FileAction({
id: 'move-copy',
displayName(nodes: Node[]) {
switch (getActionForNodes(nodes)) {
case MoveCopyAction.MOVE:
return t('files', 'Move')
case MoveCopyAction.COPY:
return t('files', 'Copy')
case MoveCopyAction.MOVE_OR_COPY:
return t('files', 'Move or copy')
}
},
iconSvgInline: () => FolderMoveSvg,
enabled(nodes: Node[]) {
// We only support moving/copying files within the user folder
if (!nodes.every(node => node.root?.startsWith('/files/'))) {
return false
}
return nodes.length > 0 && (canMove(nodes) || canCopy(nodes))
},
async exec(node: Node, view: View, dir: string) {
const action = getActionForNodes([node])
try {
await openFilePickerForAction(action, dir, node)
return true
} catch (error) {
if (error instanceof Error && !!error.message) {
showError(error.message)
// Silent action as we handle the toast
return null
}
return false
}
},
order: 15,
})

View file

@ -0,0 +1,71 @@
/**
* @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/>.
*
*/
import '@nextcloud/dialogs/style.css'
import type { Node } from '@nextcloud/files'
import { Permission } from '@nextcloud/files'
import PQueue from 'p-queue'
// This is the processing queue. We only want to allow 3 concurrent requests
let queue: PQueue
/**
* Get the processing queue
*/
export const getQueue = () => {
if (!queue) {
queue = new PQueue({ concurrency: 3 })
}
return queue
}
type ShareAttribute = {
enabled: boolean
key: string
scope: string
}
export enum MoveCopyAction {
MOVE = 'Move',
COPY = 'Copy',
MOVE_OR_COPY = 'move-or-copy',
}
export const canMove = (nodes: Node[]) => {
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
return (minPermission & Permission.UPDATE) !== 0
}
export const canDownload = (nodes: Node[]) => {
return nodes.every(node => {
const shareAttributes = JSON.parse(node.attributes?.['share-attributes'] ?? '[]') as Array<ShareAttribute>
return !shareAttributes.some(attribute => attribute.scope === 'permissions' && attribute.enabled === false && attribute.key === 'download')
})
}
export const canCopy = (nodes: Node[]) => {
// For now the only restriction is that a shared file
// cannot be copied if the download is disabled
return canDownload(nodes)
}

View file

@ -0,0 +1,180 @@
<!--
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @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 class="files-list-drag-image">
<span class="files-list-drag-image__icon">
<span ref="previewImg" />
<FolderIcon v-if="isSingleFolder" />
<FileMultipleIcon v-else />
</span>
<span class="files-list-drag-image__name">{{ name }}</span>
</div>
</template>
<script lang="ts">
import { FileType, Node, formatFileSize } from '@nextcloud/files'
import Vue from 'vue'
import FileMultipleIcon from 'vue-material-design-icons/FileMultiple.vue'
import FolderIcon from 'vue-material-design-icons/Folder.vue'
import { getSummaryFor } from '../utils/fileUtils.ts'
export default Vue.extend({
name: 'DragAndDropPreview',
components: {
FileMultipleIcon,
FolderIcon,
},
data() {
return {
nodes: [] as Node[],
}
},
computed: {
isSingleNode() {
return this.nodes.length === 1
},
isSingleFolder() {
return this.isSingleNode
&& this.nodes[0].type === FileType.Folder
},
name() {
if (!this.size) {
return this.summary
}
return `${this.summary} ${this.size}`
},
size() {
const totalSize = this.nodes.reduce((total, node) => total + node.size || 0, 0)
const size = parseInt(totalSize, 10) || 0
if (typeof size !== 'number' || size < 0) {
return null
}
return formatFileSize(size, true)
},
summary(): string {
if (this.isSingleNode) {
const node = this.nodes[0]
return node.attributes?.displayName || node.basename
}
return getSummaryFor(this.nodes)
},
},
methods: {
update(nodes: Node[]) {
this.nodes = nodes
this.$refs.previewImg.replaceChildren()
// Clone icon node from the list
nodes.slice(0, 3).forEach(node => {
const preview = document.querySelector(`[data-cy-files-list-row-fileid="${node.fileid}"] .files-list__row-icon img`)
if (preview) {
const previewElmt = this.$refs.previewImg as HTMLElement
previewElmt.appendChild(preview.parentNode.cloneNode(true))
}
})
this.$nextTick(() => {
this.$emit('loaded', this.$el)
})
},
},
})
</script>
<style lang="scss">
$size: 32px;
$stack-shift: 6px;
.files-list-drag-image {
position: absolute;
top: -9999px;
left: -9999px;
display: flex;
overflow: hidden;
align-items: center;
height: 44px;
padding: 6px 12px;
background: var(--color-main-background);
&__icon,
.files-list__row-icon {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--border-radius);
}
&__icon {
overflow: visible;
margin-right: 12px;
img {
max-width: 100%;
max-height: 100%;
}
.material-design-icon {
color: var(--color-text-maxcontrast);
&.folder-icon {
color: var(--color-primary-element);
}
}
// Previews container
> span {
display: flex;
// Stack effect if more than one element
.files-list__row-icon + .files-list__row-icon {
margin-top: $stack-shift;
margin-left: $stack-shift - $size;
& + .files-list__row-icon {
margin-top: $stack-shift * 2;
}
}
// If we have manually clone the preview,
// let's hide any fallback icons
&:not(:empty) + * {
display: none;
}
}
}
&__name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View file

@ -21,22 +21,27 @@
-->
<template>
<tr :class="{'files-list__row--visible': visible, 'files-list__row--active': isActive}"
<tr :class="{'files-list__row--visible': visible, 'files-list__row--active': isActive, 'files-list__row--dragover': dragover, 'files-list__row--loading': isLoading}"
data-cy-files-list-row
:data-cy-files-list-row-fileid="fileid"
:data-cy-files-list-row-name="source.basename"
:draggable="canDrag"
class="files-list__row"
@contextmenu="onRightClick">
@contextmenu="onRightClick"
@dragover="onDragOver"
@dragleave="onDragLeave"
@dragstart="onDragStart"
@dragend="onDragEnd"
@drop="onDrop">
<!-- Failed indicator -->
<span v-if="source.attributes.failed" class="files-list__row--failed" />
<!-- Checkbox -->
<td class="files-list__row-checkbox">
<NcCheckboxRadioSwitch v-if="visible"
<NcLoadingIcon v-if="isLoading" />
<NcCheckboxRadioSwitch v-else-if="visible"
:aria-label="t('files', 'Select the row for {displayName}', { displayName })"
:checked="selectedFiles"
:value="fileid"
name="selectedFiles"
:checked="isSelected"
@update:checked="onSelectionChange" />
</td>
@ -45,17 +50,24 @@
<!-- Icon or preview -->
<span class="files-list__row-icon" @click="execDefaultAction">
<template v-if="source.type === 'folder'">
<FolderIcon />
<OverlayIcon :is="folderOverlay"
v-if="folderOverlay"
class="files-list__row-icon-overlay" />
<FolderOpenIcon v-if="dragover" />
<template v-else>
<FolderIcon />
<OverlayIcon :is="folderOverlay"
v-if="folderOverlay"
class="files-list__row-icon-overlay" />
</template>
</template>
<!-- Decorative image, should not be aria documented -->
<span v-else-if="previewUrl && !backgroundFailed"
<img v-else-if="previewUrl && backgroundFailed !== true"
ref="previewImg"
alt=""
class="files-list__row-icon-preview"
:style="{ backgroundImage }" />
:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
:src="previewUrl"
@error="backgroundFailed = true"
@load="backgroundFailed = false">
<FileIcon v-else />
@ -68,7 +80,7 @@
</span>
<!-- Rename input -->
<form v-show="isRenaming"
<form v-if="isRenaming"
v-on-click-outside="stopRenaming"
:aria-hidden="!isRenaming"
:aria-label="t('files', 'Rename file')"
@ -85,7 +97,7 @@
@keyup.esc="stopRenaming" />
</form>
<a v-show="!isRenaming"
<a v-else
ref="basename"
:aria-hidden="isRenaming"
class="files-list__row-name-link"
@ -120,7 +132,7 @@
ref="actionsMenu"
:boundaries-element="getBoundariesElement()"
:container="getBoundariesElement()"
:disabled="source._loading"
:disabled="isLoading"
:force-name="true"
:force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */"
:inline="enabledInlineActions.length"
@ -176,15 +188,13 @@
<script lang='ts'>
import type { PropType } from 'vue'
import { CancelablePromise } from 'cancelable-promise'
import { debounce } from 'debounce'
import { emit } from '@nextcloud/event-bus'
import { extname } from 'path'
import { generateUrl } from '@nextcloud/router'
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File, Node, FileAction } from '@nextcloud/files'
import { Type as ShareType } from '@nextcloud/sharing'
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File, FileAction, NodeStatus, Node } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate } from '@nextcloud/l10n'
import { Type as ShareType } from '@nextcloud/sharing'
import { vOnClickOutside } from '@vueuse/components'
import axios from '@nextcloud/axios'
import moment from '@nextcloud/moment'
@ -193,6 +203,7 @@ import Vue from 'vue'
import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
import FileIcon from 'vue-material-design-icons/File.vue'
import FolderIcon from 'vue-material-design-icons/Folder.vue'
import FolderOpenIcon from 'vue-material-design-icons/FolderOpen.vue'
import KeyIcon from 'vue-material-design-icons/Key.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import LinkIcon from 'vue-material-design-icons/Link.vue'
@ -205,9 +216,12 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { getDragAndDropPreview } from '../utils/dragUtils.ts'
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts'
import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts'
import { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { useKeyboardStore } from '../store/keyboard.ts'
import { useRenamingStore } from '../store/renaming.ts'
@ -234,6 +248,7 @@ export default Vue.extend({
FavoriteIcon,
FileIcon,
FolderIcon,
FolderOpenIcon,
KeyIcon,
LinkIcon,
NcActionButton,
@ -278,6 +293,7 @@ export default Vue.extend({
setup() {
const actionsMenuStore = useActionsMenuStore()
const draggingStore = useDragAndDropStore()
const filesStore = useFilesStore()
const keyboardStore = useKeyboardStore()
const renamingStore = useRenamingStore()
@ -285,6 +301,7 @@ export default Vue.extend({
const userConfigStore = useUserConfigStore()
return {
actionsMenuStore,
draggingStore,
filesStore,
keyboardStore,
renamingStore,
@ -295,9 +312,11 @@ export default Vue.extend({
data() {
return {
backgroundFailed: false,
backgroundImage: '',
backgroundFailed: undefined,
loading: '',
dragover: false,
NodeStatus,
}
},
@ -322,7 +341,7 @@ export default Vue.extend({
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
},
currentFileId() {
return this.$route.params.fileid || this.$route.query.fileid || null
return this.$route.params?.fileid || this.$route.query?.fileid || null
},
fileid() {
return this.source?.fileid?.toString?.()
@ -444,6 +463,9 @@ export default Vue.extend({
}
},
draggingFiles() {
return this.draggingStore.dragging
},
selectedFiles() {
return this.selectionStore.selected
},
@ -459,10 +481,14 @@ export default Vue.extend({
return null
}
if (this.backgroundFailed === true) {
return null
}
try {
const previewUrl = this.source.attributes.previewUrl
|| generateUrl('/core/preview?fileId={fileid}', {
fileid: this.source.fileid,
|| generateUrl('/core/preview?fileid={fileid}', {
fileid: this.fileid,
})
const url = new URL(window.location.origin + previewUrl)
@ -539,6 +565,9 @@ export default Vue.extend({
isFavorite() {
return this.source.attributes.favorite === 1
},
isLoading() {
return this.source.status === NodeStatus.LOADING
},
renameLabel() {
const matchLabel: Record<FileType, string> = {
@ -566,6 +595,32 @@ export default Vue.extend({
isActive() {
return this.fileid === this.currentFileId?.toString?.()
},
canDrag() {
const canDrag = (node: Node): boolean => {
return (node.permissions & Permission.UPDATE) !== 0
}
// If we're dragging a selection, we need to check all files
if (this.selectedFiles.length > 0) {
const nodes = this.selectedFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
return nodes.every(canDrag)
}
return canDrag(this.source)
},
canDrop() {
if (this.source.type !== FileType.Folder) {
return false
}
// If the current folder is also being dragged, we can't drop it on itself
if (this.draggingFiles.includes(this.fileid)) {
return false
}
return (this.source.permissions & Permission.CREATE) !== 0
},
},
watch: {
@ -575,7 +630,6 @@ export default Vue.extend({
*/
source() {
this.resetState()
this.debounceIfNotCached()
},
/**
@ -589,106 +643,31 @@ 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 to
// avoid sharing between recycled components
this.debounceGetPreview = debounce(function() {
this.fetchAndApplyPreview()
}, 150, false)
// Fetch the preview on init
this.debounceIfNotCached()
},
beforeDestroy() {
this.resetState()
},
methods: {
async debounceIfNotCached() {
if (!this.previewUrl) {
return
}
// Check if we already have this preview cached
const isCached = await isCachedPreview(this.previewUrl)
if (isCached) {
this.backgroundImage = `url(${this.previewUrl})`
this.backgroundFailed = false
return
}
// We don't have this preview cached or it expired, requesting it
this.debounceGetPreview()
},
fetchAndApplyPreview() {
// Ignore if no preview
if (!this.previewUrl) {
return
}
// If any image is being processed, reset it
if (this.previewPromise) {
this.clearImg()
}
// Store the promise to be able to cancel it
this.previewPromise = new CancelablePromise((resolve, reject, onCancel) => {
const img = new Image()
// If visible, load the preview with higher priority
img.fetchpriority = this.visible ? 'high' : 'auto'
img.onload = () => {
this.backgroundImage = `url(${this.previewUrl})`
this.backgroundFailed = false
resolve(img)
}
img.onerror = () => {
this.backgroundFailed = true
reject(img)
}
img.src = this.previewUrl
// Image loading has been canceled
onCancel(() => {
img.onerror = null
img.onload = null
img.src = ''
})
})
},
resetState() {
// Reset loading state
this.loading = ''
// Reset the preview
this.clearImg()
// Reset background state
this.backgroundFailed = undefined
if (this.$refs.previewImg) {
this.$refs.previewImg.src = ''
}
// Close menu
this.openedMenu = false
},
clearImg() {
this.backgroundImage = ''
this.backgroundFailed = false
if (this.previewPromise) {
this.previewPromise.cancel()
this.previewPromise = null
}
},
async onActionClick(action) {
const displayName = action.displayName([this.source], this.currentView)
try {
// Set the loading marker
this.loading = action.id
Vue.set(this.source, '_loading', true)
Vue.set(this.source, 'status', NodeStatus.LOADING)
const success = await action.exec(this.source, this.currentView, this.currentDir)
@ -708,7 +687,7 @@ export default Vue.extend({
} finally {
// Reset the loading marker
this.loading = ''
Vue.set(this.source, '_loading', false)
Vue.set(this.source, 'status', undefined)
}
},
execDefaultAction(event) {
@ -728,7 +707,7 @@ export default Vue.extend({
}
},
onSelectionChange(selection) {
onSelectionChange(selected: boolean) {
const newSelectedIndex = this.index
const lastSelectedIndex = this.selectionStore.lastSelectedIndex
@ -746,7 +725,7 @@ export default Vue.extend({
// If already selected, update the new selection _without_ the current file
const selection = [...lastSelection, ...filesToSelect]
.filter(fileId => !isAlreadySelected || fileId !== this.fileid)
.filter(fileid => !isAlreadySelected || fileid !== this.fileid)
logger.debug('Shift key pressed, selecting all files in between', { start, end, filesToSelect, isAlreadySelected })
// Keep previous lastSelectedIndex to be use for further shift selections
@ -754,6 +733,10 @@ export default Vue.extend({
return
}
const selection = selected
? [...this.selectedFiles, this.fileid]
: this.selectedFiles.filter(fileid => fileid !== this.fileid)
logger.debug('Updating selection', { selection })
this.selectionStore.set(selection)
this.selectionStore.setLastIndex(newSelectedIndex)
@ -864,7 +847,7 @@ export default Vue.extend({
// Set loading state
this.loading = 'renaming'
Vue.set(this.source, '_loading', true)
Vue.set(this.source, 'status', NodeStatus.LOADING)
// Update node
this.source.rename(newName)
@ -906,7 +889,7 @@ export default Vue.extend({
showError(this.t('files', 'Could not rename "{oldName}"', { oldName }))
} finally {
this.loading = false
Vue.set(this.source, '_loading', false)
Vue.set(this.source, 'status', undefined)
}
},
@ -929,6 +912,100 @@ export default Vue.extend({
return action.displayName([this.source], this.currentView)
},
onDragOver(event: DragEvent) {
this.dragover = this.canDrop
if (!this.canDrop) {
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
return
}
// Handle copy/move drag and drop
if (event.ctrlKey) {
event.dataTransfer.dropEffect = 'copy'
} else {
event.dataTransfer.dropEffect = 'move'
}
},
onDragLeave(event: DragEvent) {
if (this.$el.contains(event.target) && event.target !== this.$el) {
return
}
this.dragover = false
},
async onDragStart(event: DragEvent) {
event.stopPropagation()
if (!this.canDrag) {
event.preventDefault()
event.stopPropagation()
return
}
logger.debug('Drag started')
// Reset any renaming
this.renamingStore.$reset()
// Dragging set of files, if we're dragging a file
// that is already selected, we use the entire selection
if (this.selectedFiles.includes(this.fileid)) {
this.draggingStore.set(this.selectedFiles)
} else {
this.draggingStore.set([this.fileid])
}
const nodes = this.draggingStore.dragging
.map(fileid => this.filesStore.getNode(fileid)) as Node[]
const image = await getDragAndDropPreview(nodes)
event.dataTransfer.setDragImage(image, -10, -10)
},
onDragEnd() {
this.draggingStore.reset()
this.dragover = false
logger.debug('Drag ended')
},
async onDrop(event) {
// If another button is pressed, cancel it
// This allows cancelling the drag with the right click
if (!this.canDrop || event.button !== 0) {
return
}
const isCopy = event.ctrlKey
this.dragover = false
logger.debug('Dropped', { event, selection: this.draggingFiles })
const nodes = this.draggingFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
nodes.forEach(async (node: Node) => {
Vue.set(node, 'status', NodeStatus.LOADING)
try {
// TODO: resolve potential conflicts prior and force overwrite
await handleCopyMoveNodeTo(node, this.source, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE)
} catch (error) {
logger.error('Error while moving file', { error })
if (isCopy) {
showError(this.t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
} else {
showError(this.t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
}
} finally {
Vue.set(node, 'status', undefined)
}
})
// Reset selection after we dropped the files
// if the dropped files are within the selection
if (this.draggingFiles.some(fileid => this.selectedFiles.includes(fileid))) {
logger.debug('Dropped selection, resetting select store...')
this.selectionStore.reset()
}
},
t: translate,
formatFileSize,
},
@ -955,7 +1032,7 @@ tr {
}
/* Preview not loaded animation effect */
.files-list__row-icon-preview:not([style*='background']) {
.files-list__row-icon-preview:not(.files-list__row-icon-preview--loaded) {
background: var(--color-loading-dark);
// animation: preview-gradient-fade 1.2s ease-in-out infinite;
}

View file

@ -55,6 +55,7 @@ import { useSelectionStore } from '../store/selection.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import CustomSvgIconRender from './CustomSvgIconRender.vue'
import logger from '../logger.js'
import { NodeStatus } from '@nextcloud/files'
// The registered actions list
const actions = getFileActions()
@ -120,7 +121,7 @@ export default Vue.extend({
},
areSomeNodesLoading() {
return this.nodes.some(node => node._loading)
return this.nodes.some(node => node.status === NodeStatus.LOADING)
},
openedMenu: {
@ -164,7 +165,7 @@ export default Vue.extend({
// Set loading markers
this.loading = action.id
this.nodes.forEach(node => {
Vue.set(node, '_loading', true)
Vue.set(node, 'status', NodeStatus.LOADING)
})
// Dispatch action execution
@ -198,7 +199,7 @@ export default Vue.extend({
// Remove loading markers
this.loading = null
this.nodes.forEach(node => {
Vue.set(node, '_loading', false)
Vue.set(node, 'status', undefined)
})
}
},

View file

@ -42,7 +42,7 @@
</template>
<script lang="ts">
import { getFileActions } from '@nextcloud/files'
import { NodeStatus, getFileActions } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
@ -121,7 +121,7 @@ export default Vue.extend({
},
areSomeNodesLoading() {
return this.nodes.some(node => node._loading)
return this.nodes.some(node => node.status === NodeStatus.LOADING)
},
openedMenu: {
@ -165,7 +165,7 @@ export default Vue.extend({
// Set loading markers
this.loading = action.id
this.nodes.forEach(node => {
Vue.set(node, '_loading', true)
Vue.set(node, 'status', NodeStatus.LOADING)
})
// Dispatch action execution
@ -199,7 +199,7 @@ export default Vue.extend({
// Remove loading markers
this.loading = null
this.nodes.forEach(node => {
Vue.set(node, '_loading', false)
Vue.set(node, 'status', undefined)
})
}
},

View file

@ -310,16 +310,22 @@ export default Vue.extend({
}
.files-list__row {
&:hover, &:focus, &:active, &--active {
&:hover, &:focus, &:active, &--active, &--dragover {
background-color: var(--color-background-dark);
> * {
--color-border: var(--color-border-dark);
}
// Hover state of the row should also change the favorite markers background
.favorite-marker-icon svg path {
stroke: var(--color-background-dark);
}
}
&--dragover * {
// Prevent dropping on row children
pointer-events: none;
}
}
// Entry preview or mime icon
@ -351,7 +357,8 @@ export default Vue.extend({
}
// Slightly increase the size of the folder icon
&.folder-icon {
&.folder-icon,
&.folder-open-icon {
margin: -3px;
svg {
width: calc(var(--icon-preview-size) + 6px);

View file

@ -48,7 +48,7 @@
<script>
import { generateUrl } from '@nextcloud/router'
import { encodeFilePath } from '../utils/fileUtils.js'
import { encodeFilePath } from '../utils/fileUtils.ts'
import { getToken, isPublic } from '../utils/davUtils.js'
// preview width generation

View file

@ -23,6 +23,7 @@ import { action as deleteAction } from './actions/deleteAction'
import { action as downloadAction } from './actions/downloadAction'
import { action as editLocallyAction } from './actions/editLocallyAction'
import { action as favoriteAction } from './actions/favoriteAction'
import { action as moveOrCopyAction } from './actions/moveOrCopyAction'
import { action as openFolderAction } from './actions/openFolderAction'
import { action as openInFilesAction } from './actions/openInFilesAction'
import { action as renameAction } from './actions/renameAction'
@ -41,6 +42,7 @@ registerFileAction(deleteAction)
registerFileAction(downloadAction)
registerFileAction(editLocallyAction)
registerFileAction(favoriteAction)
registerFileAction(moveOrCopyAction)
registerFileAction(openFolderAction)
registerFileAction(openInFilesAction)
registerFileAction(renameAction)

View file

@ -0,0 +1,46 @@
/**
* @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/>.
*
*/
import { defineStore } from 'pinia'
import Vue from 'vue'
import type { FileId, DragAndDropStore } from '../types'
export const useDragAndDropStore = defineStore('dragging', {
state: () => ({
dragging: [],
} as DragAndDropStore),
actions: {
/**
* Set the selection of fileIds
*/
set(selection = [] as FileId[]) {
Vue.set(this, 'dragging', selection)
},
/**
* Reset the selection
*/
reset() {
Vue.set(this, 'dragging', [])
},
},
})

View file

@ -87,6 +87,10 @@ export const useFilesStore = function(...args) {
onCreatedNode(node: Node) {
this.updateNodes([node])
},
onUpdatedNode(node: Node) {
this.updateNodes([node])
},
},
})
@ -95,8 +99,7 @@ export const useFilesStore = function(...args) {
if (!fileStore._initialized) {
subscribe('files:node:created', fileStore.onCreatedNode)
subscribe('files:node:deleted', fileStore.onDeletedNode)
// subscribe('files:node:moved', fileStore.onMovedNode)
// subscribe('files:node:updated', fileStore.onUpdatedNode)
subscribe('files:node:updated', fileStore.onUpdatedNode)
fileStore._initialized = true
}

View file

@ -19,12 +19,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { Node, getNavigation } from '@nextcloud/files'
import type { FileId, PathsStore, PathOptions, ServicesState } from '../types'
import { defineStore } from 'pinia'
import { Node, getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import Vue from 'vue'
import logger from '../logger'
import { subscribe } from '@nextcloud/event-bus'
export const usePathsStore = function(...args) {
const store = defineStore('paths', {

View file

@ -35,7 +35,7 @@ export const useSelectionStore = defineStore('selection', {
* Set the selection of fileIds
*/
set(selection = [] as FileId[]) {
Vue.set(this, 'selected', selection)
Vue.set(this, 'selected', [...new Set(selection)])
},
/**

View file

@ -106,3 +106,8 @@ export interface RenamingStore {
export interface UploaderStore {
queue: Upload[]
}
// Drag and drop store
export interface DragAndDropStore {
dragging: FileId[]
}

View file

@ -0,0 +1,42 @@
/**
* @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/>.
*
*/
import type { Node } from '@nextcloud/files'
import DragAndDropPreview from '../components/DragAndDropPreview.vue'
import Vue from 'vue'
const Preview = Vue.extend(DragAndDropPreview)
let preview: Vue
export const getDragAndDropPreview = async (nodes: Node[]): Promise<Element> => {
return new Promise((resolve) => {
if (!preview) {
preview = new Preview().$mount()
document.body.appendChild(preview.$el)
}
preview.update(nodes)
preview.$on('loaded', () => {
resolve(preview.$el)
preview.$off('loaded')
})
})
}

View file

@ -19,8 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { FileType, type Node } from '@nextcloud/files'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
const encodeFilePath = function(path) {
export const encodeFilePath = function(path) {
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
let relativePath = ''
pathSections.forEach((section) => {
@ -37,11 +39,35 @@ const encodeFilePath = function(path) {
* @param {string} path the full path
* @return {string[]} [dirPath, fileName]
*/
const extractFilePaths = function(path) {
export const extractFilePaths = function(path) {
const pathSections = path.split('/')
const fileName = pathSections[pathSections.length - 1]
const dirPath = pathSections.slice(0, pathSections.length - 1).join('/')
return [dirPath, fileName]
}
export { encodeFilePath, extractFilePaths }
/**
* Generate a translated summary of an array of nodes
* @param {Node[]} nodes the nodes to summarize
* @return {string}
*/
export const getSummaryFor = (nodes: Node[]): string => {
const fileCount = nodes.filter(node => node.type === FileType.File).length
const folderCount = nodes.filter(node => node.type === FileType.Folder).length
if (fileCount === 0) {
return n('files', '{folderCount} folder', '{folderCount} folders', folderCount, { folderCount })
} else if (folderCount === 0) {
return n('files', '{fileCount} file', '{fileCount} files', fileCount, { fileCount })
}
if (fileCount === 1) {
return n('files', '1 file and {folderCount} folder', '1 file and {folderCount} folders', folderCount, { folderCount })
}
if (folderCount === 1) {
return n('files', '{fileCount} file and 1 folder', '{fileCount} files and 1 folder', fileCount, { fileCount })
}
return t('files', '{fileCount} files and {folderCount} folders', { fileCount, folderCount })
}

View file

@ -63,7 +63,7 @@
data-cy-files-content-empty>
<template #action>
<NcButton v-if="dir !== '/'"
aria-label="t('files', 'Go to the previous folder')"
:aria-label="t('files', 'Go to the previous folder')"
type="primary"
:to="toPreviousDir">
{{ t('files', 'Go back') }}
@ -93,7 +93,7 @@ import { Folder, Node, Permission } from '@nextcloud/files'
import { getCapabilities } from '@nextcloud/capabilities'
import { join, dirname } from 'path'
import { orderBy } from 'natural-orderby'
import { translate } from '@nextcloud/l10n'
import { translate, translatePlural } from '@nextcloud/l10n'
import { UploadPicker } from '@nextcloud/upload'
import { Type } from '@nextcloud/sharing'
import Vue from 'vue'
@ -425,6 +425,7 @@ export default Vue.extend({
},
t: translate,
n: translatePlural,
},
})
</script>

View file

@ -24,7 +24,7 @@ import { joinPaths } from '@nextcloud/paths'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import moment from '@nextcloud/moment'
import { encodeFilePath } from '../../../files/src/utils/fileUtils.js'
import { encodeFilePath } from '../../../files/src/utils/fileUtils.ts'
import client from '../utils/davClient.js'
import davRequest from '../utils/davRequest.js'

3
dist/1567-1567.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/2719-2719.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/5912-5912.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
dist/5941-5941.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -26,28 +26,6 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @copyright 2022 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @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/>.
*
*/
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*

1
dist/5941-5941.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
!function(){"use strict";var n,e={49e3:function(n,e,t){var o=t(48033),r=t(79753),i=t(25108),u=(0,r.getRootUrl)()+"/status.php";!function n(){i.info("checking the Nextcloud maintenance status"),o.Z.get(u).then((function(n){return n.data})).then((function(e){if(!1===e.maintenance)return i.info("Nextcloud is not in maintenance mode anymore -> reloading"),void window.location.reload();i.info("Nextcloud is still in maintenance mode"),setTimeout(n,2e4)})).catch(i.error.bind(void 0))}()}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var i=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(i.exports,i,i.exports,o),i.loaded=!0,i.exports}o.m=e,n=[],o.O=function(e,t,r,i){if(!t){var u=1/0;for(l=0;l<n.length;l++){t=n[l][0],r=n[l][1],i=n[l][2];for(var c=!0,a=0;a<t.length;a++)(!1&i||u>=i)&&Object.keys(o.O).every((function(n){return o.O[n](t[a])}))?t.splice(a--,1):(c=!1,i<u&&(u=i));if(c){n.splice(l--,1);var f=r();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[t,r,i]},o.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return o.d(e,{a:e}),e},o.d=function(n,e){for(var t in e)o.o(e,t)&&!o.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:e[t]})},o.e=function(){return Promise.resolve()},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),o.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},o.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},o.nmd=function(n){return n.paths=[],n.children||(n.children=[]),n},o.j=1802,function(){o.b=document.baseURI||self.location.href;var n={1802:0};o.O.j=function(e){return 0===n[e]};var e=function(e,t){var r,i,u=t[0],c=t[1],a=t[2],f=0;if(u.some((function(e){return 0!==n[e]}))){for(r in c)o.o(c,r)&&(o.m[r]=c[r]);if(a)var l=a(o)}for(e&&e(t);f<u.length;f++)i=u[f],o.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return o.O(l)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(e.bind(null,0)),t.push=e.bind(null,t.push.bind(t))}(),o.nc=void 0;var r=o.O(void 0,[7874],(function(){return o(49e3)}));r=o.O(r)}();
//# sourceMappingURL=core-maintenance.js.map?v=b86da2eed72a6f76d114
!function(){"use strict";var n,e={49e3:function(n,e,t){var o=t(93664),r=t(79753),i=t(25108),u=(0,r.getRootUrl)()+"/status.php";!function n(){i.info("checking the Nextcloud maintenance status"),o.Z.get(u).then((function(n){return n.data})).then((function(e){if(!1===e.maintenance)return i.info("Nextcloud is not in maintenance mode anymore -> reloading"),void window.location.reload();i.info("Nextcloud is still in maintenance mode"),setTimeout(n,2e4)})).catch(i.error.bind(void 0))}()}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var i=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(i.exports,i,i.exports,o),i.loaded=!0,i.exports}o.m=e,n=[],o.O=function(e,t,r,i){if(!t){var u=1/0;for(l=0;l<n.length;l++){t=n[l][0],r=n[l][1],i=n[l][2];for(var c=!0,a=0;a<t.length;a++)(!1&i||u>=i)&&Object.keys(o.O).every((function(n){return o.O[n](t[a])}))?t.splice(a--,1):(c=!1,i<u&&(u=i));if(c){n.splice(l--,1);var f=r();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[t,r,i]},o.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return o.d(e,{a:e}),e},o.d=function(n,e){for(var t in e)o.o(e,t)&&!o.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:e[t]})},o.e=function(){return Promise.resolve()},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),o.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},o.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},o.nmd=function(n){return n.paths=[],n.children||(n.children=[]),n},o.j=1802,function(){o.b=document.baseURI||self.location.href;var n={1802:0};o.O.j=function(e){return 0===n[e]};var e=function(e,t){var r,i,u=t[0],c=t[1],a=t[2],f=0;if(u.some((function(e){return 0!==n[e]}))){for(r in c)o.o(c,r)&&(o.m[r]=c[r]);if(a)var l=a(o)}for(e&&e(t);f<u.length;f++)i=u[f],o.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return o.O(l)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(e.bind(null,0)),t.push=e.bind(null,t.push.bind(t))}(),o.nc=void 0;var r=o.O(void 0,[7874],(function(){return o(49e3)}));r=o.O(r)}();
//# sourceMappingURL=core-maintenance.js.map?v=8a93fc0e3bf1faadb384

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
!function(){"use strict";var n,e={49983:function(n,e,t){var r=t(48033),o=t(79753),i=t(69183);window.OC.Settings=window.OC.Settings||{},window.OC.Settings.Apps=window.OC.Settings.Apps||{rebuildNavigation:function(){return r.Z.get((0,o.generateOcsUrl)("core/navigation",2)+"/apps?format=json").then((function(n){var e=n.data;200===e.ocs.meta.statuscode&&((0,i.j8)("nextcloud:app-menu.refresh",{apps:e.ocs.data}),window.dispatchEvent(new Event("resize")))}))}}}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.loaded=!0,i.exports}r.m=e,n=[],r.O=function(e,t,o,i){if(!t){var u=1/0;for(s=0;s<n.length;s++){t=n[s][0],o=n[s][1],i=n[s][2];for(var a=!0,c=0;c<t.length;c++)(!1&i||u>=i)&&Object.keys(r.O).every((function(n){return r.O[n](t[c])}))?t.splice(c--,1):(a=!1,i<u&&(u=i));if(a){n.splice(s--,1);var f=o();void 0!==f&&(e=f)}}return e}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[t,o,i]},r.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(e,{a:e}),e},r.d=function(n,e){for(var t in e)r.o(e,t)&&!r.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:e[t]})},r.e=function(){return Promise.resolve()},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),r.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},r.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},r.nmd=function(n){return n.paths=[],n.children||(n.children=[]),n},r.j=1647,function(){r.b=document.baseURI||self.location.href;var n={1647:0};r.O.j=function(e){return 0===n[e]};var e=function(e,t){var o,i,u=t[0],a=t[1],c=t[2],f=0;if(u.some((function(e){return 0!==n[e]}))){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(c)var s=c(r)}for(e&&e(t);f<u.length;f++)i=u[f],r.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return r.O(s)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(e.bind(null,0)),t.push=e.bind(null,t.push.bind(t))}(),r.nc=void 0;var o=r.O(void 0,[7874],(function(){return r(49983)}));o=r.O(o)}();
//# sourceMappingURL=settings-apps.js.map?v=2aa3a3d9e4f3e5c8cb84
!function(){"use strict";var n,e={49983:function(n,e,t){var r=t(93664),o=t(79753),i=t(69183);window.OC.Settings=window.OC.Settings||{},window.OC.Settings.Apps=window.OC.Settings.Apps||{rebuildNavigation:function(){return r.Z.get((0,o.generateOcsUrl)("core/navigation",2)+"/apps?format=json").then((function(n){var e=n.data;200===e.ocs.meta.statuscode&&((0,i.j8)("nextcloud:app-menu.refresh",{apps:e.ocs.data}),window.dispatchEvent(new Event("resize")))}))}}}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.loaded=!0,i.exports}r.m=e,n=[],r.O=function(e,t,o,i){if(!t){var u=1/0;for(s=0;s<n.length;s++){t=n[s][0],o=n[s][1],i=n[s][2];for(var a=!0,c=0;c<t.length;c++)(!1&i||u>=i)&&Object.keys(r.O).every((function(n){return r.O[n](t[c])}))?t.splice(c--,1):(a=!1,i<u&&(u=i));if(a){n.splice(s--,1);var f=o();void 0!==f&&(e=f)}}return e}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[t,o,i]},r.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(e,{a:e}),e},r.d=function(n,e){for(var t in e)r.o(e,t)&&!r.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:e[t]})},r.e=function(){return Promise.resolve()},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),r.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},r.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},r.nmd=function(n){return n.paths=[],n.children||(n.children=[]),n},r.j=1647,function(){r.b=document.baseURI||self.location.href;var n={1647:0};r.O.j=function(e){return 0===n[e]};var e=function(e,t){var o,i,u=t[0],a=t[1],c=t[2],f=0;if(u.some((function(e){return 0!==n[e]}))){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(c)var s=c(r)}for(e&&e(t);f<u.length;f++)i=u[f],r.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return r.O(s)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(e.bind(null,0)),t.push=e.bind(null,t.push.bind(t))}(),r.nc=void 0;var o=r.O(void 0,[7874],(function(){return r(49983)}));o=r.O(o)}();
//# sourceMappingURL=settings-apps.js.map?v=19f79cf48705090f8898

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more