-
-
-
-
-
-
-
![]()
+
+
+
+
+
+
+
-
+
@@ -35,27 +33,23 @@ import { onMounted, useTemplateRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
-import { apiUrl } from '@@/js/config.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js';
-import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
-import { getProxiedImageUrl } from '@/utility/media-proxy.js';
-import { prefer } from '@/preferences.js';
+
+const props = defineProps<{
+ imageFile: File | Blob;
+ aspectRatio: number | null;
+ uploadFolder?: string | null;
+}>();
const emit = defineEmits<{
- (ev: 'ok', cropped: Misskey.entities.DriveFile): void;
+ (ev: 'ok', cropped: File | Blob): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
-const props = defineProps<{
- file: Misskey.entities.DriveFile;
- aspectRatio: number;
- uploadFolder?: string | null;
-}>();
-
-const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
+const imgUrl = URL.createObjectURL(props.imageFile);
const dialogEl = useTemplateRef('dialogEl');
const imgEl = useTemplateRef('imgEl');
let cropper: Cropper | null = null;
@@ -73,31 +67,10 @@ const ok = async () => {
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => {
if (!blob) return;
- const formData = new FormData();
- formData.append('file', blob);
- formData.append('name', `cropped_${props.file.name}`);
- formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
- if (props.file.comment) { formData.append('comment', props.file.comment);}
- formData.append('i', $i!.token);
- if (props.uploadFolder) {
- formData.append('folderId', props.uploadFolder);
- } else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
- formData.append('folderId', prefer.s.uploadFolder);
- }
-
- window.fetch(apiUrl + '/drive/files/create', {
- method: 'POST',
- body: formData,
- })
- .then(response => response.json())
- .then(f => {
- res(f);
- });
+ res(blob);
});
});
- os.promiseDialog(promise);
-
const f = await promise;
emit('ok', f);
@@ -126,8 +99,8 @@ onMounted(() => {
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
- selection.aspectRatio = props.aspectRatio;
- selection.initialAspectRatio = props.aspectRatio;
+ if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio;
+ selection.initialAspectRatio = props.aspectRatio ?? 1;
selection.outlined = true;
window.setTimeout(() => {
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 70ab60cfae..bbb6a7ea2a 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.isSelected]: isSelected }]"
draggable="true"
:title="title"
- @click="onClick"
@contextmenu.stop="onContextmenu"
@dragstart="onDragstart"
@dragend="onDragend"
@@ -46,24 +45,18 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
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';
-
-const router = useRouter();
+import { setDragData } from '@/drag-and-drop.js';
const props = withDefaults(defineProps<{
file: Misskey.entities.DriveFile;
folder: Misskey.entities.DriveFolder | null;
isSelected?: boolean;
- selectMode?: boolean;
}>(), {
isSelected: false,
- selectMode: false,
});
const emit = defineEmits<{
- (ev: 'chosen', r: Misskey.entities.DriveFile): void;
- (ev: 'dragstart'): void;
+ (ev: 'dragstart', dragEvent: DragEvent): void;
(ev: 'dragend'): void;
}>();
@@ -71,18 +64,6 @@ const isDragging = ref(false);
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
-function onClick(ev: MouseEvent) {
- if (props.selectMode) {
- emit('chosen', props.file);
- } else {
- if (deviceKind === 'desktop') {
- router.push(`/my/drive/file/${props.file.id}`);
- } else {
- os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
- }
- }
-}
-
function onContextmenu(ev: MouseEvent) {
os.contextMenu(getDriveFileMenu(props.file, props.folder), ev);
}
@@ -90,11 +71,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 +95,7 @@ function onDragend() {
&:hover {
background: rgba(#000, 0.05);
- > .label {
+ .label {
&::before,
&::after {
background: #0b65a5;
@@ -132,7 +113,7 @@ function onDragend() {
&:active {
background: rgba(#000, 0.1);
- > .label {
+ .label {
&::before,
&::after {
background: #0b588c;
@@ -158,19 +139,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 +221,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);
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 9c72691d21..83472eec3d 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -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"
@@ -19,14 +18,13 @@ SPDX-License-Identifier: AGPL-3.0-only
@dragstart="onDragstart"
@dragend="onDragend"
>
-
-
-
- {{ folder.name }}
-
-
+
+
{{ folder.name }}
+
{{ i18n.ts.uploadFolder }}
-
+
@@ -43,6 +41,9 @@ 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';
+import { selectDriveFolder } from '@/utility/drive.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: 'upload', files: File[], folder: Misskey.entities.DriveFolder);
(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':
@@ -141,55 +132,64 @@ function onDrop(ev: DragEvent) {
// ファイルだったら
if (ev.dataTransfer.files.length > 0) {
- for (const file of Array.from(ev.dataTransfer.files)) {
- emit('upload', file, props.folder);
- }
+ emit('upload', Array.from(ev.dataTransfer.files), props.folder);
return;
}
//#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('driveFilesUpdated', droppedData.map(x => ({
+ ...x,
+ folderId: props.folder.id,
+ folder: props.folder,
+ })));
+ });
+ }
}
//#endregion
//#region ドライブのフォルダ
- const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder !== '') {
- const folder = JSON.parse(driveFolder);
-
- // 移動先が自分自身ならreject
- if (folder.id === props.folder.id) return;
-
- emit('removeFolder', folder.id);
- misskeyApi('drive/folders/update', {
- folderId: folder.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,
- });
- }
- });
+ {
+ const droppedData = getDragData(ev, 'driveFolders');
+ if (droppedData != null) {
+ const droppedFolder = droppedData[0];
+
+ // 移動先が自分自身ならreject
+ if (droppedFolder.id === props.folder.id) return;
+
+ misskeyApi('drive/folders/update', {
+ folderId: droppedFolder.id,
+ parentId: props.folder.id,
+ }).then(() => {
+ globalEvents.emit('driveFoldersUpdated', [droppedFolder].map(x => ({
+ ...x,
+ parentId: props.folder.id,
+ parent: props.folder,
+ })));
+ }).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 +198,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 +211,6 @@ function onDragend() {
emit('dragend');
}
-function go() {
- emit('move', props.folder);
-}
-
function rename() {
os.inputText({
title: i18n.ts.renameFolder,
@@ -225,17 +221,28 @@ function rename() {
misskeyApi('drive/folders/update', {
folderId: props.folder.id,
name: name,
+ }).then(() => {
+ globalEvents.emit('driveFoldersUpdated', [{
+ ...props.folder,
+ name: name,
+ }]);
});
});
}
function move() {
- os.selectDriveFolder(false).then(folder => {
+ selectDriveFolder(null).then(folder => {
if (folder[0] && folder[0].id === props.folder.id) return;
misskeyApi('drive/folders/update', {
folderId: props.folder.id,
parentId: folder[0] ? folder[0].id : null,
+ }).then(() => {
+ globalEvents.emit('driveFoldersUpdated', [{
+ ...props.folder,
+ parentId: folder[0] ? folder[0].id : null,
+ parent: folder[0] ?? null,
+ }]);
});
});
}
@@ -247,6 +254,7 @@ function deleteFolder() {
if (prefer.s.uploadFolder === props.folder.id) {
prefer.commit('uploadFolder', null);
}
+ globalEvents.emit('driveFoldersDeleted', [props.folder]);
}).catch(err => {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
@@ -311,10 +319,9 @@ function onContextmenu(ev: MouseEvent) {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5114e98494..d07ee2a978 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ maxTextLength - textLength }}
-
+
@@ -120,14 +120,13 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import { selectFiles } from '@/utility/select-file.js';
+import { selectFiles } from '@/utility/drive.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
-import { uploadFile } from '@/utility/upload.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
@@ -138,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();
@@ -459,18 +459,6 @@ function updateFileName(file, name) {
files.value[files.value.findIndex(x => x.id === file.id)].name = name;
}
-function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void {
- files.value[files.value.findIndex(x => x.id === file.id)] = newFile;
-}
-
-function upload(file: File, name?: string): void {
- if (props.mock) return;
-
- uploadFile(file, prefer.s.uploadFolder, name).then(res => {
- files.value.push(res);
- });
-}
-
function setVisibility() {
if (props.channel) {
visibility.value = 'public';
@@ -651,16 +639,25 @@ async function onPaste(ev: ClipboardEvent) {
if (props.mock) return;
if (!ev.clipboardData) return;
+ let pastedFiles: File[] = [];
for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (!file) continue;
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
- const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
- upload(file, formatted);
+ const formattedName = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+ const renamedFile = new File([file], formattedName, { type: file.type });
+ pastedFiles.push(renamedFile);
}
}
+ if (pastedFiles.length > 0) {
+ ev.preventDefault();
+ os.launchUploader(pastedFiles, {}).then(driveFiles => {
+ files.value.push(...driveFiles);
+ });
+ return;
+ }
const paste = ev.clipboardData.getData('text');
@@ -693,7 +690,9 @@ async function onPaste(ev: ClipboardEvent) {
const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
- upload(file, `${fileName}.txt`);
+ os.launchUploader([file], {}).then(driveFiles => {
+ files.value.push(...driveFiles);
+ });
});
}
}
@@ -701,8 +700,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) {
@@ -738,16 +736,19 @@ function onDrop(ev: DragEvent): void {
// ファイルだったら
if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
ev.preventDefault();
- for (const x of Array.from(ev.dataTransfer.files)) upload(x);
+ os.launchUploader(Array.from(ev.dataTransfer.files), {}).then(driveFiles => {
+ files.value.push(...driveFiles);
+ });
return;
}
//#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
}
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index e8404cbd4f..dd594ef7f1 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -43,6 +43,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
+import { globalEvents } from '@/events.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -58,7 +59,6 @@ const emit = defineEmits<{
(ev: 'detach', id: string): void;
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
- (ev: 'replaceFile', file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void;
}>();
let menuShowing = false;
@@ -82,12 +82,13 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
type: 'warning',
text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
});
-
if (canceled) return;
- os.apiWithDialog('drive/files/delete', {
+ await os.apiWithDialog('drive/files/delete', {
fileId: file.id,
});
+
+ globalEvents.emit('driveFilesDeleted', [file]);
}
function toggleSensitive(file) {
@@ -142,13 +143,6 @@ async function describe(file: Misskey.entities.DriveFile) {
});
}
-async function crop(file: Misskey.entities.DriveFile): Promise {
- if (mock) return;
-
- const newFile = await os.cropImage(file, { aspectRatio: NaN });
- emit('replaceFile', file, newFile);
-}
-
function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | KeyboardEvent): void {
if (menuShowing) return;
@@ -172,10 +166,6 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
if (isImage) {
menuItems.push({
- text: i18n.ts.cropImage,
- icon: 'ti ti-crop',
- action: () : void => { crop(file); },
- }, {
text: i18n.ts.preview,
icon: 'ti ti-photo-search',
action: () => {
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
index d8dfbd1655..6c7bf6be6b 100644
--- a/packages/frontend/src/components/MkPreview.vue
+++ b/packages/frontend/src/components/MkPreview.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
Pleroma
- This is
- the button
+ This is
+ the button
@@ -36,14 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 30925b854c..4e96eff82e 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,7 +37,6 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSlot from '@/components/form/slot.vue';
import MkInfo from '@/components/MkInfo.vue';
-import { chooseFileFromPc } from '@/utility/select-file.js';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
@@ -49,7 +48,7 @@ const description = ref($i.description ?? '');
watch(name, () => {
os.apiWithDialog('i/update', {
// 空文字列をnullにしたいので??は使うな
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+
name: name.value || null,
}, undefined, {
'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
@@ -62,36 +61,37 @@ watch(name, () => {
watch(description, () => {
os.apiWithDialog('i/update', {
// 空文字列をnullにしたいので??は使うな
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+
description: description.value || null,
});
});
-function setAvatar(ev) {
- chooseFileFromPc(false).then(async (files) => {
- const file = files[0];
+async function setAvatar(ev) {
+ const files = await os.chooseFileFromPc({ multiple: false });
+ const file = files[0];
- let originalOrCropped = file;
+ let originalOrCropped = file;
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts.cropImageAsk,
- okText: i18n.ts.cropYes,
- cancelText: i18n.ts.cropNo,
+ 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,
});
+ }
- if (!canceled) {
- originalOrCropped = await os.cropImage(file, {
- aspectRatio: 1,
- });
- }
+ const driveFile = (await os.launchUploader([originalOrCropped], { multiple: false }))[0];
- const i = await os.apiWithDialog('i/update', {
- avatarId: originalOrCropped.id,
- });
- $i.avatarId = i.avatarId;
- $i.avatarUrl = i.avatarUrl;
+ const i = await os.apiWithDialog('i/update', {
+ avatarId: driveFile.id,
});
+ $i.avatarId = i.avatarId;
+ $i.avatarUrl = i.avatarUrl;
}
diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue
index 3454cdc9f2..d2ef0fb2d8 100644
--- a/packages/frontend/src/components/global/MkSystemIcon.vue
+++ b/packages/frontend/src/components/global/MkSystemIcon.vue
@@ -28,13 +28,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -62,6 +66,10 @@ const props = defineProps<{
&.error {
color: var(--MI_THEME-error);
}
+
+ &.waiting {
+ color: var(--MI_THEME-accent);
+ }
}
.line {
@@ -87,6 +95,13 @@ const props = defineProps<{
transform: rotate(-90deg);
}
+.animCircleWaiting {
+ stroke-dasharray: var(--l);
+ stroke-dashoffset: calc(var(--l) / 1.5);
+ animation: waiting 0.75s linear infinite;
+ transform-origin: center;
+}
+
.animFade {
opacity: 0;
animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
@@ -104,6 +119,15 @@ const props = defineProps<{
}
}
+@keyframes waiting {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
@keyframes fade-in {
0% {
opacity: 0;
diff --git a/packages/frontend/src/drag-and-drop.ts b/packages/frontend/src/drag-and-drop.ts
new file mode 100644
index 0000000000..3c6f22f24b
--- /dev/null
+++ b/packages/frontend/src/drag-and-drop.ts
@@ -0,0 +1,48 @@
+/*
+ * 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;
+};
+
+// NOTE: dataTransfer の format は大文字小文字区別されないっぽいので toLowerCase が必要
+
+export function setDragData
(
+ event: DragEvent,
+ type: T,
+ data: DragDataMap[T],
+) {
+ if (event.dataTransfer == null) return;
+
+ event.dataTransfer.setData(`misskey/${type}`.toLowerCase(), JSON.stringify(data));
+}
+
+export function getDragData(
+ event: DragEvent,
+ type: T,
+): DragDataMap[T] | null {
+ if (event.dataTransfer == null) return null;
+
+ const data = event.dataTransfer.getData(`misskey/${type}`.toLowerCase());
+ 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) => `misskey/${type}`.toLowerCase() === dataType.toLowerCase());
+}
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index 26b1881d15..649561cd75 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -13,6 +13,11 @@ type Events = {
clientNotification: (notification: Misskey.entities.Notification) => void;
notePosted: (note: Misskey.entities.Note) => void;
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
+ driveFileCreated: (file: Misskey.entities.DriveFile) => void;
+ driveFilesUpdated: (files: Misskey.entities.DriveFile[]) => void;
+ driveFilesDeleted: (files: Misskey.entities.DriveFile[]) => void;
+ driveFoldersUpdated: (folders: Misskey.entities.DriveFolder[]) => void;
+ driveFoldersDeleted: (folders: Misskey.entities.DriveFolder[]) => void;
};
export const globalEvents = new EventEmitter();
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index d891525782..6d49408f26 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -592,38 +592,6 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
});
}
-export async function selectDriveFile(multiple: boolean): Promise {
- return new Promise(resolve => {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
- type: 'file',
- multiple,
- }, {
- done: files => {
- if (files) {
- resolve(files);
- }
- },
- closed: () => dispose(),
- });
- });
-}
-
-export async function selectDriveFolder(multiple: boolean): Promise {
- return new Promise(resolve => {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
- type: 'folder',
- multiple,
- }, {
- done: folders => {
- if (folders) {
- resolve(folders);
- }
- },
- closed: () => dispose(),
- });
- });
-}
-
export async function selectRole(params: ComponentProps): Promise<
{ canceled: true; result: undefined; } |
{ canceled: false; result: Misskey.entities.Role[] }
@@ -655,15 +623,13 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps {
+export async function cropImageFile(imageFile: File | Blob, options: {
+ aspectRatio: number | null;
+}): Promise {
return new Promise(resolve => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
- file: image,
+ imageFile: imageFile,
aspectRatio: options.aspectRatio,
- uploadFolder: options.uploadFolder,
}, {
ok: x => {
resolve(x);
@@ -775,3 +741,52 @@ export function checkExistence(fileData: ArrayBuffer): Promise {
});
});
}*/
+
+export function chooseFileFromPc(
+ options: {
+ multiple?: boolean;
+ } = {},
+): Promise {
+ return new Promise((res, rej) => {
+ const input = window.document.createElement('input');
+ input.type = 'file';
+ input.multiple = options.multiple ?? false;
+ input.onchange = () => {
+ if (!input.files) return res([]);
+
+ res(Array.from(input.files));
+
+ // 一応廃棄
+ (window as any).__misskey_input_ref__ = null;
+ };
+
+ // https://qiita.com/fukasawah/items/b9dc732d95d99551013d
+ // iOS Safari で正常に動かす為のおまじない
+ (window as any).__misskey_input_ref__ = input;
+
+ input.click();
+ });
+}
+
+export function launchUploader(
+ files: File[],
+ options?: {
+ folderId?: string | null;
+ multiple?: boolean;
+ },
+): Promise {
+ return new Promise((res, rej) => {
+ if (files.length === 0) return rej();
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), {
+ files: markRaw(files),
+ folderId: options?.folderId,
+ multiple: options?.multiple,
+ }, {
+ done: driveFiles => {
+ if (driveFiles.length === 0) return rej();
+ res(driveFiles);
+ },
+ closed: () => dispose(),
+ });
+ });
+}
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index 0ff251abb8..68c7048ae1 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -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';
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
index e8e944df32..e1dabe549f 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
@@ -35,20 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
- {{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }}
-
-
+
+ {{ i18n.ts.uplaod }}
+ {{ i18n.ts.fromDrive }}
@@ -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;
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 009514cdc8..355b5464a1 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -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';
diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue
index 9389b16ce7..7e3be67230 100644
--- a/packages/frontend/src/pages/chat/room.form.vue
+++ b/packages/frontend/src/pages/chat/room.form.vue
@@ -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() {
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 46e494e6f6..c2bc621f6a 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -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';
diff --git a/packages/frontend/src/pages/debug.vue b/packages/frontend/src/pages/debug.vue
index 4a28d513f5..5cd68c2c3a 100644
--- a/packages/frontend/src/pages/debug.vue
+++ b/packages/frontend/src/pages/debug.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 21be0b18a9..e8ac13c223 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -20,9 +20,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
@@ -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');
}
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index bee54f3fd2..38939f9503 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
- folder = x"/>
+ folder = x"/>
-
-
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 2085c73e03..4e79b301e3 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -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);
}
}
diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts
new file mode 100644
index 0000000000..e29b010c81
--- /dev/null
+++ b/packages/frontend/src/utility/drive.ts
@@ -0,0 +1,246 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+import { apiUrl } from '@@/js/config.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { useStream } from '@/stream.js';
+import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
+import { $i } from '@/i.js';
+import { instance } from '@/instance.js';
+import { globalEvents } from '@/events.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
+
+export function uploadFile(file: File | Blob, options: {
+ name?: string;
+ folderId?: string | null;
+ onProgress?: (ctx: { total: number; loaded: number; }) => void;
+} = {}): Promise
{
+ return new Promise((resolve, reject) => {
+ if ($i == null) return reject();
+
+ if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+ });
+ return reject();
+ }
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', apiUrl + '/drive/files/create', true);
+ xhr.onload = ((ev: ProgressEvent) => {
+ if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
+ if (xhr.status === 413) {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+ });
+ } else if (ev.target?.response) {
+ const res = JSON.parse(ev.target.response);
+ if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseInappropriate,
+ });
+ } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseNoFreeSpace,
+ });
+ } else {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
+ });
+ }
+ } else {
+ os.alert({
+ type: 'error',
+ title: 'Failed to upload',
+ text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
+ });
+ }
+
+ reject();
+ return;
+ }
+
+ const driveFile = JSON.parse(ev.target.response);
+ globalEvents.emit('driveFileCreated', driveFile);
+ resolve(driveFile);
+ }) as (ev: ProgressEvent) => any;
+
+ if (options.onProgress) {
+ xhr.upload.onprogress = ev => {
+ if (ev.lengthComputable) {
+ options.onProgress({
+ total: ev.total,
+ loaded: ev.loaded,
+ });
+ }
+ };
+ }
+
+ const formData = new FormData();
+ formData.append('i', $i.token);
+ formData.append('force', 'true');
+ formData.append('file', file);
+ formData.append('name', options.name ?? file.name ?? 'untitled');
+ if (options.folderId) formData.append('folderId', options.folderId);
+
+ xhr.send(formData);
+ });
+}
+
+export function chooseFileFromPcAndUpload(
+ options: {
+ multiple?: boolean;
+ folderId?: string | null;
+ } = {},
+): Promise {
+ return new Promise((res, rej) => {
+ os.chooseFileFromPc({ multiple: options.multiple }).then(files => {
+ if (files.length === 0) return;
+ os.launchUploader(files, {
+ folderId: options.folderId,
+ }).then(driveFiles => {
+ res(driveFiles);
+ });
+ });
+ });
+}
+
+export function chooseDriveFile(options: {
+ multiple?: boolean;
+} = {}): Promise {
+ return new Promise(resolve => {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveFileSelectDialog.vue')), {
+ multiple: options.multiple,
+ }, {
+ done: files => {
+ if (files) {
+ resolve(files);
+ }
+ },
+ closed: () => dispose(),
+ });
+ });
+}
+
+export function chooseFileFromUrl(): Promise {
+ return new Promise((res, rej) => {
+ os.inputText({
+ title: i18n.ts.uploadFromUrl,
+ type: 'url',
+ placeholder: i18n.ts.uploadFromUrlDescription,
+ }).then(({ canceled, result: url }) => {
+ if (canceled) return;
+
+ const marker = Math.random().toString(); // TODO: UUIDとか使う
+
+ // TODO: no websocketモード対応
+ const connection = useStream().useChannel('main');
+ connection.on('urlUploadFinished', urlResponse => {
+ if (urlResponse.marker === marker) {
+ res(urlResponse.file);
+ connection.dispose();
+ }
+ });
+
+ misskeyApi('drive/files/upload-from-url', {
+ url: url,
+ folderId: prefer.s.uploadFolder,
+ marker,
+ });
+
+ os.alert({
+ title: i18n.ts.uploadFromUrlRequested,
+ text: i18n.ts.uploadFromUrlMayTakeTime,
+ });
+ });
+ });
+}
+
+function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise {
+ return new Promise((res, rej) => {
+ os.popupMenu([label ? {
+ text: label,
+ type: 'label',
+ } : undefined, {
+ text: i18n.ts.upload,
+ icon: 'ti ti-upload',
+ action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
+ }, {
+ text: i18n.ts.fromDrive,
+ icon: 'ti ti-cloud',
+ action: () => chooseDriveFile({ multiple }).then(files => res(files)),
+ }, {
+ text: i18n.ts.fromUrl,
+ icon: 'ti ti-link',
+ action: () => chooseFileFromUrl().then(file => res([file])),
+ }], src);
+ });
+}
+
+export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise {
+ return select(src, label, false).then(files => files[0]);
+}
+
+export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise {
+ return select(src, label, true);
+}
+
+export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {
+ aspectRatio: number | null;
+}): Promise {
+ return new Promise(resolve => {
+ const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true);
+ const image = new Image();
+ image.src = imgUrl;
+ image.onload = () => {
+ const canvas = window.document.createElement('canvas');
+ const ctx = canvas.getContext('2d')!;
+ canvas.width = image.width;
+ canvas.height = image.height;
+ ctx.drawImage(image, 0, 0);
+ canvas.toBlob(blob => {
+ os.cropImageFile(blob, {
+ aspectRatio: options.aspectRatio,
+ }).then(croppedImageFile => {
+ uploadFile(croppedImageFile, {
+ name: imageDriveFile.name,
+ folderId: imageDriveFile.folderId,
+ }).then(driveFile => {
+ resolve(driveFile);
+ });
+ });
+ });
+ };
+ });
+}
+
+export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise {
+ return new Promise(resolve => {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveFolderSelectDialog.vue')), {
+ initialFolder,
+ }, {
+ done: folders => {
+ if (folders) {
+ resolve(folders);
+ }
+ },
+ closed: () => dispose(),
+ });
+ });
+}
diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
index 3c6cbba002..4b4bab3125 100644
--- a/packages/frontend/src/utility/get-drive-file-menu.ts
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -5,12 +5,14 @@
import * as Misskey from 'misskey-js';
import { defineAsyncComponent } from 'vue';
+import { selectDriveFolder } from './drive.js';
import type { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
+import { globalEvents } from '@/events.js';
function rename(file: Misskey.entities.DriveFile) {
os.inputText({
@@ -42,7 +44,7 @@ function describe(file: Misskey.entities.DriveFile) {
}
function move(file: Misskey.entities.DriveFile) {
- os.selectDriveFolder(false).then(folder => {
+ selectDriveFolder(null).then(folder => {
misskeyApi('drive/files/update', {
fileId: file.id,
folderId: folder[0] ? folder[0].id : null,
@@ -77,11 +79,13 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
type: 'warning',
text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
});
-
if (canceled) return;
- misskeyApi('drive/files/delete', {
+
+ await os.apiWithDialog('drive/files/delete', {
fileId: file.id,
});
+
+ globalEvents.emit('driveFilesDeleted', [file]);
}
export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
@@ -112,17 +116,6 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
action: () => describe(file),
});
- if (isImage) {
- menuItems.push({
- text: i18n.ts.cropImage,
- icon: 'ti ti-crop',
- action: () => os.cropImage(file, {
- aspectRatio: NaN,
- uploadFolder: folder ? folder.id : folder,
- }),
- });
- }
-
menuItems.push({ type: 'divider' }, {
text: i18n.ts.createNoteFromTheFile,
icon: 'ti ti-pencil',
diff --git a/packages/frontend/src/utility/isWebpSupported.ts b/packages/frontend/src/utility/isWebpSupported.ts
new file mode 100644
index 0000000000..affd81fd57
--- /dev/null
+++ b/packages/frontend/src/utility/isWebpSupported.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+let isWebpSupportedCache: boolean | undefined;
+export function isWebpSupported() {
+ if (isWebpSupportedCache === undefined) {
+ const canvas = window.document.createElement('canvas');
+ canvas.width = 1;
+ canvas.height = 1;
+ isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
+ }
+ return isWebpSupportedCache;
+}
diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts
deleted file mode 100644
index 731ef58302..0000000000
--- a/packages/frontend/src/utility/select-file.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
-import { useStream } from '@/stream.js';
-import { i18n } from '@/i18n.js';
-import { uploadFile } from '@/utility/upload.js';
-import { prefer } from '@/preferences.js';
-
-export function chooseFileFromPc(
- multiple: boolean,
- options?: {
- uploadFolder?: string | null;
- keepOriginal?: boolean;
- nameConverter?: (file: File) => string | undefined;
- },
-): Promise {
- const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
- const keepOriginal = options?.keepOriginal ?? false;
- const nameConverter = options?.nameConverter ?? (() => undefined);
-
- return new Promise((res, rej) => {
- const input = window.document.createElement('input');
- input.type = 'file';
- input.multiple = multiple;
- input.onchange = () => {
- if (!input.files) return res([]);
- const promises = Array.from(
- input.files,
- file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
- );
-
- Promise.all(promises).then(driveFiles => {
- res(driveFiles);
- }).catch(err => {
- // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
- });
-
- // 一応廃棄
- (window as any).__misskey_input_ref__ = null;
- };
-
- // https://qiita.com/fukasawah/items/b9dc732d95d99551013d
- // iOS Safari で正常に動かす為のおまじない
- (window as any).__misskey_input_ref__ = input;
-
- input.click();
- });
-}
-
-export function chooseFileFromDrive(multiple: boolean): Promise {
- return new Promise((res, rej) => {
- os.selectDriveFile(multiple).then(files => {
- res(files);
- });
- });
-}
-
-export function chooseFileFromUrl(): Promise {
- return new Promise((res, rej) => {
- os.inputText({
- title: i18n.ts.uploadFromUrl,
- type: 'url',
- placeholder: i18n.ts.uploadFromUrlDescription,
- }).then(({ canceled, result: url }) => {
- if (canceled) return;
-
- const marker = Math.random().toString(); // TODO: UUIDとか使う
-
- const connection = useStream().useChannel('main');
- connection.on('urlUploadFinished', urlResponse => {
- if (urlResponse.marker === marker) {
- res(urlResponse.file);
- connection.dispose();
- }
- });
-
- misskeyApi('drive/files/upload-from-url', {
- url: url,
- folderId: prefer.s.uploadFolder,
- marker,
- });
-
- os.alert({
- title: i18n.ts.uploadFromUrlRequested,
- text: i18n.ts.uploadFromUrlMayTakeTime,
- });
- });
- });
-}
-
-function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise {
- return new Promise((res, rej) => {
- os.popupMenu([label ? {
- text: label,
- type: 'label',
- } : undefined, {
- text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
- icon: 'ti ti-upload',
- action: () => chooseFileFromPc(multiple, { keepOriginal: false }).then(files => res(files)),
- }, {
- text: i18n.ts.upload,
- icon: 'ti ti-upload',
- action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)),
- }, {
- text: i18n.ts.fromDrive,
- icon: 'ti ti-cloud',
- action: () => chooseFileFromDrive(multiple).then(files => res(files)),
- }, {
- text: i18n.ts.fromUrl,
- icon: 'ti ti-link',
- action: () => chooseFileFromUrl().then(file => res([file])),
- }], src);
- });
-}
-
-export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise {
- return select(src, label, false).then(files => files[0]);
-}
-
-export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise {
- return select(src, label, true);
-}
diff --git a/packages/frontend/src/utility/timeline-date-separate.ts b/packages/frontend/src/utility/timeline-date-separate.ts
index 1071a80962..33ddea048b 100644
--- a/packages/frontend/src/utility/timeline-date-separate.ts
+++ b/packages/frontend/src/utility/timeline-date-separate.ts
@@ -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 = {
- 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(items: Ref) {
+export type DateSeparetedTimelineItem = {
+ id: string;
+ type: 'item';
+ data: T;
+} | {
+ id: string;
+ type: 'date';
+ prev: Date;
+ prevText: string;
+ next: Date;
+ nextText: string;
+};
+
+export function makeDateSeparatedTimelineComputedRef(items: Ref | ShallowRef) {
return computed[]>(() => {
const tl: DateSeparetedTimelineItem[] = [];
for (let i = 0; i < items.value.length; i++) {
@@ -92,3 +92,35 @@ export function makeDateSeparatedTimelineComputedRef = {
+ date: Date;
+ items: T[];
+};
+
+export function makeDateGroupedTimelineComputedRef(items: Ref | ShallowRef, span: 'day' | 'month' = 'day') {
+ return computed[]>(() => {
+ const tl: DateGroupedTimelineItem[] = [];
+ 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;
+ });
+}
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
deleted file mode 100644
index 03240749e9..0000000000
--- a/packages/frontend/src/utility/upload.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { reactive, ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import { v4 as uuid } from 'uuid';
-import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
-import { apiUrl } from '@@/js/config.js';
-import { getCompressionConfig } from './upload/compress-config.js';
-import { $i } from '@/i.js';
-import { alert } from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import { prefer } from '@/preferences.js';
-
-type Uploading = {
- id: string;
- name: string;
- progressMax: number | undefined;
- progressValue: number | undefined;
- img: string;
-};
-export const uploads = ref([]);
-
-const mimeTypeMap = {
- 'image/webp': 'webp',
- 'image/jpeg': 'jpg',
- 'image/png': 'png',
-} as const;
-
-export function uploadFile(
- file: File,
- folder?: string | Misskey.entities.DriveFolder | null,
- name?: string,
- keepOriginal = false,
-): Promise {
- if ($i == null) throw new Error('Not logged in');
-
- const _folder = typeof folder === 'string' ? folder : folder?.id;
-
- if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
- });
- return Promise.reject();
- }
-
- return new Promise((resolve, reject) => {
- const id = uuid();
-
- const reader = new FileReader();
- reader.onload = async (): Promise => {
- const filename = name ?? file.name ?? 'untitled';
- const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
-
- const ctx = reactive({
- id,
- name: prefer.s.keepOriginalFilename ? filename : id + extension,
- progressMax: undefined,
- progressValue: undefined,
- img: window.URL.createObjectURL(file),
- });
-
- uploads.value.push(ctx);
-
- const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
- let resizedImage: Blob | undefined;
- if (config) {
- try {
- const resized = await readAndCompressImage(file, config);
- if (resized.size < file.size || file.type === 'image/webp') {
- // The compression may not always reduce the file size
- // (and WebP is not browser safe yet)
- resizedImage = resized;
- }
- if (_DEV_) {
- const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
- console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
- }
-
- ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
- } catch (err) {
- console.error('Failed to resize image', err);
- }
- }
-
- const formData = new FormData();
- formData.append('i', $i!.token);
- formData.append('force', 'true');
- formData.append('file', resizedImage ?? file);
- formData.append('name', ctx.name);
- if (_folder) formData.append('folderId', _folder);
-
- const xhr = new XMLHttpRequest();
- xhr.open('POST', apiUrl + '/drive/files/create', true);
- xhr.onload = ((ev: ProgressEvent) => {
- if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
- // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
- uploads.value = uploads.value.filter(x => x.id !== id);
-
- if (xhr.status === 413) {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
- });
- } else if (ev.target?.response) {
- const res = JSON.parse(ev.target.response);
- if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseInappropriate,
- });
- } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseNoFreeSpace,
- });
- } else {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
- });
- }
- } else {
- alert({
- type: 'error',
- title: 'Failed to upload',
- text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
- });
- }
-
- reject();
- return;
- }
-
- const driveFile = JSON.parse(ev.target.response);
-
- resolve(driveFile);
-
- uploads.value = uploads.value.filter(x => x.id !== id);
- }) as (ev: ProgressEvent) => any;
-
- xhr.upload.onprogress = ev => {
- if (ev.lengthComputable) {
- ctx.progressMax = ev.total;
- ctx.progressValue = ev.loaded;
- }
- };
-
- xhr.send(formData);
- };
- reader.readAsArrayBuffer(file);
- });
-}
diff --git a/packages/frontend/src/utility/upload/compress-config.ts b/packages/frontend/src/utility/upload/compress-config.ts
deleted file mode 100644
index 3046b7f518..0000000000
--- a/packages/frontend/src/utility/upload/compress-config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import isAnimated from 'is-file-animated';
-import { isWebpSupported } from './isWebpSupported.js';
-import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
-
-const compressTypeMap = {
- 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
- 'image/png': { quality: 1, mimeType: 'image/webp' },
- 'image/webp': { quality: 0.90, mimeType: 'image/webp' },
- 'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
-} as const;
-
-const compressTypeMapFallback = {
- 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
- 'image/png': { quality: 1, mimeType: 'image/png' },
- 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
- 'image/svg+xml': { quality: 1, mimeType: 'image/png' },
-} as const;
-
-export async function getCompressionConfig(file: File): Promise {
- const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
- if (!imgConfig || await isAnimated(file)) {
- return;
- }
-
- return {
- maxWidth: 2048,
- maxHeight: 2048,
- debug: true,
- ...imgConfig,
- };
-}
diff --git a/packages/frontend/src/utility/upload/isWebpSupported.ts b/packages/frontend/src/utility/upload/isWebpSupported.ts
deleted file mode 100644
index affd81fd57..0000000000
--- a/packages/frontend/src/utility/upload/isWebpSupported.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-let isWebpSupportedCache: boolean | undefined;
-export function isWebpSupported() {
- if (isWebpSupportedCache === undefined) {
- const canvas = window.document.createElement('canvas');
- canvas.width = 1;
- canvas.height = 1;
- isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
- }
- return isWebpSupportedCache;
-}
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 2ccbb7a28f..3fe8cfa7e6 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -26,6 +26,7 @@ import type { GetFormResultType } from '@/utility/form.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
+import { selectDriveFolder } from '@/utility/drive.js';
const name = 'slideshow';
@@ -93,7 +94,7 @@ const fetch = () => {
};
const choose = () => {
- os.selectDriveFolder(false).then(folder => {
+ selectDriveFolder(null).then(folder => {
if (folder[0] == null) {
return;
}
--
cgit v1.2.3-freya