Merge branch 'drive-bulk' into upload-dialog
This commit is contained in:
commit
f7b9f6d3a5
|
|
@ -8,7 +8,7 @@ import * as fs from 'node:fs';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
|
||||
|
|
@ -720,6 +720,21 @@ export class DriveService {
|
|||
return fileObj;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) {
|
||||
const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({
|
||||
id: folderId,
|
||||
userId: userId,
|
||||
}) : null;
|
||||
|
||||
await this.driveFilesRepository.update({
|
||||
id: In(fileIds),
|
||||
userId: userId,
|
||||
}, {
|
||||
folderId: folder ? folder.id : null,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||
if (file.storedInternal) {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js';
|
|||
export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
|
||||
export * as 'drive/files/show' from './endpoints/drive/files/show.js';
|
||||
export * as 'drive/files/update' from './endpoints/drive/files/update.js';
|
||||
export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js';
|
||||
export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
|
||||
export * as 'drive/folders' from './endpoints/drive/folders.js';
|
||||
export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:drive',
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: ['fileIds'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private driveService: DriveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import { $i } from '@/i.js';
|
|||
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { setDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'chosen', r: Misskey.entities.DriveFile): void;
|
||||
(ev: 'dragstart'): void;
|
||||
(ev: 'dragstart', dragEvent: DragEvent): void;
|
||||
(ev: 'dragend'): void;
|
||||
}>();
|
||||
|
||||
|
|
@ -90,11 +91,11 @@ function onContextmenu(ev: MouseEvent) {
|
|||
function onDragstart(ev: DragEvent) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
|
||||
setDragData(ev, 'driveFiles', [props.file]);
|
||||
}
|
||||
isDragging.value = true;
|
||||
|
||||
emit('dragstart');
|
||||
emit('dragstart', ev);
|
||||
}
|
||||
|
||||
function onDragend() {
|
||||
|
|
@ -114,7 +115,7 @@ function onDragend() {
|
|||
&:hover {
|
||||
background: rgba(#000, 0.05);
|
||||
|
||||
> .label {
|
||||
.label {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #0b65a5;
|
||||
|
|
@ -132,7 +133,7 @@ function onDragend() {
|
|||
&:active {
|
||||
background: rgba(#000, 0.1);
|
||||
|
||||
> .label {
|
||||
.label {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #0b588c;
|
||||
|
|
@ -158,19 +159,19 @@ function onDragend() {
|
|||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
}
|
||||
|
||||
> .label {
|
||||
.label {
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .name {
|
||||
color: #fff;
|
||||
.name {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
color: #fff;
|
||||
.thumbnail {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,8 +241,8 @@ function onDragend() {
|
|||
|
||||
.name {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.8em;
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 82%;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
color: var(--MI_THEME-fg);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@mouseover="onMouseover"
|
||||
@mouseout="onMouseout"
|
||||
|
|
@ -43,6 +42,8 @@ import { i18n } from '@/i18n.js';
|
|||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
|
|
@ -56,10 +57,7 @@ const props = withDefaults(defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'unchose', v: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'move', v: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
|
||||
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
||||
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
||||
(ev: 'dragstart'): void;
|
||||
(ev: 'dragend'): void;
|
||||
}>();
|
||||
|
|
@ -78,10 +76,6 @@ function checkboxClicked() {
|
|||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
emit('move', props.folder);
|
||||
}
|
||||
|
||||
function onMouseover() {
|
||||
hover.value = true;
|
||||
}
|
||||
|
|
@ -101,10 +95,7 @@ function onDragover(ev: DragEvent) {
|
|||
}
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) {
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
|
|
@ -148,48 +139,51 @@ function onDrop(ev: DragEvent) {
|
|||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
emit('removeFile', file.id);
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder.id,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
misskeyApi('drive/files/move-bulk', {
|
||||
fileIds: droppedData.map(f => f.id),
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
globalEvents.emit('driveFilesMoved', droppedData, props.folder);
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder !== '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFolders');
|
||||
if (droppedData != null) {
|
||||
const droppedFolder = droppedData[0];
|
||||
|
||||
// 移動先が自分自身ならreject
|
||||
if (folder.id === props.folder.id) return;
|
||||
// 移動先が自分自身ならreject
|
||||
if (droppedFolder.id === props.folder.id) return;
|
||||
|
||||
emit('removeFolder', folder.id);
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: props.folder.id,
|
||||
}).then(() => {
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: droppedFolder.id,
|
||||
parentId: props.folder.id,
|
||||
}).then(() => {
|
||||
// noop
|
||||
}).catch(err => {
|
||||
switch (err.code) {
|
||||
case 'RECURSIVE_NESTING':
|
||||
claimAchievement('driveFolderCircularReference');
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToProcess,
|
||||
text: i18n.ts.circularReferenceFolder,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
switch (err.code) {
|
||||
case 'RECURSIVE_NESTING':
|
||||
claimAchievement('driveFolderCircularReference');
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToProcess,
|
||||
text: i18n.ts.circularReferenceFolder,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
@ -198,7 +192,7 @@ function onDragstart(ev: DragEvent) {
|
|||
if (!ev.dataTransfer) return;
|
||||
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
|
||||
setDragData(ev, 'driveFolders', [props.folder]);
|
||||
isDragging.value = true;
|
||||
|
||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
||||
|
|
@ -211,10 +205,6 @@ function onDragend() {
|
|||
emit('dragend');
|
||||
}
|
||||
|
||||
function go() {
|
||||
emit('move', props.folder);
|
||||
}
|
||||
|
||||
function rename() {
|
||||
os.inputText({
|
||||
title: i18n.ts.renameFolder,
|
||||
|
|
@ -230,7 +220,7 @@ function rename() {
|
|||
}
|
||||
|
||||
function move() {
|
||||
os.selectDriveFolder(false).then(folder => {
|
||||
os.selectDriveFolder().then(folder => {
|
||||
if (folder[0] && folder[0].id === props.folder.id) return;
|
||||
|
||||
misskeyApi('drive/folders/update', {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div
|
||||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
@click="onClick"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
|
|
@ -22,6 +21,8 @@ import { ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const props = defineProps<{
|
||||
folder?: Misskey.entities.DriveFolder;
|
||||
|
|
@ -29,27 +30,11 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'move', v?: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
|
||||
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
||||
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
||||
}>();
|
||||
|
||||
const hover = ref(false);
|
||||
const draghover = ref(false);
|
||||
|
||||
function onClick() {
|
||||
emit('move', props.folder);
|
||||
}
|
||||
|
||||
function onMouseover() {
|
||||
hover.value = true;
|
||||
}
|
||||
|
||||
function onMouseout() {
|
||||
hover.value = false;
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
|
|
@ -59,10 +44,7 @@ function onDragover(ev: DragEvent) {
|
|||
}
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) {
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
|
|
@ -108,28 +90,31 @@ function onDrop(ev: DragEvent) {
|
|||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
emit('removeFile', file.id);
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
misskeyApi('drive/files/move-bulk', {
|
||||
fileIds: droppedData.map(f => f.id),
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
}).then(() => {
|
||||
globalEvents.emit('driveFilesMoved', droppedData, props.folder ?? null);
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder !== '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
// 移動先が自分自身ならreject
|
||||
if (props.folder && folder.id === props.folder.id) return;
|
||||
emit('removeFolder', folder.id);
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFolders');
|
||||
if (droppedData != null) {
|
||||
const droppedFolder = droppedData[0];
|
||||
// 移動先が自分自身ならreject
|
||||
if (props.folder && droppedFolder.id === props.folder.id) return;
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: droppedFolder.id,
|
||||
parentId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XNavFolder
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@click="cd(null)"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
|
|
@ -22,10 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:folder="f"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@click="cd(f)"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
|
|
@ -35,71 +31,110 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</nav>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="main"
|
||||
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents">
|
||||
<div>
|
||||
<div v-if="select === 'folder'">
|
||||
<template v-if="folder == null">
|
||||
<MkButton v-if="!isRootSelected" @click="isRootSelected = true">
|
||||
<i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
|
||||
</MkButton>
|
||||
<MkButton v-else @click="isRootSelected = false">
|
||||
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
|
||||
</MkButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MkButton v-if="!selectedFolders.some(f => f.id === folder!.id)" @click="selectedFolders.push(folder)">
|
||||
<i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
|
||||
</MkButton>
|
||||
<MkButton v-else @click="selectedFolders = selectedFolders.filter(f => f.id !== folder!.id)">
|
||||
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
|
||||
</MkButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="main"
|
||||
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<MkInfo v-if="!store.r.readDriveTip.value" closable @close="closeTip()"><div v-html="i18n.ts.driveAboutTip"></div></MkInfo>
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders">
|
||||
<XFolder
|
||||
v-for="(f, i) in folders"
|
||||
:key="f.id"
|
||||
v-anim="i"
|
||||
:class="$style.folder"
|
||||
:folder="f"
|
||||
:selectMode="select === 'folder'"
|
||||
:isSelected="selectedFolders.some(x => x.id === f.id)"
|
||||
@chosen="chooseFolder"
|
||||
@unchose="unchoseFolder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
|
||||
<MkButton v-if="moreFolders" ref="moreFolders" @click="fetchMoreFolders">{{ i18n.ts.loadMore }}</MkButton>
|
||||
<div v-show="foldersPaginator.items.value.length > 0">
|
||||
<div :class="$style.folders">
|
||||
<XFolder
|
||||
v-for="(f, i) in foldersPaginator.items.value"
|
||||
:key="f.id"
|
||||
v-anim="i"
|
||||
:class="$style.folder"
|
||||
:folder="f"
|
||||
:selectMode="select === 'folder'"
|
||||
:isSelected="selectedFolders.some(x => x.id === f.id)"
|
||||
@chosen="chooseFolder"
|
||||
@unchose="unchoseFolder"
|
||||
@click="cd(f)"
|
||||
@upload="upload"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
</div>
|
||||
<MkButton v-if="foldersPaginator.canFetchOlder.value" primary rounded @click="foldersPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-show="files.length > 0" ref="filesContainer" :class="$style.files">
|
||||
<XFile
|
||||
v-for="(file, i) in files"
|
||||
:key="file.id"
|
||||
v-anim="i"
|
||||
:class="$style.file"
|
||||
:file="file"
|
||||
:folder="folder"
|
||||
:selectMode="select === 'file'"
|
||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||
@chosen="chooseFile"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
|
||||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
|
||||
|
||||
<div v-show="filesPaginator.items.value.length > 0">
|
||||
<MkStickyContainer v-for="(item, i) in filesTimeline" :key="`${item.date.getFullYear()}/${item.date.getMonth() + 1}`">
|
||||
<template #header>
|
||||
<div :class="$style.date">
|
||||
<span><i class="ti ti-chevron-down"></i> {{ item.date.getFullYear() }}/{{ item.date.getMonth() + 1 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_files_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_files_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_files_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_files_leaveTo : ''"
|
||||
:moveClass="prefer.s.animation ? $style.transition_files_move : ''"
|
||||
:class="$style.files"
|
||||
>
|
||||
<XFile
|
||||
v-for="file in item.items" :key="file.id"
|
||||
:class="$style.file"
|
||||
:file="file"
|
||||
:folder="folder"
|
||||
:selectMode="select === 'file' || isEditMode"
|
||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||
@chosen="onChooseFile"
|
||||
@dragstart="onFileDragstart(file, $event)"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</MkStickyContainer>
|
||||
<MkButton v-show="filesPaginator.canFetchOlder.value" primary rounded @click="filesPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
|
||||
|
||||
<div v-if="filesPaginator.items.value.length == 0 && foldersPaginator.items.value.length == 0 && !fetching" :class="$style.empty">
|
||||
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
|
||||
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
</div>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="isEditMode" :class="$style.footer">
|
||||
<MkButton primary rounded @click="moveFilesBulk()"><i class="ti ti-folder-symlink"></i> {{ i18n.ts.move }}...</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
|
|
@ -115,9 +150,13 @@ import { claimAchievement } from '@/utility/achievements.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||
import { store } from '@/store.js';
|
||||
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
|
||||
import { usePagination } from '@/composables/use-pagination.js';
|
||||
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
select?: 'file' | 'folder' | null;
|
||||
|
|
@ -127,25 +166,16 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
||||
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'changeSelectedFiles', v: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'changeSelectedFolders', v: (Misskey.entities.DriveFolder | null)[]): void;
|
||||
(ev: 'move-root'): void;
|
||||
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
||||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||
}>();
|
||||
|
||||
const loadMoreFiles = useTemplateRef('loadMoreFiles');
|
||||
|
||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const folders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const moreFiles = ref(false);
|
||||
const moreFolders = ref(false);
|
||||
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = uploa______ds;
|
||||
const connection = useStream().useChannel('drive');
|
||||
const uploadings = uploads;
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
|
@ -154,51 +184,87 @@ const draghover = ref(false);
|
|||
// (自分自身の階層にドロップできないようにするためのフラグ)
|
||||
const isDragSource = ref(false);
|
||||
|
||||
const fetching = ref(true);
|
||||
const isEditMode = ref(false);
|
||||
|
||||
const ilFilesObserver = new IntersectionObserver(
|
||||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
||||
);
|
||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const isRootSelected = ref(false);
|
||||
|
||||
watch(selectedFiles, () => {
|
||||
emit('changeSelectedFiles', selectedFiles.value);
|
||||
});
|
||||
|
||||
watch([selectedFolders, isRootSelected], () => {
|
||||
emit('changeSelectedFolders', isRootSelected.value ? [null, ...selectedFolders.value] : selectedFolders.value);
|
||||
});
|
||||
|
||||
const fetching = ref(true);
|
||||
|
||||
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
|
||||
|
||||
watch(folder, () => emit('cd', folder.value));
|
||||
watch(sortModeSelect, () => {
|
||||
fetch();
|
||||
const filesPaginator = usePagination({
|
||||
ctx: {
|
||||
endpoint: 'drive/files',
|
||||
limit: 30,
|
||||
canFetchDetection: 'limit',
|
||||
params: computed(() => ({
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
type: props.type,
|
||||
sort: sortModeSelect.value,
|
||||
})),
|
||||
},
|
||||
autoInit: false,
|
||||
autoReInit: false,
|
||||
});
|
||||
|
||||
const foldersPaginator = usePagination({
|
||||
ctx: {
|
||||
endpoint: 'drive/folders',
|
||||
limit: 30,
|
||||
canFetchDetection: 'limit',
|
||||
params: computed(() => ({
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
})),
|
||||
},
|
||||
autoInit: false,
|
||||
autoReInit: false,
|
||||
});
|
||||
|
||||
const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
|
||||
|
||||
watch(folder, () => emit('cd', folder.value));
|
||||
watch(sortModeSelect, () => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
async function initialize() {
|
||||
fetching.value = true;
|
||||
await Promise.all([
|
||||
foldersPaginator.init(),
|
||||
filesPaginator.init(),
|
||||
]);
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
|
||||
addFile(file, true);
|
||||
}
|
||||
|
||||
function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
|
||||
const current = folder.value ? folder.value.id : null;
|
||||
if (current !== file.folderId) {
|
||||
removeFile(file);
|
||||
} else {
|
||||
addFile(file, true);
|
||||
if (file.folderId === (folder.value?.id ?? null)) {
|
||||
filesPaginator.prepend(file);
|
||||
}
|
||||
}
|
||||
|
||||
function onStreamDriveFileDeleted(fileId: string) {
|
||||
removeFile(fileId);
|
||||
}
|
||||
function onFileDragstart(file: Misskey.entities.DriveFile, ev: DragEvent) {
|
||||
if (isEditMode.value) {
|
||||
if (!selectedFiles.value.some(f => f.id === file.id)) {
|
||||
selectedFiles.value.push(file);
|
||||
}
|
||||
|
||||
function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
|
||||
addFolder(createdFolder, true);
|
||||
}
|
||||
|
||||
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
|
||||
const current = folder.value ? folder.value.id : null;
|
||||
if (current !== updatedFolder.parentId) {
|
||||
removeFolder(updatedFolder);
|
||||
} else {
|
||||
addFolder(updatedFolder, true);
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
setDragData(ev, 'driveFiles', selectedFiles.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onStreamDriveFolderDeleted(folderId: string) {
|
||||
removeFolder(folderId);
|
||||
isDragSource.value = true;
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
|
|
@ -212,9 +278,7 @@ function onDragover(ev: DragEvent) {
|
|||
}
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) {
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
|
|
@ -260,102 +324,103 @@ function onDrop(ev: DragEvent) {
|
|||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
if (files.value.some(f => f.id === file.id)) return;
|
||||
removeFile(file.id);
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
misskeyApi('drive/files/move-bulk', {
|
||||
fileIds: droppedData.map(f => f.id),
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
}).then(() => {
|
||||
globalEvents.emit('driveFilesMoved', droppedData, folder.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder !== '') {
|
||||
const droppedFolder = JSON.parse(driveFolder);
|
||||
|
||||
// 移動先が自分自身ならreject
|
||||
if (folder.value && droppedFolder.id === folder.value.id) return false;
|
||||
if (folders.value.some(f => f.id === droppedFolder.id)) return false;
|
||||
removeFolder(droppedFolder.id);
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: droppedFolder.id,
|
||||
parentId: folder.value ? folder.value.id : null,
|
||||
}).then(() => {
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFolders');
|
||||
if (droppedData != null) {
|
||||
const droppedFolder = droppedData[0];
|
||||
// 移動先が自分自身ならreject
|
||||
if (folder.value && droppedFolder.id === folder.value.id) return false;
|
||||
if (foldersPaginator.items.value.some(f => f.id === droppedFolder.id)) return false;
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: droppedFolder.id,
|
||||
parentId: folder.value ? folder.value.id : null,
|
||||
}).then(() => {
|
||||
// noop
|
||||
}).catch(err => {
|
||||
switch (err.code) {
|
||||
case 'RECURSIVE_NESTING':
|
||||
claimAchievement('driveFolderCircularReference');
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToProcess,
|
||||
text: i18n.ts.circularReferenceFolder,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
switch (err.code) {
|
||||
case 'RECURSIVE_NESTING':
|
||||
claimAchievement('driveFolderCircularReference');
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.unableToProcess,
|
||||
text: i18n.ts.circularReferenceFolder,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
function urlUpload() {
|
||||
os.inputText({
|
||||
async function urlUpload() {
|
||||
const { canceled, result: url } = await os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
type: 'url',
|
||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled || !url) return;
|
||||
misskeyApi('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: folder.value ? folder.value.id : undefined,
|
||||
});
|
||||
});
|
||||
if (canceled || !url) return;
|
||||
|
||||
os.alert({
|
||||
title: i18n.ts.uploadFromUrlRequested,
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||
});
|
||||
await os.apiWithDialog('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: folder.value ? folder.value.id : undefined,
|
||||
});
|
||||
|
||||
os.alert({
|
||||
title: i18n.ts.uploadFromUrlRequested,
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||
});
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
os.inputText({
|
||||
async function createFolder() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts.createFolder,
|
||||
placeholder: i18n.ts.folderName,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled || name == null) return;
|
||||
misskeyApi('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: folder.value ? folder.value.id : undefined,
|
||||
}).then(createdFolder => {
|
||||
addFolder(createdFolder, true);
|
||||
});
|
||||
});
|
||||
if (canceled || name == null) return;
|
||||
|
||||
const createdFolder = await os.apiWithDialog('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: folder.value ? folder.value.id : undefined,
|
||||
});
|
||||
|
||||
foldersPaginator.prepend(createdFolder);
|
||||
}
|
||||
|
||||
function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||
os.inputText({
|
||||
async function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts.renameFolder,
|
||||
placeholder: i18n.ts.inputNewFolderName,
|
||||
default: folderToRename.name,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: folderToRename.id,
|
||||
name: name,
|
||||
}).then(updatedFolder => {
|
||||
// FIXME: 画面を更新するために自分自身に移動
|
||||
move(updatedFolder);
|
||||
});
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const updatedFolder = await os.apiWithDialog('drive/folders/update', {
|
||||
folderId: folderToRename.id,
|
||||
name: name,
|
||||
});
|
||||
|
||||
// FIXME: 画面を更新するために自分自身に移動
|
||||
cd(updatedFolder);
|
||||
}
|
||||
|
||||
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
||||
|
|
@ -363,7 +428,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
|||
folderId: folderToDelete.id,
|
||||
}).then(() => {
|
||||
// 削除時に親フォルダに移動
|
||||
move(folderToDelete.parentId);
|
||||
cd(folderToDelete.parentId);
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
|
|
@ -384,25 +449,35 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
|||
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => {
|
||||
addFile(res, true);
|
||||
if (res.folderId === (folder.value?.id ?? null)) {
|
||||
filesPaginator.prepend(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function chooseFile(file: Misskey.entities.DriveFile) {
|
||||
function onChooseFile(file: Misskey.entities.DriveFile) {
|
||||
const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
|
||||
|
||||
if (isEditMode.value) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
|
||||
} else {
|
||||
selectedFiles.value.push(file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
|
||||
} else {
|
||||
selectedFiles.value.push(file);
|
||||
}
|
||||
emit('change-selection', selectedFiles.value);
|
||||
} else {
|
||||
if (isAlreadySelected) {
|
||||
emit('selected', file);
|
||||
//emit('selected', file);
|
||||
} else {
|
||||
selectedFiles.value = [file];
|
||||
emit('change-selection', [file]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -415,23 +490,20 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
|||
} else {
|
||||
selectedFolders.value.push(folderToChoose);
|
||||
}
|
||||
emit('change-selection', selectedFolders.value);
|
||||
} else {
|
||||
if (isAlreadySelected) {
|
||||
emit('selected', folderToChoose);
|
||||
//emit('selected', folderToChoose);
|
||||
} else {
|
||||
selectedFolders.value = [folderToChoose];
|
||||
emit('change-selection', [folderToChoose]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) {
|
||||
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id);
|
||||
emit('change-selection', selectedFolders.value);
|
||||
}
|
||||
|
||||
function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
|
||||
function cd(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
|
||||
if (!target) {
|
||||
goRoot();
|
||||
return;
|
||||
|
|
@ -455,71 +527,23 @@ function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFold
|
|||
if (folderToMove.parent) dive(folderToMove.parent);
|
||||
|
||||
emit('open-folder', folderToMove);
|
||||
fetch();
|
||||
initialize();
|
||||
});
|
||||
}
|
||||
|
||||
function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
|
||||
const current = folder.value ? folder.value.id : null;
|
||||
if (current !== folderToAdd.parentId) return;
|
||||
async function moveFilesBulk() {
|
||||
if (selectedFiles.value.length === 0) return;
|
||||
|
||||
if (folders.value.some(f => f.id === folderToAdd.id)) {
|
||||
const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
|
||||
folders.value[exist] = folderToAdd;
|
||||
return;
|
||||
}
|
||||
const toFolder = await os.selectDriveFolder(folder.value ? folder.value.id : null);
|
||||
|
||||
if (unshift) {
|
||||
folders.value.unshift(folderToAdd);
|
||||
} else {
|
||||
folders.value.push(folderToAdd);
|
||||
}
|
||||
await os.apiWithDialog('drive/files/move-bulk', {
|
||||
fileIds: selectedFiles.value.map(f => f.id),
|
||||
folderId: toFolder[0] ? toFolder[0].id : null,
|
||||
});
|
||||
|
||||
globalEvents.emit('driveFilesMoved', selectedFiles.value, toFolder[0]);
|
||||
}
|
||||
|
||||
function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
|
||||
const current = folder.value ? folder.value.id : null;
|
||||
if (current !== fileToAdd.folderId) return;
|
||||
|
||||
if (files.value.some(f => f.id === fileToAdd.id)) {
|
||||
const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
|
||||
files.value[exist] = fileToAdd;
|
||||
return;
|
||||
}
|
||||
|
||||
if (unshift) {
|
||||
files.value.unshift(fileToAdd);
|
||||
} else {
|
||||
files.value.push(fileToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
|
||||
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
|
||||
folders.value = folders.value.filter(f => f.id !== folderIdToRemove);
|
||||
}
|
||||
|
||||
function removeFile(file: Misskey.entities.DriveFile | string) {
|
||||
const fileId = typeof file === 'object' ? file.id : file;
|
||||
files.value = files.value.filter(f => f.id !== fileId);
|
||||
}
|
||||
|
||||
function appendFile(file: Misskey.entities.DriveFile) {
|
||||
addFile(file);
|
||||
}
|
||||
|
||||
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
|
||||
addFolder(folderToAppend);
|
||||
}
|
||||
|
||||
/*
|
||||
function prependFile(file: Misskey.entities.DriveFile) {
|
||||
addFile(file, true);
|
||||
}
|
||||
|
||||
function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
|
||||
addFolder(folderToPrepend, true);
|
||||
}
|
||||
*/
|
||||
function goRoot() {
|
||||
// 既にrootにいるなら何もしない
|
||||
if (folder.value == null) return;
|
||||
|
|
@ -527,95 +551,7 @@ function goRoot() {
|
|||
folder.value = null;
|
||||
hierarchyFolders.value = [];
|
||||
emit('move-root');
|
||||
fetch();
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
folders.value = [];
|
||||
files.value = [];
|
||||
moreFolders.value = false;
|
||||
moreFiles.value = false;
|
||||
fetching.value = true;
|
||||
|
||||
const foldersMax = 30;
|
||||
const filesMax = 30;
|
||||
|
||||
const foldersPromise = misskeyApi('drive/folders', {
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
limit: foldersMax + 1,
|
||||
}).then(fetchedFolders => {
|
||||
if (fetchedFolders.length === foldersMax + 1) {
|
||||
moreFolders.value = true;
|
||||
fetchedFolders.pop();
|
||||
}
|
||||
return fetchedFolders;
|
||||
});
|
||||
|
||||
const filesPromise = misskeyApi('drive/files', {
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
type: props.type,
|
||||
limit: filesMax + 1,
|
||||
sort: sortModeSelect.value,
|
||||
}).then(fetchedFiles => {
|
||||
if (fetchedFiles.length === filesMax + 1) {
|
||||
moreFiles.value = true;
|
||||
fetchedFiles.pop();
|
||||
}
|
||||
return fetchedFiles;
|
||||
});
|
||||
|
||||
const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
|
||||
|
||||
for (const x of fetchedFolders) appendFolder(x);
|
||||
for (const x of fetchedFiles) appendFile(x);
|
||||
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
function fetchMoreFolders() {
|
||||
fetching.value = true;
|
||||
|
||||
const max = 30;
|
||||
|
||||
misskeyApi('drive/folders', {
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
type: props.type,
|
||||
untilId: folders.value.at(-1)?.id,
|
||||
limit: max + 1,
|
||||
}).then(folders => {
|
||||
if (folders.length === max + 1) {
|
||||
moreFolders.value = true;
|
||||
folders.pop();
|
||||
} else {
|
||||
moreFolders.value = false;
|
||||
}
|
||||
for (const x of folders) appendFolder(x);
|
||||
fetching.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMoreFiles() {
|
||||
fetching.value = true;
|
||||
|
||||
const max = 30;
|
||||
|
||||
// ファイル一覧取得
|
||||
misskeyApi('drive/files', {
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
type: props.type,
|
||||
untilId: files.value.at(-1)?.id,
|
||||
limit: max + 1,
|
||||
sort: sortModeSelect.value,
|
||||
}).then(files => {
|
||||
if (files.length === max + 1) {
|
||||
moreFiles.value = true;
|
||||
files.pop();
|
||||
} else {
|
||||
moreFiles.value = false;
|
||||
}
|
||||
for (const x of files) appendFile(x);
|
||||
fetching.value = false;
|
||||
});
|
||||
initialize();
|
||||
}
|
||||
|
||||
function getMenu() {
|
||||
|
|
@ -692,6 +628,11 @@ function getMenu() {
|
|||
text: i18n.ts.createFolder,
|
||||
icon: 'ti ti-folder-plus',
|
||||
action: () => { createFolder(); },
|
||||
}, { type: 'divider' }, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.edit,
|
||||
icon: 'ti ti-pointer',
|
||||
ref: isEditMode,
|
||||
});
|
||||
|
||||
return menu;
|
||||
|
|
@ -709,42 +650,54 @@ function closeTip() {
|
|||
store.set('readDriveTip', true);
|
||||
}
|
||||
|
||||
useGlobalEvent('driveFilesMoved', (files, to) => {
|
||||
for (const f of files) {
|
||||
filesPaginator.removeItem(f.id);
|
||||
}
|
||||
if ((to?.id ?? null) === (folder.value?.id ?? null)) {
|
||||
filesPaginator.unshiftItems(files);
|
||||
}
|
||||
});
|
||||
|
||||
let connection: Misskey.ChannelConnection<Misskey.Channels['drive']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
if (store.s.realtimeMode) {
|
||||
connection = useStream().useChannel('drive');
|
||||
connection.on('fileCreated', onStreamDriveFileCreated);
|
||||
}
|
||||
|
||||
connection.on('fileCreated', onStreamDriveFileCreated);
|
||||
connection.on('fileUpdated', onStreamDriveFileUpdated);
|
||||
connection.on('fileDeleted', onStreamDriveFileDeleted);
|
||||
connection.on('folderCreated', onStreamDriveFolderCreated);
|
||||
connection.on('folderUpdated', onStreamDriveFolderUpdated);
|
||||
connection.on('folderDeleted', onStreamDriveFolderDeleted);
|
||||
|
||||
if (props.initialFolder) {
|
||||
move(props.initialFolder);
|
||||
cd(props.initialFolder);
|
||||
} else {
|
||||
fetch();
|
||||
initialize();
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (prefer.s.enableInfiniteScroll) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
ilFilesObserver.disconnect();
|
||||
if (connection != null) {
|
||||
connection.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_files_move,
|
||||
.transition_files_enterActive,
|
||||
.transition_files_leaveActive {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.transition_files_enterFrom,
|
||||
.transition_files_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
.transition_files_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
@ -799,9 +752,6 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--MI-margin);
|
||||
user-select: none;
|
||||
|
||||
&.fetching {
|
||||
|
|
@ -817,22 +767,26 @@ onBeforeUnmount(() => {
|
|||
|
||||
.folders,
|
||||
.files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 12px;
|
||||
padding: var(--MI-margin);
|
||||
}
|
||||
|
||||
.folder,
|
||||
.file {
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
.date {
|
||||
padding: 8px 16px;
|
||||
font-size: 90%;
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
|
||||
backdrop-filter: var(--MI-blur, blur(8px));
|
||||
background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.85);
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
.footer {
|
||||
padding: 8px 16px;
|
||||
font-size: 90%;
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
|
||||
backdrop-filter: var(--MI-blur, blur(8px));
|
||||
background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.85);
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MkDriveSelectDialog from './MkDriveSelectDialog.vue';
|
||||
import MkDriveSelectDialog from './MkDriveFileSelectDialog.vue';
|
||||
void MkDriveSelectDialog;
|
||||
|
|
@ -9,43 +9,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:width="800"
|
||||
:height="500"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="(type === 'file') && (selected.length === 0)"
|
||||
:okButtonDisabled="selected.length === 0"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
{{ multiple ? i18n.ts.selectFiles : i18n.ts.selectFile }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length }})</span>
|
||||
</template>
|
||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
||||
<MkDrive :multiple="multiple" select="file" :initialFolder="initialFolder" @changeSelectedFiles="onChangeSelection"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import MkDrive from '@/components/MkDrive.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
type?: 'file' | 'folder';
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
multiple: boolean;
|
||||
}>(), {
|
||||
type: 'file',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', r?: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
|
||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
||||
function ok() {
|
||||
emit('done', selected.value);
|
||||
|
|
@ -57,7 +55,7 @@ function cancel() {
|
|||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) {
|
||||
function onChangeSelection(v: Misskey.entities.DriveFile[]) {
|
||||
selected.value = v;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="selected.length === 0"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ multiple ? i18n.ts.selectFolders : i18n.ts.selectFolder }}
|
||||
<span v-if="multiple && selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length }})</span>
|
||||
</template>
|
||||
<MkDrive :multiple="multiple" select="folder" :initialFolder="initialFolder" @changeSelectedFolders="onChangeSelection"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkDrive from '@/components/MkDrive.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
multiple?: boolean;
|
||||
}>(), {
|
||||
initialFolder: null,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', r?: Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
|
||||
function ok() {
|
||||
emit('done', selected.value);
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('done');
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onChangeSelection(v: Misskey.entities.DriveFolder[]) {
|
||||
selected.value = v;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -137,6 +137,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
|
@ -700,8 +701,7 @@ async function onPaste(ev: ClipboardEvent) {
|
|||
function onDragover(ev) {
|
||||
if (!ev.dataTransfer.items[0]) return;
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles'])) {
|
||||
ev.preventDefault();
|
||||
draghover.value = true;
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
|
|
@ -742,11 +742,12 @@ function onDrop(ev: DragEvent): void {
|
|||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
files.value.push(file);
|
||||
ev.preventDefault();
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
files.value.push(...droppedData);
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,15 @@ export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoint
|
|||
|
||||
baseId?: MisskeyEntity['id'];
|
||||
direction?: 'newer' | 'older';
|
||||
|
||||
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
|
||||
canFetchDetection?: 'safe' | 'limit';
|
||||
};
|
||||
|
||||
export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I : never>(props: {
|
||||
ctx: PagingCtx<Endpoint>;
|
||||
autoInit?: boolean;
|
||||
autoReInit?: boolean;
|
||||
useShallowRef?: boolean;
|
||||
}) {
|
||||
const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
|
||||
|
|
@ -49,8 +54,9 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
|
|||
const canFetchOlder = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
||||
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
|
||||
if (props.autoReInit !== false) {
|
||||
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
|
||||
}
|
||||
|
||||
function getNewestId(): string | null | undefined {
|
||||
// 様々な要因により並び順は保証されないのでソートが必要
|
||||
|
|
@ -92,12 +98,20 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
|
|||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (res.length === 0 || props.ctx.noPaging) {
|
||||
pushItems(res);
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
pushItems(res);
|
||||
canFetchOlder.value = true;
|
||||
pushItems(res);
|
||||
|
||||
if (props.ctx.canFetchDetection === 'limit') {
|
||||
if (res.length < FIRST_FETCH_LIMIT) {
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
canFetchOlder.value = true;
|
||||
}
|
||||
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
|
||||
if (res.length === 0 || props.ctx.noPaging) {
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
canFetchOlder.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
error.value = false;
|
||||
|
|
@ -130,13 +144,20 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
|
|||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
canFetchOlder.value = false;
|
||||
fetchingOlder.value = false;
|
||||
} else {
|
||||
pushItems(res);
|
||||
canFetchOlder.value = true;
|
||||
fetchingOlder.value = false;
|
||||
pushItems(res);
|
||||
|
||||
if (props.ctx.canFetchDetection === 'limit') {
|
||||
if (res.length < FIRST_FETCH_LIMIT) {
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
canFetchOlder.value = true;
|
||||
}
|
||||
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
|
||||
if (res.length === 0) {
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
canFetchOlder.value = true;
|
||||
}
|
||||
}
|
||||
}, err => {
|
||||
fetchingOlder.value = false;
|
||||
|
|
@ -232,9 +253,11 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
|
|||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
if (props.autoInit !== false) {
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: items as DeepReadonly<ShallowRef<T[]>>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
type DragDataMap = {
|
||||
driveFiles: Misskey.entities.DriveFile[];
|
||||
driveFolders: Misskey.entities.DriveFolder[];
|
||||
deckColumn: string;
|
||||
};
|
||||
|
||||
export function setDragData<T extends keyof DragDataMap>(
|
||||
event: DragEvent,
|
||||
type: T,
|
||||
data: DragDataMap[T],
|
||||
) {
|
||||
if (event.dataTransfer == null) return;
|
||||
|
||||
event.dataTransfer.setData(`misskey/${type}`, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getDragData<T extends keyof DragDataMap>(
|
||||
event: DragEvent,
|
||||
type: T,
|
||||
): DragDataMap[T] | null {
|
||||
if (event.dataTransfer == null) return null;
|
||||
|
||||
const data = event.dataTransfer.getData(`misskey/${type}`);
|
||||
if (data == null || data === '') return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
export function checkDragDataType(
|
||||
event: DragEvent,
|
||||
types: (keyof DragDataMap)[],
|
||||
): boolean {
|
||||
if (event.dataTransfer == null) return false;
|
||||
|
||||
const dataType = event.dataTransfer.types[0];
|
||||
if (dataType == null || dataType === '') return false;
|
||||
|
||||
return types.some((type) => dataType === `misskey/${type}`);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ type Events = {
|
|||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||
notePosted: (note: Misskey.entities.Note) => void;
|
||||
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
|
||||
driveFilesMoved: (files: Misskey.entities.DriveFile[], to: Misskey.entities.DriveFolder | null) => void;
|
||||
};
|
||||
|
||||
export const globalEvents = new EventEmitter<Events>();
|
||||
|
|
|
|||
|
|
@ -594,8 +594,7 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
|
|||
|
||||
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||
type: 'file',
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveFileSelectDialog.vue')), {
|
||||
multiple,
|
||||
}, {
|
||||
done: files => {
|
||||
|
|
@ -608,11 +607,10 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
|
|||
});
|
||||
}
|
||||
|
||||
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
|
||||
export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<Misskey.entities.DriveFolder[]> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||
type: 'folder',
|
||||
multiple,
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveFolderSelectDialog.vue')), {
|
||||
initialFolder,
|
||||
}, {
|
||||
done: folders => {
|
||||
if (folders) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||
import { emojiPicker } from '@/utility/emoji-picker.js';
|
||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const props = defineProps<{
|
||||
user?: Misskey.entities.UserDetailed | null;
|
||||
|
|
@ -100,8 +101,7 @@ function onDragover(ev: DragEvent) {
|
|||
if (!ev.dataTransfer) return;
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles'])) {
|
||||
ev.preventDefault();
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
|
|
@ -140,10 +140,12 @@ function onDrop(ev: DragEvent): void {
|
|||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
file.value = JSON.parse(driveFile);
|
||||
ev.preventDefault();
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
file.value = droppedData[0];
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function crop() {
|
|||
function move() {
|
||||
if (!file.value) return;
|
||||
|
||||
os.selectDriveFolder(false).then(folder => {
|
||||
os.selectDriveFolder().then(folder => {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.value.id,
|
||||
folderId: folder[0] ? folder[0].id : null,
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ if (prefer.s.uploadFolder) {
|
|||
}
|
||||
|
||||
function chooseUploadFolder() {
|
||||
os.selectDriveFolder(false).then(async folder => {
|
||||
os.selectDriveFolder().then(async folder => {
|
||||
prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
|
||||
os.success();
|
||||
if (prefer.s.uploadFolder) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
|
||||
provide('shouldHeaderThin', true);
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
|
@ -262,7 +263,7 @@ function goTop() {
|
|||
|
||||
function onDragstart(ev) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||
setDragData(ev, 'deckColumn', props.column.id);
|
||||
|
||||
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||
|
|
@ -281,7 +282,7 @@ function onDragover(ev) {
|
|||
// 自分自身にはドロップさせない
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
} else {
|
||||
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
|
||||
const isDeckColumn = checkDragDataType(ev, ['deckColumn']);
|
||||
|
||||
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
|
||||
|
|
@ -297,8 +298,8 @@ function onDrop(ev) {
|
|||
draghover.value = false;
|
||||
os.deckGlobalEvents.emit('column.dragEnd');
|
||||
|
||||
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||
if (id != null && id !== '') {
|
||||
const id = getDragData(ev, 'deckColumn');
|
||||
if (id != null) {
|
||||
swapColumn(props.column.id, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function describe(file: Misskey.entities.DriveFile) {
|
|||
}
|
||||
|
||||
function move(file: Misskey.entities.DriveFile) {
|
||||
os.selectDriveFolder(false).then(folder => {
|
||||
os.selectDriveFolder().then(folder => {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: folder[0] ? folder[0].id : null,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Ref, ShallowRef } from 'vue';
|
||||
|
||||
export function getDateText(dateInstance: Date) {
|
||||
const date = dateInstance.getDate();
|
||||
|
|
@ -12,19 +12,6 @@ export function getDateText(dateInstance: Date) {
|
|||
return `${month.toString()}/${date.toString()}`;
|
||||
}
|
||||
|
||||
export type DateSeparetedTimelineItem<T> = {
|
||||
id: string;
|
||||
type: 'item';
|
||||
data: T;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'date';
|
||||
prev: Date;
|
||||
prevText: string;
|
||||
next: Date;
|
||||
nextText: string;
|
||||
};
|
||||
|
||||
// TODO: いちいちDateインスタンス作成するのは無駄感あるから文字列のまま解析したい
|
||||
export function isSeparatorNeeded(
|
||||
prev: string | null,
|
||||
|
|
@ -56,7 +43,20 @@ export function getSeparatorInfo(
|
|||
};
|
||||
}
|
||||
|
||||
export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]>) {
|
||||
export type DateSeparetedTimelineItem<T> = {
|
||||
id: string;
|
||||
type: 'item';
|
||||
data: T;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'date';
|
||||
prev: Date;
|
||||
prevText: string;
|
||||
next: Date;
|
||||
nextText: string;
|
||||
};
|
||||
|
||||
export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]> | ShallowRef<T[]>) {
|
||||
return computed<DateSeparetedTimelineItem<T>[]>(() => {
|
||||
const tl: DateSeparetedTimelineItem<T>[] = [];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
|
|
@ -92,3 +92,35 @@ export function makeDateSeparatedTimelineComputedRef<T extends { id: string; cre
|
|||
return tl;
|
||||
});
|
||||
}
|
||||
|
||||
export type DateGroupedTimelineItem<T> = {
|
||||
date: Date;
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export function makeDateGroupedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]> | ShallowRef<T[]>, span: 'day' | 'month' = 'day') {
|
||||
return computed<DateGroupedTimelineItem<T>[]>(() => {
|
||||
const tl: DateGroupedTimelineItem<T>[] = [];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
const date = new Date(item.createdAt);
|
||||
const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
|
||||
|
||||
if (tl.length === 0 || (
|
||||
span === 'day' && tl[tl.length - 1].date.getTime() !== date.getTime()
|
||||
) || (
|
||||
span === 'month' && (
|
||||
tl[tl.length - 1].date.getFullYear() !== date.getFullYear() ||
|
||||
tl[tl.length - 1].date.getMonth() !== date.getMonth()
|
||||
)
|
||||
)) {
|
||||
tl.push({
|
||||
date,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
tl[tl.length - 1].items.push(item);
|
||||
}
|
||||
return tl;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const fetch = () => {
|
|||
};
|
||||
|
||||
const choose = () => {
|
||||
os.selectDriveFolder(false).then(folder => {
|
||||
os.selectDriveFolder().then(folder => {
|
||||
if (folder[0] == null) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,9 +148,6 @@ export function getConfig(): UserConfig {
|
|||
_ENV_: JSON.stringify(process.env.NODE_ENV),
|
||||
_DEV_: process.env.NODE_ENV !== 'production',
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
_DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'),
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'),
|
||||
_DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue