1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-24 16:14:11 +02:00

Feat: ドライブ周りのUIの強化 (#16011)

* wip

* wip

* Update MkDrive.vue

* wip

* Update MkDrive.vue

* Update MkDrive.vue

* wip

* Update MkDrive.vue

* Update MkDrive.vue

* wip

* Update MkDrive.vue

* wip

* wip

* wip

* wip

* Update MkDrive.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* feat(frontend): upload dialog (#16032)

* wip

* wip

* Update MkUploadDialog.vue

* wip

* wip

* wip

* wip

* wip

* Update MkUploadDialog.vue

* wip

* wip

* Update MkDrive.vue

* wip

* wip

* Update MkPostForm.vue

* wip

* Update room.form.vue

* Update os.ts

* wiop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update select-file.ts

* wip

* wip

* Update MkDrive.vue

* Update drag-and-drop.ts

* wip

* wip

* wop

* wip

* wip

* Update MkDrive.vue

* Update CHANGELOG.md

* wipo

* Update MkDrive.folder.vue

* wip

* Update MkUploaderDialog.vue

* wip

* wip

* Update MkUploaderDialog.vue

* wip

* Update MkDrive.vue

* Update MkDrive.vue

* wip

* wip
This commit is contained in:
syuilo
2025-05-21 07:31:24 +09:00
committed by GitHub
parent f74c38f313
commit 9480120eba
72 changed files with 1976 additions and 1468 deletions

View File

@@ -87,7 +87,7 @@ import MkButton from '@/components/MkButton.vue';
import { validators } from '@/components/grid/cell-validators.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
import { useLoading } from '@/composables/use-loading.js';

View File

@@ -35,20 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<XRegisterLogs :logs="requestLogs"/>
</MkFolder>
<div
:class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]"
@dragover.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false"
@drop.prevent.stop="onDrop"
>
<div style="margin-top: 1em">
{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }}
</div>
<ul>
<li>{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList1 }}</li>
<li><a @click.prevent="onFileSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList2 }}</a></li>
<li><a @click.prevent="onDriveSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList3 }}</a></li>
</ul>
<div class="_buttonsCenter">
<MkButton primary rounded @click="onFileSelectClicked">{{ i18n.ts.uplaod }}</MkButton>
<MkButton primary rounded @click="onDriveSelectClicked">{{ i18n.ts.fromDrive }}</MkButton>
</div>
<div v-if="gridItems.length > 0" :class="$style.gridArea">
@@ -94,8 +83,7 @@ import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { validators } from '@/components/grid/cell-validators.js';
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
import { uploadFile } from '@/utility/upload.js';
import { chooseDriveFile, chooseFileFromPcAndUpload } from '@/utility/drive.js';
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
@@ -311,75 +299,21 @@ async function onClearClicked() {
}
}
async function onDrop(ev: DragEvent) {
isDragOver.value = false;
const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it));
const confirm = await os.confirm({
type: 'info',
text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }),
});
if (confirm.canceled) {
return;
}
const uploadedItems = Array.of<{ droppedFile: DroppedFile, driveFile: Misskey.entities.DriveFile }>();
try {
uploadedItems.push(
...await os.promiseDialog(
Promise.all(
droppedFiles.map(async (it) => ({
droppedFile: it,
driveFile: await uploadFile(
it.file,
selectedFolderId.value,
it.file.name.replace(/\.[^.]+$/, ''),
true,
),
}),
),
),
() => {
},
() => {
},
),
);
} catch (err) {
// ダイアログは共通部品側で出ているはずなので何もしない
return;
}
const items = uploadedItems.map(({ droppedFile, driveFile }) => {
const item = fromDriveFile(driveFile);
if (directoryToCategory.value) {
item.category = droppedFile.path
.replace(/^\//, '')
.replace(/\/[^/]+$/, '')
.replace(droppedFile.file.name, '');
}
return item;
});
gridItems.value.push(...items);
}
async function onFileSelectClicked() {
const driveFiles = await chooseFileFromPc(
true,
{
uploadFolder: selectedFolderId.value,
keepOriginal: true,
// 拡張子は消す
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
},
);
const driveFiles = await chooseFileFromPcAndUpload({
multiple: true,
folderId: selectedFolderId.value,
// 拡張子は消す
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
});
gridItems.value.push(...driveFiles.map(fromDriveFile));
}
async function onDriveSelectClicked() {
const driveFiles = await chooseFileFromDrive(true);
const driveFiles = await chooseDriveFile({
multiple: true,
});
gridItems.value.push(...driveFiles.map(fromDriveFile));
}
@@ -436,23 +370,6 @@ onMounted(async () => {
background-color: var(--MI_THEME-infoWarnBg);
}
.uploadBox {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: auto;
border: 0.5px dotted var(--MI_THEME-accentedBg);
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-accentedBg);
box-sizing: border-box;
&.dragOver {
cursor: copy;
}
}
.gridArea {
padding-top: 8px;
padding-bottom: 8px;

View File

@@ -73,7 +73,7 @@ import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';

View File

@@ -38,15 +38,15 @@ import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly, onBefo
import * as Misskey from 'misskey-js';
//import insertTextAtCursor from 'insert-text-at-cursor';
import { formatTimeString } from '@/utility/format-time-string.js';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { uploadFile } from '@/utility/upload.js';
import { miLocalStorage } from '@/local-storage.js';
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;
@@ -84,8 +84,11 @@ async function onPaste(ev: ClipboardEvent) {
if (!pastedFile) return;
const lio = pastedFile.name.lastIndexOf('.');
const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
const formatted = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext;
if (formatted) upload(pastedFile, formatted);
const formattedName = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext;
const renamedFile = new File([pastedFile], formattedName, { type: pastedFile.type });
os.launchUploader([renamedFile], { multiple: false }).then(driveFiles => {
file.value = driveFiles[0];
});
}
} else {
if (items[0].kind === 'file') {
@@ -101,8 +104,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':
@@ -129,7 +131,7 @@ function onDrop(ev: DragEvent): void {
// ファイルだったら
if (ev.dataTransfer.files.length === 1) {
ev.preventDefault();
upload(ev.dataTransfer.files[0]);
os.launchUploader([Array.from(ev.dataTransfer.files)[0]], { multiple: false });
return;
} else if (ev.dataTransfer.files.length > 1) {
ev.preventDefault();
@@ -141,10 +143,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
}
@@ -172,13 +176,11 @@ function chooseFile(ev: MouseEvent) {
function onChangeFile() {
if (fileEl.value == null || fileEl.value.files == null) return;
if (fileEl.value.files[0]) upload(fileEl.value.files[0]);
}
function upload(fileToUpload: File, name?: string) {
uploadFile(fileToUpload, prefer.s.uploadFolder, name).then(res => {
file.value = res;
});
if (fileEl.value.files[0]) {
os.launchUploader(Array.from(fileEl.value.files), { multiple: false }).then(driveFiles => {
file.value = driveFiles[0];
});
}
}
function send() {

View File

@@ -78,7 +78,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';

View File

@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSystemIcon v-if="iconType === 'success'" type="success" style="width: 150px;"/>
<MkSystemIcon v-if="iconType === 'warn'" type="warn" style="width: 150px;"/>
<MkSystemIcon v-if="iconType === 'error'" type="error" style="width: 150px;"/>
<MkSystemIcon v-if="iconType === 'waiting'" type="waiting" style="width: 150px;"/>
<MkSelect
v-model="iconType" :items="[
{ label: 'info', value: 'info' },
@@ -30,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{ label: 'success', value: 'success' },
{ label: 'warn', value: 'warn' },
{ label: 'error', value: 'error' },
{ label: 'waiting', value: 'waiting' },
]"
></MkSelect>

View File

@@ -20,9 +20,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-tooltip="i18n.ts.createNoteFromTheFile" class="_button" :class="$style.fileQuickActionsOthersButton" @click="postThis()">
<i class="ti ti-pencil"></i>
</button>
<button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()">
<i class="ti ti-crop"></i>
</button>
<button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
<i class="ti ti-eye"></i>
</button>
@@ -83,6 +80,8 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useRouter } from '@/router.js';
import { selectDriveFolder } from '@/utility/drive.js';
import { globalEvents } from '@/events.js';
const router = useRouter();
@@ -127,19 +126,10 @@ function postThis() {
});
}
function crop() {
if (!file.value) return;
os.cropImage(file.value, {
aspectRatio: NaN,
uploadFolder: file.value.folderId ?? null,
});
}
function move() {
if (!file.value) return;
os.selectDriveFolder(false).then(folder => {
selectDriveFolder(null).then(folder => {
misskeyApi('drive/files/update', {
fileId: file.value.id,
folderId: folder[0] ? folder[0].id : null,
@@ -210,12 +200,14 @@ async function deleteFile() {
type: 'warning',
text: i18n.tsx.driveFileDeleteConfirm({ name: file.value.name }),
});
if (canceled) return;
await os.apiWithDialog('drive/files/delete', {
fileId: file.value.id,
});
globalEvents.emit('driveFilesDeleted', [file.value]);
router.push('/my/drive');
}

View File

@@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<XDrive @cd="x => folder = x"/>
<MkDrive @cd="x => folder = x"/>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XDrive from '@/components/MkDrive.vue';
import MkDrive from '@/components/MkDrive.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';

View File

@@ -91,7 +91,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { customEmojiCategories } from '@/custom-emojis.js';
import MkSwitch from '@/components/MkSwitch.vue';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{

View File

@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import { selectFiles } from '@/utility/select-file.js';
import { selectFiles } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';

View File

@@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XContainer from '../page-editor.container.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { chooseDriveFile } from '@/utility/drive.js';
const props = defineProps<{
modelValue: Misskey.entities.PageBlock & { type: 'image' };
@@ -41,7 +41,7 @@ const emit = defineEmits<{
const file = ref<Misskey.entities.DriveFile | null>(null);
async function choose() {
os.selectDriveFile(false).then((fileResponse) => {
chooseDriveFile({ multiple: false }).then((fileResponse) => {
file.value = fileResponse[0];
emit('update:modelValue', {
...props.modelValue,

View File

@@ -71,7 +71,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';

View File

@@ -164,7 +164,7 @@ import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';

View File

@@ -98,7 +98,7 @@ import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
const navWindow = prefer.model('deck.navWindow');
const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');

View File

@@ -99,6 +99,7 @@ import { ensureSignin } from '@/i.js';
import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { selectDriveFolder } from '@/utility/drive.js';
const $i = ensureSignin();
@@ -138,7 +139,7 @@ if (prefer.s.uploadFolder) {
}
function chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
selectDriveFolder(null).then(async folder => {
prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
os.success();
if (prefer.s.uploadFolder) {

View File

@@ -161,7 +161,7 @@ import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSlot from '@/components/form/slot.vue';
import { selectFile } from '@/utility/select-file.js';
import { chooseDriveFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i.js';
@@ -257,54 +257,100 @@ function save() {
}
function changeAvatar(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
if (!canceled) {
originalOrCropped = await os.cropImage(file, {
aspectRatio: 1,
});
}
async function done(driveFile) {
const i = await os.apiWithDialog('i/update', {
avatarId: originalOrCropped.id,
avatarId: driveFile.id,
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
claimAchievement('profileFilled');
});
}
os.popupMenu([{
text: i18n.ts.avatar,
type: 'label',
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: async () => {
const files = await os.chooseFileFromPc({ multiple: false });
const file = files[0];
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
if (!canceled) {
originalOrCropped = await os.cropImageFile(file, {
aspectRatio: 1,
});
}
const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0];
done(driveFile);
},
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
action: () => {
chooseDriveFile({ multiple: false }).then(files => {
done(files[0]);
});
},
}], ev.currentTarget ?? ev.target);
}
function changeBanner(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
if (!canceled) {
originalOrCropped = await os.cropImage(file, {
aspectRatio: 2,
});
}
async function done(driveFile) {
const i = await os.apiWithDialog('i/update', {
bannerId: originalOrCropped.id,
bannerId: driveFile.id,
});
$i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl;
});
}
os.popupMenu([{
text: i18n.ts.banner,
type: 'label',
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: async () => {
const files = await os.chooseFileFromPc({ multiple: false });
const file = files[0];
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
if (!canceled) {
originalOrCropped = await os.cropImageFile(file, {
aspectRatio: 2,
});
}
const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0];
done(driveFile);
},
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
action: () => {
chooseDriveFile({ multiple: false }).then(files => {
done(files[0]);
});
},
}], ev.currentTarget ?? ev.target);
}
const headerActions = computed(() => []);

View File

@@ -40,7 +40,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/select-file.js';
import { selectFile } from '@/utility/drive.js';
const props = defineProps<{
type: SoundType;