Merge branch 'drive' into develop

# Conflicts:
#	locales/index.d.ts
#	locales/ja-JP.yml
#	packages/frontend/src/components/MkDrive.folder.vue
#	packages/frontend/src/components/MkDrive.navFolder.vue
#	packages/frontend/src/components/MkDrive.vue
This commit is contained in:
mattyatea 2023-10-06 08:09:04 +09:00
commit ef3dac4b1e
6 changed files with 356 additions and 286 deletions

3
locales/index.d.ts vendored
View file

@ -79,6 +79,9 @@ export interface Locale {
"files": string; "files": string;
"download": string; "download": string;
"driveFileDeleteConfirm": string; "driveFileDeleteConfirm": string;
"driveFilesDeleteConfirm": string;
"driveFilesSensitiveonConfirm": string;
"driveFilesSensitiveoffConfirm": string;
"driveFolderDeleteConfirm": string; "driveFolderDeleteConfirm": string;
"unfollowConfirm": string; "unfollowConfirm": string;
"exportRequested": string; "exportRequested": string;

View file

@ -76,6 +76,9 @@ export: "エクスポート"
files: "ファイル" files: "ファイル"
download: "ダウンロード" download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。" driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。"
driveFilesDeleteConfirm: "{name}つのファイルを削除しますか?このファイルを使用した一部のコンテンツも削除されます。"
driveFilesSensitiveonConfirm: "{name}つのファイルをセンシティブにしますか?"
driveFilesSensitiveoffConfirm: "{name}つのファイルのセンシティブを解除しますか?"
driveFolderDeleteConfirm: "フォルダ「{name}」を削除しますか?このフォルダの中に存在するファイルを使用した一部のコンテンツも削除されます。" driveFolderDeleteConfirm: "フォルダ「{name}」を削除しますか?このフォルダの中に存在するファイルを使用した一部のコンテンツも削除されます。"
unfollowConfirm: "{name}のフォローを解除しますか?" unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.9.3-prismisskey.2", "version": "2023.9.3-prismisskey.3",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -18,6 +18,7 @@
"build-assets": "node ./scripts/build-assets.mjs", "build-assets": "node ./scripts/build-assets.mjs",
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"build-and-start": "pnpm build && pnpm start",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate", "init": "pnpm migrate",

View file

@ -18,7 +18,6 @@ SPDX-License-Identifier: AGPL-3.0-only
@drop.prevent.stop="onDrop" @drop.prevent.stop="onDrop"
@dragstart="onDragstart" @dragstart="onDragstart"
@dragend="onDragend" @dragend="onDragend"
> >
<p :class="$style.name"> <p :class="$style.name">
<template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> <template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
@ -45,7 +44,7 @@ const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder; folder: Misskey.entities.DriveFolder;
isSelected?: boolean; isSelected?: boolean;
selectMode?: boolean; selectMode?: boolean;
multipleselect?; selectedFiles?: string[];
}>(), { }>(), {
isSelected: false, isSelected: false,
selectMode: false, selectMode: false,
@ -145,8 +144,8 @@ function onDrop(ev: DragEvent) {
if (driveFile != null && driveFile !== '') { if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
emit('removeFile', file.id); emit('removeFile', file.id);
if (props.multipleselect.length > 0) { if (props.selectedFiles.length > 0) {
props.multipleselect.forEach((e)=>{ props.selectedFiles.forEach((e)=>{
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: e.id, fileId: e.id,
folderId: props.folder.id, folderId: props.folder.id,
@ -158,9 +157,7 @@ function onDrop(ev: DragEvent) {
folderId: props.folder.id, folderId: props.folder.id,
}); });
} }
}
}
//#endregion //#endregion
//#region //#region
@ -234,120 +231,120 @@ function rename() {
} }
function deleteFolder() { function deleteFolder() {
os.api('drive/folders/show', { os.api('drive/folders/show', {
folderId: props.folder.id,
}).then(async (r) => {
if (r.foldersCount > 0) {
await os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: 'フォルダ内にフォルダが存在するため、削除できません。 \n フォルダ内のフォルダを削除してから試してみてください。',
});
}
if (r.filesCount > 0) {
const {canceled} = await os.confirm({
type: 'warning',
text: i18n.t('driveFolderDeleteConfirm', {name: props.folder.name}),
});
if (canceled) return;
let allResults = [];
let Result = await os.api('drive/files', {folderId: props.folder.id, limit: 31});
allResults = allResults.concat(Result)
while (Result.length >= 31) {
const untilId = Result[Result.length - 1].id;
Result = await os.api('drive/files', { folderId: props.folder.id, limit: 31, untilId });
allResults = allResults.concat(Result); // pushconcat
}
allResults.forEach((r,i)=>{
os.api('drive/files/delete',{fileId: r.id})
})
os.api('drive/folders/show', {
folderId: props.folder.id, folderId: props.folder.id,
}).then(async (r) => { }).then(async (r) =>{
if (r.filesCount > 0) {
if (r.foldersCount > 0) { let allResults = [];
await os.alert({ let Result = await os.api('drive/files', {folderId: props.folder.id, limit: 31});
type: 'error', allResults = allResults.concat(Result)
title: i18n.ts.unableToDelete, while (Result.length >= 31) {
text: 'フォルダ内にフォルダが存在するため、削除できません。 \n フォルダ内のフォルダを削除してから試してみてください。', const untilId = Result[Result.length - 1].id;
}); Result = await os.api('drive/files', { folderId: props.folder.id, limit: 31, untilId });
} allResults = allResults.concat(Result);
}
allResults.forEach((r,i)=>{
os.api('drive/files/delete',{fileId: r.id})
})
if (r.filesCount > 0) { os.api('drive/folders/delete', {
folderId: props.folder.id,
const {canceled} = await os.confirm({ }).then(() => {
type: 'warning', if (defaultStore.state.uploadFolder === props.folder.id) {
text: i18n.t('driveFolderDeleteConfirm', {name: props.folder.name}), defaultStore.set('uploadFolder', null);
});
if (canceled) return;
let allResults = [];
let Result = await os.api('drive/files', {folderId: props.folder.id, limit: 31});
allResults = allResults.concat(Result)
while (Result.length >= 31) {
const untilId = Result[Result.length - 1].id;
Result = await os.api('drive/files', { folderId: props.folder.id, limit: 31, untilId });
allResults = allResults.concat(Result); // pushconcat
}
allResults.forEach((r,i)=>{
os.api('drive/files/delete',{fileId: r.id})
})
os.api('drive/folders/show', {
folderId: props.folder.id,
}).then(async (r) =>{
if (r.filesCount > 0) {
let allResults = [];
let Result = await os.api('drive/files', {folderId: props.folder.id, limit: 31});
allResults = allResults.concat(Result)
while (Result.length >= 31) {
const untilId = Result[Result.length - 1].id;
Result = await os.api('drive/files', { folderId: props.folder.id, limit: 31, untilId });
allResults = allResults.concat(Result); // pushconcat
} }
allResults.forEach((r,i)=>{ }).catch(err => {
console.log(r) switch (err.id) {
os.api('drive/files/delete',{fileId: r.id}) case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
}) os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.unableToDelete,
});
}
});
os.api('drive/folders/delete', { os.api('drive/folders/delete', {
folderId: props.folder.id, folderId: props.folder.id,
}).then(() => { })
if (defaultStore.state.uploadFolder === props.folder.id) { }else{
defaultStore.set('uploadFolder', null); os.api('drive/folders/delete', {
} folderId: props.folder.id,
}).catch(err => { })
switch (err.id) { }
case 'b0fc8a17-963c-405d-bfbc-859a487295e1': })
os.alert({
type: 'error', } else {
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders, await os.api('drive/folders/delete', {
}); folderId: props.folder.id,
break; }).then(() => {
default: if (defaultStore.state.uploadFolder === props.folder.id) {
os.alert({ defaultStore.set('uploadFolder', null);
type: 'error', }
text: i18n.ts.unableToDelete, }).catch(err => {
}); switch (err.id) {
} case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders,
}); });
break;
os.api('drive/folders/delete', { default:
folderId: props.folder.id, os.alert({
}) type: 'error',
}else{ text: i18n.ts.unableToDelete,
os.api('drive/folders/delete', { });
folderId: props.folder.id, }
}) });
} }
}) })
} else {
await os.api('drive/folders/delete', {
folderId: props.folder.id,
}).then(() => {
if (defaultStore.state.uploadFolder === props.folder.id) {
defaultStore.set('uploadFolder', null);
}
}).catch(err => {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.unableToDelete,
});
}
});
}
})
} }
function setAsUploadFolder() { function setAsUploadFolder() {
defaultStore.set('uploadFolder', props.folder.id); defaultStore.set('uploadFolder', props.folder.id);
} }

View file

@ -26,7 +26,7 @@ import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
folder?: Misskey.entities.DriveFolder; folder?: Misskey.entities.DriveFolder;
parentFolder: Misskey.entities.DriveFolder | null; parentFolder: Misskey.entities.DriveFolder | null;
multipleselect?; selectedFiles: string[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -113,22 +113,19 @@ function onDrop(ev: DragEvent) {
if (driveFile != null && driveFile !== '') { if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
emit('removeFile', file.id); emit('removeFile', file.id);
if (props.selectedFiles.length > 0) {
if (props.multipleselect.length > 0) { props.selectedFiles.forEach((e) => {
os.api('drive/files/update', {
props.multipleselect.forEach((e)=>{ fileId: e.id,
os.api('drive/files/update', { folderId: props.folder ? props.folder.id : null,
fileId: e.id, });
folderId: props.folder ? props.folder.id : null, });
}); } else {
}) os.api('drive/files/update', {
fileId: file.id,
}else{ folderId: props.folder ? props.folder.id : null,
os.api('drive/files/update', { });
fileId: file.id, }
folderId: props.folder ? props.folder.id : null,
});
}
} }
//#endregion //#endregion

View file

@ -4,12 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="$style.root" @scroll="handleScroll"> <div :class="$style.root">
<nav :class="$style.nav"> <nav :class="$style.nav">
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder <XNavFolder
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
:parentFolder="folder" :parentFolder="folder"
:selectedFiles="selectedFiles"
@move="move" @move="move"
@upload="upload" @upload="upload"
@removeFile="removeFile" @removeFile="removeFile"
@ -27,10 +28,23 @@ SPDX-License-Identifier: AGPL-3.0-only
@removeFolder="removeFolder" @removeFolder="removeFolder"
/> />
</template> </template>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i
class="ti ti-chevron-right"
></i></span>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
</div> </div>
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> <button v-if="!multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード</button>
<span v-if="multiple && selectedFiles.length > 0" style="padding-right: 12px; margin-top: auto; margin-bottom: auto;opacity: 0.5;">
({{ number(selectedFiles.length) }})
</span>
<button v-if="multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード解除</button>
<button v-if="multiple && selectedFiles.length === 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect">
全選択
</button>
<button v-if="multiple && selectedFiles.length !== 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect">
全選択解除
</button>
<button class="_button" @click="showMenu"><i class="ti ti-dots"></i></button>
</nav> </nav>
<div <div
ref="main" ref="main"
@ -51,6 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:folder="f" :folder="f"
:selectMode="select === 'folder'" :selectMode="select === 'folder'"
:isSelected="selectedFolders.some(x => x.id === f.id)" :isSelected="selectedFolders.some(x => x.id === f.id)"
:selectedFiles="selectedFiles"
@chosen="chooseFolder" @chosen="chooseFolder"
@move="move" @move="move"
@upload="upload" @upload="upload"
@ -73,15 +88,10 @@ SPDX-License-Identifier: AGPL-3.0-only
:folder="folder" :folder="folder"
:selectMode="select === 'file'" :selectMode="select === 'file'"
:isSelected="selectedFiles.some(x => x.id === file.id)" :isSelected="selectedFiles.some(x => x.id === file.id)"
:SelectFiles="SelectFiles" @click.shift.left.exact="filesSelect"
@chosen="chooseFile" @chosen="chooseFile"
@dragstart="isDragSource = true" @dragstart="isDragSource = true"
@dragend="isDragSource = false" @dragend="isDragSource = false"
@pointerdown="startLongPress(file)"
@pointerup="endLongPress"
@pointermove="debounceEvent"
@pointercancel="debounceEvent"
@click.shift.left.exact="multipleSelect(file)"
/> />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <!-- 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> <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
@ -89,14 +99,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty"> <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
<div v-if="draghover">{{ i18n.t('empty-draghover') }}</div> <div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div> <div v-if="!draghover && folder == null">
<strong>{{
i18n.ts.emptyDrive
}}</strong><br/>{{ i18n.t('empty-drive-description') }}
</div>
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div> <div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div> </div>
</div> </div>
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
</div> </div>
<div v-if="draghover" :class="$style.dropzone"></div> <div v-if="draghover" :class="$style.dropzone"></div>
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> <input
ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1"
@change="onChangeFileInput"
/>
</div> </div>
</template> </template>
@ -113,23 +130,24 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { uploadFile, uploads } from '@/scripts/upload.js'; import { uploadFile, uploads } from '@/scripts/upload.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { isTouchUsing } from '@/scripts/touch.js'; import number from "@/filters/number.js";
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder; initialFolder?: Misskey.entities.DriveFolder;
type?: string; type?: string;
multiple?: boolean; multiple?: boolean;
select?: 'file' | 'folder' | null; select?: 'file' | 'folder' | null;
}>(), { }>(), {
multiple: false, multiple: false,
select: null, select: null,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; (ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; (ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
(ev: 'move-root'): void; (ev: 'move-root'): void;
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void; (ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void; (ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
}>(); }>();
const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>(); const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
@ -146,10 +164,8 @@ const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads; const uploadings = uploads;
const connection = useStream().useChannel('drive'); const connection = useStream().useChannel('drive');
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // $ref使 const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // $ref使
const SelectFiles = ref([]); const multiple = ref(props.multiple || false);
const isLongPressing = ref(false); const select = ref(props.select || null);
let longPressTimeout = null;
const isScrolling = ref(false); //
// //
const draghover = ref(false); const draghover = ref(false);
@ -169,47 +185,6 @@ watch(folder, () => emit('cd', folder.value));
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
addFile(file, true); addFile(file, true);
} }
let timeout;
const debounceEvent = (event) => {
if (isTouchUsing && SelectFiles.value.length === 0) {
clearTimeout(timeout);
timeout = setTimeout(() => {
isLongPressing.value = false;
}, 900);
}else{
clearTimeout(timeout);
timeout = setTimeout(() => {
isLongPressing.value = false;
}, 80);
}
};
function startLongPress(file) {
console.log(SelectFiles.value.length)
if (isTouchUsing && SelectFiles.value.length === 0) {
isLongPressing.value = true;
longPressTimeout = setTimeout(() => {
if (isLongPressing.value) {
multipleSelect(file);
}
}, 1000);
}else{
isLongPressing.value = true;
longPressTimeout = setTimeout(() => {
if (isLongPressing.value) {
multipleSelect(file);
}
}, 100);
}
}
function endLongPress() {
if (isTouchUsing) {
isLongPressing.value = false;
clearTimeout(longPressTimeout);
}
}
function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) { function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
const current = folder.value ? folder.value.id : null; const current = folder.value ? folder.value.id : null;
@ -227,14 +202,7 @@ function onStreamDriveFileDeleted(fileId: string) {
function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) { function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
addFolder(createdFolder, true); addFolder(createdFolder, true);
} }
function multipleSelect(file) {
const index = SelectFiles.value.findIndex(item => item.id === file.id);
if (index !== -1) {
SelectFiles.value.splice(index, 1);
} else {
SelectFiles.value.push(file);
}
}
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) { function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
const current = folder.value ? folder.value.id : null; const current = folder.value ? folder.value.id : null;
if (current !== updatedFolder.parentId) { if (current !== updatedFolder.parentId) {
@ -448,7 +416,7 @@ function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null
function chooseFile(file: Misskey.entities.DriveFile) { function chooseFile(file: Misskey.entities.DriveFile) {
const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
if (props.multiple) { if (multiple.value) {
if (isAlreadySelected) { if (isAlreadySelected) {
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id); selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
} else { } else {
@ -467,7 +435,7 @@ function chooseFile(file: Misskey.entities.DriveFile) {
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id); const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
if (props.multiple) { if (multiple.value) {
if (isAlreadySelected) { if (isAlreadySelected) {
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id); selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
} else { } else {
@ -554,6 +522,7 @@ function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
function removeFile(file: Misskey.entities.DriveFile | string) { function removeFile(file: Misskey.entities.DriveFile | string) {
const fileId = typeof file === 'object' ? file.id : file; const fileId = typeof file === 'object' ? file.id : file;
files.value = files.value.filter(f => f.id !== fileId); files.value = files.value.filter(f => f.id !== fileId);
selectedFiles.value = selectedFiles.value.filter(f => f.id !== fileId);
} }
function appendFile(file: Misskey.entities.DriveFile) { function appendFile(file: Misskey.entities.DriveFile) {
@ -563,6 +532,7 @@ function appendFile(file: Misskey.entities.DriveFile) {
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) { function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
addFolder(folderToAppend); addFolder(folderToAppend);
} }
/* /*
function prependFile(file: Misskey.entities.DriveFile) { function prependFile(file: Misskey.entities.DriveFile) {
addFile(file, true); addFile(file, true);
@ -679,33 +649,130 @@ function getMenu() {
}, { }, {
text: i18n.ts.upload, text: i18n.ts.upload,
icon: 'ti ti-upload', icon: 'ti ti-upload',
action: () => { selectLocalFile(); }, action: () => {
selectLocalFile();
},
}, { }, {
text: i18n.ts.fromUrl, text: i18n.ts.fromUrl,
icon: 'ti ti-link', icon: 'ti ti-link',
action: () => { urlUpload(); }, action: () => {
urlUpload();
},
}, null, { }, null, {
text: folder.value ? folder.value.name : i18n.ts.drive, text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label', type: 'label',
}, folder.value ? { }, folder.value ? {
text: i18n.ts.renameFolder, text: i18n.ts.renameFolder,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: () => { renameFolder(folder.value); }, action: () => {
renameFolder(folder.value);
},
} : undefined, folder.value ? { } : undefined, folder.value ? {
text: i18n.ts.deleteFolder, text: i18n.ts.deleteFolder,
icon: 'ti ti-trash', icon: 'ti ti-trash',
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, action: () => {
deleteFolder(folder.value as Misskey.entities.DriveFolder);
},
} : undefined, { } : undefined, {
text: i18n.ts.createFolder, text: i18n.ts.createFolder,
icon: 'ti ti-folder-plus', icon: 'ti ti-folder-plus',
action: () => { createFolder(); }, action: () => {
createFolder();
},
}]; }];
} }
function showMenu(ev: MouseEvent) { async function isSensitive(Files, isSensitive: boolean) {
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t(isSensitive ? 'driveFilesSensitiveonConfirm' : 'driveFilesSensitiveoffConfirm', { name: Files.length }),
});
if (canceled) return;
Files.forEach((file) => {
os.api('drive/files/update', {
fileId: file.id,
isSensitive,
});
});
} }
const contents = ref(null);
async function fileDelete(Files) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('driveFilesDeleteConfirm', { name: Files.length }),
});
if (canceled) return;
Files.forEach((file) => {
os.api('drive/files/delete', {
fileId: file.id,
});
});
}
function getFilesMenu(Files) {
return [{
text: i18n.ts.createNoteFromTheFile,
icon: 'ti ti-pencil',
action: () => {
if (Files.length >= 16) {
os.confirm({
type: 'warning',
text: '16ファイル以上添付しようとしています',
});
return;
} else {
os.post({
initialFiles: [...Files],
});
}
},
}, {
text: i18n.ts.unmarkAsSensitive,
icon: 'ti ti-eye',
action: () => {
isSensitive(Files, false);
},
}, {
text: i18n.ts.markAsSensitive,
icon: 'ti ti-eye-exclamation',
action: () => {
isSensitive(Files, true);
},
}, {
text: i18n.ts.delete,
icon: 'ti ti-trash',
danger: true,
action: () => {
fileDelete(Files);
},
}];
}
function filesSelect() {
multiple.value = !multiple.value;
select.value = (select.value === null) ? 'file' : null;
selectedFiles.value = [];
}
function filesAllSelect() {
if (selectedFiles.value.length === 0) {
selectedFiles.value = files.value;
} else {
selectedFiles.value = [];
}
}
function showMenu(ev: MouseEvent) {
console.log(selectedFiles.value.length);
if (selectedFiles.value.length === 0) {
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
} else {
os.popupMenu(getFilesMenu(selectedFiles.value), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
}
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev); os.contextMenu(getMenu(), ev);
} }
@ -743,119 +810,121 @@ onBeforeUnmount(() => {
connection.dispose(); connection.dispose();
ilFilesObserver.disconnect(); ilFilesObserver.disconnect();
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root { .root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.nav { .nav {
display: flex; display: flex;
z-index: 2; top: 0;
width: 100%; position: sticky;
padding: 0 8px; z-index: 1000;
box-sizing: border-box; background-color: var(--bg);
overflow: auto; width: 100%;
font-size: 0.9em; padding: 0 8px;
box-shadow: 0 1px 0 var(--divider); box-sizing: border-box;
user-select: none; overflow: auto;
font-size: 0.9em;
box-shadow: 0 1px 0 var(--divider);
user-select: none;
} }
.navPath { .navPath {
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
line-height: 42px; line-height: 42px;
white-space: nowrap; white-space: nowrap;
} }
.navPathItem { .navPathItem {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
padding: 0 8px; padding: 0 8px;
line-height: 42px; line-height: 42px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
&.navCurrent { &.navCurrent {
font-weight: bold; font-weight: bold;
cursor: default; cursor: default;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
} }
&.navSeparator { &.navSeparator {
margin: 0; margin: 0;
padding: 0; padding: 0;
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;
} }
} }
.navMenu { .navMenu {
margin-left: auto; margin-left: auto;
padding: 0 12px; padding: 0 12px;
} }
.main { .main {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: var(--margin); padding: var(--margin);
user-select: none; user-select: none;
&.fetching { &.fetching {
cursor: wait !important; cursor: wait !important;
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
&.uploading { &.uploading {
height: calc(100% - 38px - 100px); height: calc(100% - 38px - 100px);
} }
} }
.folders, .folders,
.files { .files {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.folder, .folder,
.file { .file {
flex-grow: 1; flex-grow: 1;
width: 128px; width: 128px;
margin: 4px; margin: 4px;
box-sizing: border-box; box-sizing: border-box;
} }
.padding { .padding {
flex-grow: 1; flex-grow: 1;
pointer-events: none; pointer-events: none;
width: 128px + 8px; width: 128px + 8px;
} }
.empty { .empty {
padding: 16px; padding: 16px;
text-align: center; text-align: center;
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
} }
.dropzone { .dropzone {
position: absolute; position: absolute;
left: 0; left: 0;
top: 38px; top: 38px;
width: 100%; width: 100%;
height: calc(100% - 38px); height: calc(100% - 38px);
border: dashed 2px var(--focus); border: dashed 2px var(--focus);
pointer-events: none; pointer-events: none;
} }
</style> </style>