mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 08:44:07 -04:00
Merge pull request #39998 from nextcloud/feat/f2v/dnd
This commit is contained in:
commit
d395fa862b
122 changed files with 996 additions and 308 deletions
|
|
@ -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>
|
||||
|
|
|
|||
248
apps/files/src/actions/moveOrCopyAction.ts
Normal file
248
apps/files/src/actions/moveOrCopyAction.ts
Normal 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,
|
||||
})
|
||||
71
apps/files/src/actions/moveOrCopyActionUtils.ts
Normal file
71
apps/files/src/actions/moveOrCopyActionUtils.ts
Normal 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)
|
||||
}
|
||||
180
apps/files/src/components/DragAndDropPreview.vue
Normal file
180
apps/files/src/components/DragAndDropPreview.vue
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
46
apps/files/src/store/dragging.ts
Normal file
46
apps/files/src/store/dragging.ts
Normal 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', [])
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -106,3 +106,8 @@ export interface RenamingStore {
|
|||
export interface UploaderStore {
|
||||
queue: Upload[]
|
||||
}
|
||||
|
||||
// Drag and drop store
|
||||
export interface DragAndDropStore {
|
||||
dragging: FileId[]
|
||||
}
|
||||
|
|
|
|||
42
apps/files/src/utils/dragUtils.ts
Normal file
42
apps/files/src/utils/dragUtils.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
3
dist/1567-1567.js
vendored
File diff suppressed because one or more lines are too long
1
dist/1567-1567.js.map
vendored
1
dist/1567-1567.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/2719-2719.js
vendored
4
dist/2719-2719.js
vendored
File diff suppressed because one or more lines are too long
2
dist/2719-2719.js.map
vendored
2
dist/2719-2719.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/5912-5912.js
vendored
4
dist/5912-5912.js
vendored
File diff suppressed because one or more lines are too long
2
dist/5912-5912.js.map
vendored
2
dist/5912-5912.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/5941-5941.js
vendored
Normal file
3
dist/5941-5941.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
1
dist/5941-5941.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-maintenance.js
vendored
4
dist/core-maintenance.js
vendored
|
|
@ -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
|
||||
2
dist/core-maintenance.js.map
vendored
2
dist/core-maintenance.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-profile.js
vendored
4
dist/core-profile.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-profile.js.map
vendored
2
dist/core-profile.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-recommendedapps.js
vendored
4
dist/core-recommendedapps.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-recommendedapps.js.map
vendored
2
dist/core-recommendedapps.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unified-search.js
vendored
4
dist/core-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unified-search.js.map
vendored
2
dist/core-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dashboard-main.js
vendored
4
dist/dashboard-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/dashboard-main.js.map
vendored
2
dist/dashboard-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dav-settings-admin-caldav.js
vendored
4
dist/dav-settings-admin-caldav.js
vendored
File diff suppressed because one or more lines are too long
2
dist/dav-settings-admin-caldav.js.map
vendored
2
dist/dav-settings-admin-caldav.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dav-settings-personal-availability.js
vendored
4
dist/dav-settings-personal-availability.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
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-personal-settings.js
vendored
4
dist/files-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-personal-settings.js.map
vendored
2
dist/files-personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-reference-files.js
vendored
4
dist/files-reference-files.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-reference-files.js.map
vendored
2
dist/files-reference-files.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-sidebar.js
vendored
4
dist/files-sidebar.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-sidebar.js.map
vendored
2
dist/files-sidebar.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_external-init.js
vendored
4
dist/files_external-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_external-init.js.map
vendored
2
dist/files_external-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_reminders-main.js
vendored
4
dist/files_reminders-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_reminders-main.js.map
vendored
2
dist/files_reminders-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-files_sharing_tab.js
vendored
4
dist/files_sharing-files_sharing_tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-files_sharing_tab.js.map
vendored
2
dist/files_sharing-files_sharing_tab.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-init.js
vendored
4
dist/files_sharing-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-init.js.map
vendored
2
dist/files_sharing-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-personal-settings.js
vendored
4
dist/files_sharing-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-personal-settings.js.map
vendored
2
dist/files_sharing-personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_trashbin-main.js
vendored
4
dist/files_trashbin-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_trashbin-main.js.map
vendored
2
dist/files_trashbin-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_versions-files_versions.js
vendored
4
dist/files_versions-files_versions.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_versions-files_versions.js.map
vendored
2
dist/files_versions-files_versions.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/oauth2-oauth2.js
vendored
4
dist/oauth2-oauth2.js
vendored
File diff suppressed because one or more lines are too long
2
dist/oauth2-oauth2.js.map
vendored
2
dist/oauth2-oauth2.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-7418.js
vendored
4
dist/settings-apps-view-7418.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-7418.js.map
vendored
2
dist/settings-apps-view-7418.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps.js
vendored
4
dist/settings-apps.js
vendored
|
|
@ -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
|
||||
2
dist/settings-apps.js.map
vendored
2
dist/settings-apps.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-users-8351.js
vendored
4
dist/settings-users-8351.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-users-8351.js.map
vendored
2
dist/settings-users-8351.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-ai.js
vendored
4
dist/settings-vue-settings-admin-ai.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-vue-settings-admin-ai.js.map
vendored
2
dist/settings-vue-settings-admin-ai.js.map
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
4
dist/settings-vue-settings-admin-security.js
vendored
4
dist/settings-vue-settings-admin-security.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
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.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
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue