Merge 9f775395f5
into f74c38f313
This commit is contained in:
commit
b88e3dade8
|
@ -14,6 +14,14 @@
|
|||
- デフォルト値は「ローカルのコンテンツだけ公開」になっています
|
||||
|
||||
### Client
|
||||
- Feat: ドライブのUIが強化されました
|
||||
- 複数のファイルをまとめて移動できるようになりました
|
||||
- Feat: ファイルのアップロードUIが一新されました
|
||||
- アップロード前にファイル情報を確認できるようになりました
|
||||
- 圧縮の品質を選択できるようになりました
|
||||
- アップロードに失敗したときに再試行できるようになりました
|
||||
- アップロード前に画像のクロッピングを行えるようになりました
|
||||
- ファイルサイズのチェックは圧縮後の実際にアップロードされるサイズで行われるようになりました
|
||||
- Feat: サーバー初期設定ウィザードが実装されました
|
||||
- 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます
|
||||
- Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta)
|
||||
|
|
|
@ -1210,6 +1210,10 @@ export interface Locale extends ILocale {
|
|||
* アップロードが完了するまで時間がかかる場合があります。
|
||||
*/
|
||||
"uploadFromUrlMayTakeTime": string;
|
||||
/**
|
||||
* {n}個のファイルをアップロード
|
||||
*/
|
||||
"uploadNFiles": ParameterizedString<"n">;
|
||||
/**
|
||||
* みつける
|
||||
*/
|
||||
|
@ -8535,10 +8539,6 @@ export interface Locale extends ILocale {
|
|||
* 入力ボックスの縁取り
|
||||
*/
|
||||
"inputBorder": string;
|
||||
/**
|
||||
* ドライブフォルダーの背景
|
||||
*/
|
||||
"driveFolderBg": string;
|
||||
/**
|
||||
* バッジ
|
||||
*/
|
||||
|
@ -11463,22 +11463,6 @@ export interface Locale extends ILocale {
|
|||
* ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を"category"に入力します。
|
||||
*/
|
||||
"directoryToCategoryCaption": string;
|
||||
/**
|
||||
* いずれかの方法で登録する絵文字を選択してください。
|
||||
*/
|
||||
"emojiInputAreaCaption": string;
|
||||
/**
|
||||
* この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ
|
||||
*/
|
||||
"emojiInputAreaList1": string;
|
||||
/**
|
||||
* このリンクをクリックしてPCから選択する
|
||||
*/
|
||||
"emojiInputAreaList2": string;
|
||||
/**
|
||||
* このリンクをクリックしてドライブから選択する
|
||||
*/
|
||||
"emojiInputAreaList3": string;
|
||||
/**
|
||||
* リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)
|
||||
*/
|
||||
|
@ -11912,6 +11896,28 @@ export interface Locale extends ILocale {
|
|||
"text3": string;
|
||||
};
|
||||
};
|
||||
"_uploader": {
|
||||
/**
|
||||
* {x}に圧縮
|
||||
*/
|
||||
"compressedToX": ParameterizedString<"x">;
|
||||
/**
|
||||
* {x}%節約
|
||||
*/
|
||||
"savedXPercent": ParameterizedString<"x">;
|
||||
/**
|
||||
* アップロードされていないファイルがありますが、中止しますか?
|
||||
*/
|
||||
"abortConfirm": string;
|
||||
/**
|
||||
* アップロードされていないファイルがありますが、完了しますか?
|
||||
*/
|
||||
"doneConfirm": string;
|
||||
/**
|
||||
* アップロード可能な最大ファイルサイズは{x}です。
|
||||
*/
|
||||
"maxFileSizeIsX": ParameterizedString<"x">;
|
||||
};
|
||||
"_clientPerformanceIssueTip": {
|
||||
/**
|
||||
* バッテリー消費が多いと感じたら
|
||||
|
|
|
@ -298,6 +298,7 @@ uploadFromUrl: "URLアップロード"
|
|||
uploadFromUrlDescription: "アップロードしたいファイルのURL"
|
||||
uploadFromUrlRequested: "アップロードをリクエストしました"
|
||||
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
|
||||
uploadNFiles: "{n}個のファイルをアップロード"
|
||||
explore: "みつける"
|
||||
messageRead: "既読"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
|
@ -2237,7 +2238,6 @@ _theme:
|
|||
buttonBg: "ボタンの背景"
|
||||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
driveFolderBg: "ドライブフォルダーの背景"
|
||||
badge: "バッジ"
|
||||
messageBg: "チャットの背景"
|
||||
fgHighlighted: "強調された文字"
|
||||
|
@ -3056,10 +3056,6 @@ _customEmojisManager:
|
|||
uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。"
|
||||
directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する"
|
||||
directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。"
|
||||
emojiInputAreaCaption: "いずれかの方法で登録する絵文字を選択してください。"
|
||||
emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ"
|
||||
emojiInputAreaList2: "このリンクをクリックしてPCから選択する"
|
||||
emojiInputAreaList3: "このリンクをクリックしてドライブから選択する"
|
||||
confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)"
|
||||
confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?"
|
||||
confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?"
|
||||
|
@ -3186,6 +3182,13 @@ _serverSetupWizard:
|
|||
text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。"
|
||||
text3: "支援者向け特典もあります!"
|
||||
|
||||
_uploader:
|
||||
compressedToX: "{x}に圧縮"
|
||||
savedXPercent: "{x}%節約"
|
||||
abortConfirm: "アップロードされていないファイルがありますが、中止しますか?"
|
||||
doneConfirm: "アップロードされていないファイルがありますが、完了しますか?"
|
||||
maxFileSizeIsX: "アップロード可能な最大ファイルサイズは{x}です。"
|
||||
|
||||
_clientPerformanceIssueTip:
|
||||
title: "バッテリー消費が多いと感じたら"
|
||||
makeSureDisabledAdBlocker: "アドブロッカーを無効にしてください"
|
||||
|
|
|
@ -8,7 +8,7 @@ import * as fs from 'node:fs';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
|
||||
|
@ -720,6 +720,21 @@ export class DriveService {
|
|||
return fileObj;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) {
|
||||
const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({
|
||||
id: folderId,
|
||||
userId: userId,
|
||||
}) : null;
|
||||
|
||||
await this.driveFilesRepository.update({
|
||||
id: In(fileIds),
|
||||
userId: userId,
|
||||
}, {
|
||||
folderId: folder ? folder.id : null,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||
if (file.storedInternal) {
|
||||
|
|
|
@ -175,6 +175,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js';
|
|||
export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
|
||||
export * as 'drive/files/show' from './endpoints/drive/files/show.js';
|
||||
export * as 'drive/files/update' from './endpoints/drive/files/update.js';
|
||||
export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js';
|
||||
export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
|
||||
export * as 'drive/folders' from './endpoints/drive/folders.js';
|
||||
export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:drive',
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: ['fileIds'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private driveService: DriveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -10,9 +10,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
@ -30,9 +30,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
@ -11,9 +11,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
@ -35,9 +35,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
switchOnFg: '@accent',
|
||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
success: '#86b300',
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
switchOnFg: '@fgOnAccent',
|
||||
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
success: '#86b300',
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
navIndicator: '@accent',
|
||||
buttonGradateA: '@accent',
|
||||
buttonGradateB: ':hue<-20<@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
navIndicator: '@indicator',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
navIndicator: '@indicator',
|
||||
buttonHoverBg: '#0000001a',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
navIndicator: '@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':darken<3<@fg',
|
||||
fgOnWhite: '@accent',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
|
|
|
@ -10,9 +10,6 @@ declare const _VERSION_: string;
|
|||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
|
|
@ -30,9 +30,6 @@ export default [
|
|||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||
},
|
||||
parser,
|
||||
parserOptions: {
|
||||
|
|
|
@ -15,18 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.cropImage }}</template>
|
||||
<template #default="{ width, height }">
|
||||
<div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`">
|
||||
<Transition name="fade">
|
||||
<div v-if="loading" class="loading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
<div class="mk-cropper-dialog" :style="`--vw: 100%; --vh: 100%;`">
|
||||
<Transition name="fade">
|
||||
<div v-if="loading" class="loading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<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>
|
||||
{{ folder.name }}
|
||||
</p>
|
||||
<p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload">
|
||||
<svg :class="[$style.shape]" viewBox="0 0 200 150" preserveAspectRatio="none">
|
||||
<path d="M190,25C195.523,25 200,29.477 200,35C200,58.415 200,116.585 200,140C200,145.523 195.523,150 190,150C155.86,150 44.14,150 10,150C4.477,150 0,145.523 0,140C0,112.727 0,37.273 0,10C0,4.477 4.477,0 10,-0C26.642,0 59.332,0 70.858,0C73.51,-0 76.054,1.054 77.929,2.929C82.74,7.74 92.26,17.26 97.071,22.071C98.946,23.946 101.49,25 104.142,25C118.808,25 168.535,25 190,25Z" style="fill:var(--MI_THEME-accentedBg);"/>
|
||||
</svg>
|
||||
<div :class="$style.name">{{ folder.name }}</div>
|
||||
<div v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload">
|
||||
{{ i18n.ts.uploadFolder }}
|
||||
</p>
|
||||
</div>
|
||||
<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
|
||||
<div :class="[$style.checkbox, { [$style.checked]: isSelected }]"></div>
|
||||
</button>
|
||||
|
@ -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);
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFolders');
|
||||
if (droppedData != null) {
|
||||
const droppedFolder = droppedData[0];
|
||||
|
||||
// 移動先が自分自身ならreject
|
||||
if (folder.id === props.folder.id) return;
|
||||
// 移動先が自分自身ならreject
|
||||
if (droppedFolder.id === props.folder.id) return;
|
||||
|
||||
emit('removeFolder', folder.id);
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: props.folder.id,
|
||||
}).then(() => {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
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) {
|
|||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
height: 64px;
|
||||
background: var(--MI_THEME-driveFolderBg);
|
||||
border-radius: 4px;
|
||||
height: 90px;
|
||||
padding: 24px 16px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
&.draghover {
|
||||
|
@ -332,6 +339,14 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.checkboxWrapper {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
|
@ -373,7 +388,6 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
@ -384,7 +398,6 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
.upload {
|
||||
margin: 4px 4px;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div
|
||||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
@click="onClick"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
|
@ -22,6 +21,8 @@ import { ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const props = defineProps<{
|
||||
folder?: Misskey.entities.DriveFolder;
|
||||
|
@ -29,27 +30,11 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'move', v?: Misskey.entities.DriveFolder): void;
|
||||
(ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
|
||||
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
||||
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
||||
(ev: 'upload', files: File[], folder?: Misskey.entities.DriveFolder | null): void;
|
||||
}>();
|
||||
|
||||
const hover = ref(false);
|
||||
const draghover = ref(false);
|
||||
|
||||
function onClick() {
|
||||
emit('move', props.folder);
|
||||
}
|
||||
|
||||
function onMouseover() {
|
||||
hover.value = true;
|
||||
}
|
||||
|
||||
function onMouseout() {
|
||||
hover.value = false;
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
|
@ -59,10 +44,7 @@ function onDragover(ev: DragEvent) {
|
|||
}
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) {
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
|
@ -101,35 +83,46 @@ 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 ? props.folder.id : null,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFiles');
|
||||
if (droppedData != null) {
|
||||
misskeyApi('drive/files/move-bulk', {
|
||||
fileIds: droppedData.map(f => f.id),
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
}).then(() => {
|
||||
globalEvents.emit('driveFilesUpdated', droppedData.map(x => ({
|
||||
...x,
|
||||
folderId: props.folder ? props.folder.id : null,
|
||||
folder: props.folder ?? null,
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder !== '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
// 移動先が自分自身ならreject
|
||||
if (props.folder && folder.id === props.folder.id) return;
|
||||
emit('removeFolder', folder.id);
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: props.folder ? props.folder.id : null,
|
||||
});
|
||||
{
|
||||
const droppedData = getDragData(ev, 'driveFolders');
|
||||
if (droppedData != null) {
|
||||
const droppedFolder = droppedData[0];
|
||||
// 移動先が自分自身ならreject
|
||||
if (props.folder && droppedFolder.id === props.folder.id) return;
|
||||
misskeyApi('drive/folders/update', {
|
||||
folderId: droppedFolder.id,
|
||||
parentId: props.folder ? props.folder.id : null,
|
||||
}).then(() => {
|
||||
globalEvents.emit('driveFoldersUpdated', [droppedFolder].map(x => ({
|
||||
...x,
|
||||
parentId: props.folder ? props.folder.id : null,
|
||||
parent: props.folder ?? null,
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,5 +3,5 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MkDriveSelectDialog from './MkDriveSelectDialog.vue';
|
||||
import MkDriveSelectDialog from './MkDriveFileSelectDialog.vue';
|
||||
void MkDriveSelectDialog;
|
|
@ -9,43 +9,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:width="800"
|
||||
:height="500"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="(type === 'file') && (selected.length === 0)"
|
||||
:okButtonDisabled="selected.length === 0"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
{{ multiple ? i18n.ts.selectFiles : i18n.ts.selectFile }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length }})</span>
|
||||
</template>
|
||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
||||
<MkDrive :multiple="multiple" select="file" :initialFolder="initialFolder" @changeSelectedFiles="onChangeSelection"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import MkDrive from '@/components/MkDrive.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
type?: 'file' | 'folder';
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
multiple: boolean;
|
||||
}>(), {
|
||||
type: 'file',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', r?: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
|
||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
||||
function ok() {
|
||||
emit('done', selected.value);
|
||||
|
@ -57,7 +55,7 @@ function cancel() {
|
|||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) {
|
||||
function onChangeSelection(v: Misskey.entities.DriveFile[]) {
|
||||
selected.value = v;
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="selected.length === 0"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ multiple ? i18n.ts.selectFolders : i18n.ts.selectFolder }}
|
||||
<span v-if="multiple && selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length }})</span>
|
||||
</template>
|
||||
<MkDrive :multiple="multiple" select="folder" :initialFolder="initialFolder" @changeSelectedFolders="onChangeSelection"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkDrive from '@/components/MkDrive.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
multiple?: boolean;
|
||||
}>(), {
|
||||
initialFolder: null,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', r?: Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
|
||||
function ok() {
|
||||
emit('done', selected.value);
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('done');
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onChangeSelection(v: Misskey.entities.DriveFolder[]) {
|
||||
selected.value = v;
|
||||
}
|
||||
</script>
|
|
@ -14,19 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>
|
||||
{{ i18n.ts.drive }}
|
||||
</template>
|
||||
<XDrive :initialFolder="initialFolder"/>
|
||||
<MkDrive :initialFolder="initialFolder"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import MkDrive from '@/components/MkDrive.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
initialFolder?: Misskey.entities.DriveFolder | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import { selectFile } from '@/utility/drive.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
|
||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
|
||||
<div ref="headerEl" :class="$style.header">
|
||||
<div :class="$style.header">
|
||||
<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
|
||||
<span :class="$style.title">
|
||||
<slot name="header"></slot>
|
||||
|
@ -15,7 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="$slots.footer" :class="$style.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
|
@ -48,10 +51,6 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const modal = useTemplateRef('modal');
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const headerEl = useTemplateRef('headerEl');
|
||||
const bodyWidth = ref(0);
|
||||
const bodyHeight = ref(0);
|
||||
|
||||
function close() {
|
||||
modal.value?.close();
|
||||
|
@ -61,23 +60,6 @@ function onBgClick() {
|
|||
emit('click');
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (rootEl.value == null || headerEl.value == null) return;
|
||||
bodyWidth.value = rootEl.value.offsetWidth;
|
||||
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (rootEl.value == null || headerEl.value == null) return;
|
||||
bodyWidth.value = rootEl.value.offsetWidth;
|
||||
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
||||
ro.observe(rootEl.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
close,
|
||||
});
|
||||
|
@ -143,7 +125,14 @@ defineExpose({
|
|||
.body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--MI_THEME-panel);
|
||||
background: var(--MI_THEME-bg);
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 8px 16px;
|
||||
overflow: auto;
|
||||
background: var(--MI_THEME-bg);
|
||||
border-top: 1px solid var(--MI_THEME-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<void> {
|
||||
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: () => {
|
||||
|
|
|
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
|
||||
</div>
|
||||
<div :class="$style.preview__content1__button">
|
||||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.preview__content2" style="pointer-events: none;">
|
||||
|
@ -36,14 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as config from '@@/js/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { chooseDriveFile } from '@/utility/drive.js';
|
||||
|
||||
const text = ref('');
|
||||
const flag = ref(true);
|
||||
|
@ -79,7 +80,9 @@ const openForm = async () => {
|
|||
};
|
||||
|
||||
const openDrive = async () => {
|
||||
await os.selectDriveFile(false);
|
||||
await chooseDriveFile({
|
||||
multiple: false,
|
||||
});
|
||||
};
|
||||
|
||||
const selectUser = async () => {
|
||||
|
|
|
@ -0,0 +1,505 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
@close="cancel()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<i class="ti ti-upload"></i> {{ i18n.tsx.uploadNFiles({ n: files.length }) }}
|
||||
</template>
|
||||
|
||||
<div :class="$style.root">
|
||||
<div :class="[$style.overallProgress, canRetry ? $style.overallProgressError : null]" :style="{ '--op': `${overallProgress}%` }"></div>
|
||||
|
||||
<div :class="$style.main" class="_gaps_s">
|
||||
<div :class="$style.items" class="_gaps_s">
|
||||
<div
|
||||
v-for="ctx in items"
|
||||
:key="ctx.id"
|
||||
v-panel
|
||||
:class="[$style.item, ctx.waiting ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]"
|
||||
:style="{ '--p': ctx.progress != null ? `${ctx.progress.value / ctx.progress.max * 100}%` : '0%' }"
|
||||
>
|
||||
<div :class="$style.itemInner">
|
||||
<div :class="$style.itemActionWrapper">
|
||||
<MkButton :iconOnly="true" rounded @click="showMenu($event, ctx)"><i class="ti ti-dots"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
|
||||
<div :class="$style.itemBody">
|
||||
<div><MkCondensedLine :minScale="2 / 3">{{ ctx.name }}</MkCondensedLine></div>
|
||||
<div :class="$style.itemInfo">
|
||||
<span>{{ ctx.file.type }}</span>
|
||||
<span>{{ bytes(ctx.file.size) }}</span>
|
||||
<span v-if="ctx.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }})</span>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.itemIconWrapper">
|
||||
<MkSystemIcon v-if="ctx.uploading" :class="$style.itemIcon" type="waiting"/>
|
||||
<MkSystemIcon v-else-if="ctx.uploaded" :class="$style.itemIcon" type="success"/>
|
||||
<MkSystemIcon v-else-if="ctx.uploadFailed" :class="$style.itemIcon" type="error"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.multiple">
|
||||
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
||||
</div>
|
||||
|
||||
<MkSelect
|
||||
v-if="items.length > 0"
|
||||
v-model="compressionLevel"
|
||||
:items="[
|
||||
{ value: 0, label: i18n.ts.none },
|
||||
{ value: 1, label: i18n.ts.low },
|
||||
{ value: 2, label: i18n.ts.middle },
|
||||
{ value: 3, label: i18n.ts.high },
|
||||
]"
|
||||
>
|
||||
<template #label>{{ i18n.ts.compress }}</template>
|
||||
</MkSelect>
|
||||
|
||||
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="isUploading" rounded @click="cancel()"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
||||
|
||||
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
|
||||
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||
import isAnimated from 'is-file-animated';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
||||
import { uploadFile } from '@/utility/drive.js';
|
||||
import * as os from '@/os.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const COMPRESSION_SUPPORTED_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
];
|
||||
|
||||
const CROPPING_SUPPORTED_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
const mimeTypeMap = {
|
||||
'image/webp': 'webp',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
} as const;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
files: File[];
|
||||
folderId?: string | null;
|
||||
multiple?: boolean;
|
||||
}>(), {
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'canceled'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const items = ref([] as {
|
||||
id: string;
|
||||
name: string;
|
||||
progress: { max: number; value: number } | null;
|
||||
thumbnail: string;
|
||||
waiting: boolean;
|
||||
uploading: boolean;
|
||||
uploaded: Misskey.entities.DriveFile | null;
|
||||
uploadFailed: boolean;
|
||||
compressedSize?: number | null;
|
||||
compressedImage?: Blob | null;
|
||||
file: File;
|
||||
}[]);
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const firstUploadAttempted = ref(false);
|
||||
const isUploading = computed(() => items.value.some(item => item.uploading));
|
||||
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.waiting) && items.value.some(item => item.uploaded == null));
|
||||
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
||||
const overallProgress = computed(() => {
|
||||
const max = items.value.length;
|
||||
if (max === 0) return 0;
|
||||
const v = items.value.reduce((acc, item) => {
|
||||
if (item.uploaded) return acc + 1;
|
||||
if (item.progress) return acc + (item.progress.value / item.progress.max);
|
||||
return acc;
|
||||
}, 0);
|
||||
return Math.round((v / max) * 100);
|
||||
});
|
||||
|
||||
const compressionLevel = ref<0 | 1 | 2 | 3>(2);
|
||||
const compressionSettings = computed(() => {
|
||||
if (compressionLevel.value === 1) {
|
||||
return {
|
||||
maxWidth: 2000,
|
||||
maxHeight: 2000,
|
||||
};
|
||||
} else if (compressionLevel.value === 2) {
|
||||
return {
|
||||
maxWidth: 2000 * 0.75, // =1500
|
||||
maxHeight: 2000 * 0.75, // =1500
|
||||
};
|
||||
} else if (compressionLevel.value === 3) {
|
||||
return {
|
||||
maxWidth: 2000 * 0.75 * 0.75, // =1125
|
||||
maxHeight: 2000 * 0.75 * 0.75, // =1125
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(items, () => {
|
||||
if (items.value.length === 0) {
|
||||
emit('canceled');
|
||||
dialog.value?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.value.every(item => item.uploaded)) {
|
||||
emit('done', items.value.map(item => item.uploaded!));
|
||||
dialog.value?.close();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
async function cancel() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts._uploader.abortConfirm,
|
||||
okText: i18n.ts.yes,
|
||||
cancelText: i18n.ts.no,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
emit('canceled');
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function done() {
|
||||
if (items.value.some(item => item.uploaded == null)) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts._uploader.doneConfirm,
|
||||
okText: i18n.ts.yes,
|
||||
cancelText: i18n.ts.no,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
emit('done', items.value.filter(item => item.uploaded != null).map(item => item.uploaded!));
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
if (CROPPING_SUPPORTED_TYPES.includes(item.file.type) && !item.waiting && !item.uploading && !item.uploaded) {
|
||||
menu.push({
|
||||
icon: 'ti ti-crop',
|
||||
text: i18n.ts.cropImage,
|
||||
action: async () => {
|
||||
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
|
||||
items.value.splice(items.value.indexOf(item), 1, {
|
||||
...item,
|
||||
file: markRaw(cropped),
|
||||
thumbnail: window.URL.createObjectURL(cropped),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!item.waiting && !item.uploading && !item.uploaded) {
|
||||
menu.push({
|
||||
icon: 'ti ti-x',
|
||||
text: i18n.ts.remove,
|
||||
action: () => {
|
||||
items.value.splice(items.value.indexOf(item), 1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
||||
firstUploadAttempted.value = true;
|
||||
|
||||
for (const item of items.value.filter(item => item.uploaded == null)) {
|
||||
item.waiting = true;
|
||||
item.uploadFailed = false;
|
||||
|
||||
const shouldCompress = item.compressedImage == null && compressionLevel.value !== 0 && compressionSettings.value && COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !(await isAnimated(item.file));
|
||||
|
||||
if (shouldCompress) {
|
||||
const config = {
|
||||
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
|
||||
maxWidth: compressionSettings.value.maxWidth,
|
||||
maxHeight: compressionSettings.value.maxHeight,
|
||||
quality: isWebpSupported() ? 0.85 : 0.8,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await readAndCompressImage(item.file, config);
|
||||
if (result.size < item.file.size || item.file.type === 'image/webp') {
|
||||
// The compression may not always reduce the file size
|
||||
// (and WebP is not browser safe yet)
|
||||
item.compressedImage = markRaw(result);
|
||||
item.compressedSize = result.size;
|
||||
item.name = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to resize image', err);
|
||||
}
|
||||
}
|
||||
|
||||
item.uploading = true;
|
||||
|
||||
const driveFile = await uploadFile(item.compressedImage ?? item.file, {
|
||||
name: item.name,
|
||||
folderId: props.folderId,
|
||||
onProgress: (progress) => {
|
||||
item.waiting = false;
|
||||
if (item.progress == null) {
|
||||
item.progress = { max: progress.total, value: progress.loaded };
|
||||
} else {
|
||||
item.progress.value = progress.loaded;
|
||||
item.progress.max = progress.total;
|
||||
}
|
||||
},
|
||||
}).catch(err => {
|
||||
item.uploadFailed = true;
|
||||
item.progress = null;
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
item.uploading = false;
|
||||
item.waiting = false;
|
||||
});
|
||||
|
||||
item.uploaded = driveFile;
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseFile(ev: MouseEvent) {
|
||||
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
||||
|
||||
for (const file of newFiles) {
|
||||
initializeFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFile(file: File) {
|
||||
const id = uuid();
|
||||
const filename = file.name ?? 'untitled';
|
||||
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
||||
items.value.push({
|
||||
id,
|
||||
name: prefer.s.keepOriginalFilename ? filename : id + extension,
|
||||
progress: null,
|
||||
thumbnail: window.URL.createObjectURL(file),
|
||||
waiting: false,
|
||||
uploading: false,
|
||||
uploaded: null,
|
||||
uploadFailed: false,
|
||||
file: markRaw(file),
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
for (const file of props.files) {
|
||||
initializeFile(file);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overallProgress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--op);
|
||||
height: 4px;
|
||||
background: var(--MI_THEME-accent);
|
||||
border-radius: 0 999px 999px 0;
|
||||
transition: width 0.2s ease;
|
||||
|
||||
&.overallProgressError {
|
||||
background: var(--MI_THEME-warn);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.items {
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--p);
|
||||
height: 100%;
|
||||
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
||||
transition: width 0.2s ease, left 0.2s ease;
|
||||
}
|
||||
|
||||
&.itemWaiting {
|
||||
&::after {
|
||||
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
|
||||
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
||||
background-size: 25px 25px;
|
||||
animation: stripe .8s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
&.itemCompleted {
|
||||
&::before {
|
||||
left: 100%;
|
||||
width: var(--p);
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&.itemFailed {
|
||||
.itemBody {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes stripe {
|
||||
0% { background-position-x: 0; }
|
||||
100% { background-position-x: -25px; }
|
||||
}
|
||||
|
||||
.itemInner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.itemThumbnail {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: var(--MI_THEME-bg);
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.itemInfo {
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
font-size: 90%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.itemIcon {
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.itemInner {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
font-size: 90%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.itemActionWrapper {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.itemInfo {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.itemIconWrapper {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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,
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
originalOrCropped = await os.cropImage(file, {
|
||||
aspectRatio: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
avatarId: originalOrCropped.id,
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
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];
|
||||
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
avatarId: driveFile.id,
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -28,13 +28,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<path d="M96,63L63,96" style="--l:47;--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.animLine]"/>
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'waiting'" :class="[$style.icon, $style.waiting]" viewBox="0 0 160 160">
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircleWaiting]"/>
|
||||
<circle cx="80" cy="80" r="56" style="opacity: 0.25;" :class="[$style.line]"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'info' | 'question' | 'success' | 'warn' | 'error';
|
||||
type: 'info' | 'question' | 'success' | 'warn' | 'error' | 'waiting';
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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<T extends keyof DragDataMap>(
|
||||
event: DragEvent,
|
||||
type: T,
|
||||
data: DragDataMap[T],
|
||||
) {
|
||||
if (event.dataTransfer == null) return;
|
||||
|
||||
event.dataTransfer.setData(`misskey/${type}`.toLowerCase(), JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getDragData<T extends keyof DragDataMap>(
|
||||
event: DragEvent,
|
||||
type: T,
|
||||
): DragDataMap[T] | null {
|
||||
if (event.dataTransfer == null) return null;
|
||||
|
||||
const data = event.dataTransfer.getData(`misskey/${type}`.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());
|
||||
}
|
|
@ -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<Events>();
|
||||
|
|
|
@ -592,38 +592,6 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
|
|||
});
|
||||
}
|
||||
|
||||
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||
type: 'file',
|
||||
multiple,
|
||||
}, {
|
||||
done: files => {
|
||||
if (files) {
|
||||
resolve(files);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
|
||||
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<typeof MkRoleSelectDialog_TypeReferenceOnly>): Promise<
|
||||
{ canceled: true; result: undefined; } |
|
||||
{ canceled: false; result: Misskey.entities.Role[] }
|
||||
|
@ -655,15 +623,13 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof Mk
|
|||
});
|
||||
}
|
||||
|
||||
export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||
aspectRatio: number;
|
||||
uploadFolder?: string | null;
|
||||
}): Promise<Misskey.entities.DriveFile> {
|
||||
export async function cropImageFile(imageFile: File | Blob, options: {
|
||||
aspectRatio: number | null;
|
||||
}): Promise<File> {
|
||||
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<any> {
|
|||
});
|
||||
});
|
||||
}*/
|
||||
|
||||
export function chooseFileFromPc(
|
||||
options: {
|
||||
multiple?: boolean;
|
||||
} = {},
|
||||
): Promise<File[]> {
|
||||
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<Misskey.entities.DriveFile[]> {
|
||||
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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(() => []);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -65,8 +65,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-on="popup.events"
|
||||
/>
|
||||
|
||||
<XUpload v-if="uploads.length > 0"/>
|
||||
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
tag="div"
|
||||
|
@ -105,7 +103,6 @@ import { swInject } from './sw-inject.js';
|
|||
import XNotification from './notification.vue';
|
||||
import { popups } from '@/os.js';
|
||||
import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
|
||||
import { uploads } from '@/utility/upload.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
|
@ -116,7 +113,6 @@ import { store } from '@/store.js';
|
|||
import XNavbar from '@/ui/_common_/navbar.vue';
|
||||
|
||||
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||
const XWidgets = defineAsyncComponent(() => import('./widgets.vue'));
|
||||
|
||||
const drawerMenuShowing = defineModel<boolean>('drawerMenuShowing');
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="mk-uploader _acrylic" :style="{ zIndex }">
|
||||
<ol v-if="uploads.length > 0">
|
||||
<li v-for="ctx in uploads" :key="ctx.id">
|
||||
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
||||
<div class="top">
|
||||
<p class="name"><MkLoading :em="true"/>{{ ctx.name }}</p>
|
||||
<p class="status">
|
||||
<span v-if="ctx.progressValue === undefined" class="initing">{{ i18n.ts.waiting }}<MkEllipsis/></span>
|
||||
<span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
||||
<span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { uploads } from '@/utility/upload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const zIndex = os.claimZIndex('high');
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-uploader {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
width: 260px;
|
||||
top: 32px;
|
||||
padding: 16px 20px;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.mk-uploader:empty {
|
||||
display: none;
|
||||
}
|
||||
.mk-uploader > ol {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.mk-uploader > ol > li {
|
||||
display: grid;
|
||||
margin: 8px 0 0 0;
|
||||
padding: 0;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
border-top: solid 8px transparent;
|
||||
grid-template-columns: 36px calc(100% - 44px);
|
||||
grid-template-rows: 1fr 8px;
|
||||
column-gap: 8px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.mk-uploader > ol > li:first-child {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
border-top: none;
|
||||
}
|
||||
.mk-uploader > ol > li > .img {
|
||||
display: block;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
grid-column: 1/2;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
.mk-uploader > ol > li > .top {
|
||||
display: flex;
|
||||
grid-column: 2/3;
|
||||
grid-row: 1/2;
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .name {
|
||||
display: block;
|
||||
padding: 0 8px 0 0;
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .name > i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .status {
|
||||
display: block;
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0;
|
||||
font-size: 0.8em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .status > .initing {
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .status > .kb {
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .status > .percentage {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
.mk-uploader > ol > li > .top > .status > .percentage:after {
|
||||
content: '%';
|
||||
}
|
||||
.mk-uploader > ol > li > progress {
|
||||
display: block;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
grid-column: 2/3;
|
||||
grid-row: 2/3;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
}
|
||||
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
||||
background: var(--MI_THEME-accent);
|
||||
}
|
||||
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
||||
//background: var(--MI_THEME-accentAlpha01);
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Misskey.entities.DriveFile> {
|
||||
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<XMLHttpRequest>) => {
|
||||
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<EventTarget>) => 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<Misskey.entities.DriveFile[]> {
|
||||
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<Misskey.entities.DriveFile[]> {
|
||||
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<Misskey.entities.DriveFile> {
|
||||
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<Misskey.entities.DriveFile[]> {
|
||||
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<Misskey.entities.DriveFile> {
|
||||
return select(src, label, false).then(files => files[0]);
|
||||
}
|
||||
|
||||
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
return select(src, label, true);
|
||||
}
|
||||
|
||||
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {
|
||||
aspectRatio: number | null;
|
||||
}): Promise<Misskey.entities.DriveFile> {
|
||||
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<Misskey.entities.DriveFolder[]> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveFolderSelectDialog.vue')), {
|
||||
initialFolder,
|
||||
}, {
|
||||
done: folders => {
|
||||
if (folders) {
|
||||
resolve(folders);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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<Misskey.entities.DriveFile[]> {
|
||||
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<Misskey.entities.DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
os.selectDriveFile(multiple).then(files => {
|
||||
res(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
||||
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<Misskey.entities.DriveFile[]> {
|
||||
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<Misskey.entities.DriveFile> {
|
||||
return select(src, label, false).then(files => files[0]);
|
||||
}
|
||||
|
||||
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
return select(src, label, true);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Ref, ShallowRef } from 'vue';
|
||||
|
||||
export function getDateText(dateInstance: Date) {
|
||||
const date = dateInstance.getDate();
|
||||
|
@ -12,19 +12,6 @@ export function getDateText(dateInstance: Date) {
|
|||
return `${month.toString()}/${date.toString()}`;
|
||||
}
|
||||
|
||||
export type DateSeparetedTimelineItem<T> = {
|
||||
id: string;
|
||||
type: 'item';
|
||||
data: T;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'date';
|
||||
prev: Date;
|
||||
prevText: string;
|
||||
next: Date;
|
||||
nextText: string;
|
||||
};
|
||||
|
||||
// TODO: いちいちDateインスタンス作成するのは無駄感あるから文字列のまま解析したい
|
||||
export function isSeparatorNeeded(
|
||||
prev: string | null,
|
||||
|
@ -56,7 +43,20 @@ export function getSeparatorInfo(
|
|||
};
|
||||
}
|
||||
|
||||
export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]>) {
|
||||
export type DateSeparetedTimelineItem<T> = {
|
||||
id: string;
|
||||
type: 'item';
|
||||
data: T;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'date';
|
||||
prev: Date;
|
||||
prevText: string;
|
||||
next: Date;
|
||||
nextText: string;
|
||||
};
|
||||
|
||||
export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]> | ShallowRef<T[]>) {
|
||||
return computed<DateSeparetedTimelineItem<T>[]>(() => {
|
||||
const tl: DateSeparetedTimelineItem<T>[] = [];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
|
@ -92,3 +92,35 @@ export function makeDateSeparatedTimelineComputedRef<T extends { id: string; cre
|
|||
return tl;
|
||||
});
|
||||
}
|
||||
|
||||
export type DateGroupedTimelineItem<T> = {
|
||||
date: Date;
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export function makeDateGroupedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]> | ShallowRef<T[]>, span: 'day' | 'month' = 'day') {
|
||||
return computed<DateGroupedTimelineItem<T>[]>(() => {
|
||||
const tl: DateGroupedTimelineItem<T>[] = [];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
const date = new Date(item.createdAt);
|
||||
const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
|
||||
|
||||
if (tl.length === 0 || (
|
||||
span === 'day' && tl[tl.length - 1].date.getTime() !== date.getTime()
|
||||
) || (
|
||||
span === 'month' && (
|
||||
tl[tl.length - 1].date.getFullYear() !== date.getFullYear() ||
|
||||
tl[tl.length - 1].date.getMonth() !== date.getMonth()
|
||||
)
|
||||
)) {
|
||||
tl.push({
|
||||
date,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
tl[tl.length - 1].items.push(item);
|
||||
}
|
||||
return tl;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<Uploading[]>([]);
|
||||
|
||||
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<Misskey.entities.DriveFile> {
|
||||
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<void> => {
|
||||
const filename = name ?? file.name ?? 'untitled';
|
||||
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
||||
|
||||
const ctx = reactive<Uploading>({
|
||||
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<XMLHttpRequest>) => {
|
||||
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<EventTarget>) => any;
|
||||
|
||||
xhr.upload.onprogress = ev => {
|
||||
if (ev.lengthComputable) {
|
||||
ctx.progressMax = ev.total;
|
||||
ctx.progressValue = ev.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
|
@ -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<BrowserImageResizerConfigWithConvertedOutput | undefined> {
|
||||
const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
|
||||
if (!imgConfig || await isAnimated(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
debug: true,
|
||||
...imgConfig,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -148,9 +148,6 @@ export function getConfig(): UserConfig {
|
|||
_ENV_: JSON.stringify(process.env.NODE_ENV),
|
||||
_DEV_: process.env.NODE_ENV !== 'production',
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
_DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'),
|
||||
_DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'),
|
||||
_DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
|
|
|
@ -1247,6 +1247,9 @@ type DriveFilesFindRequest = operations['drive___files___find']['requestBody']['
|
|||
// @public (undocumented)
|
||||
type DriveFilesFindResponse = operations['drive___files___find']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type DriveFilesMoveBulkRequest = operations['drive___files___move-bulk']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1732,6 +1735,7 @@ declare namespace entities {
|
|||
DriveFilesFindResponse,
|
||||
DriveFilesFindByHashRequest,
|
||||
DriveFilesFindByHashResponse,
|
||||
DriveFilesMoveBulkRequest,
|
||||
DriveFilesShowRequest,
|
||||
DriveFilesShowResponse,
|
||||
DriveFilesUpdateRequest,
|
||||
|
|
|
@ -2073,6 +2073,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:drive*
|
||||
*/
|
||||
request<E extends 'drive/files/move-bulk', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* Show the properties of a drive file.
|
||||
*
|
||||
|
|
|
@ -282,6 +282,7 @@ import type {
|
|||
DriveFilesFindResponse,
|
||||
DriveFilesFindByHashRequest,
|
||||
DriveFilesFindByHashResponse,
|
||||
DriveFilesMoveBulkRequest,
|
||||
DriveFilesShowRequest,
|
||||
DriveFilesShowResponse,
|
||||
DriveFilesUpdateRequest,
|
||||
|
@ -823,6 +824,7 @@ export type Endpoints = {
|
|||
'drive/files/delete': { req: DriveFilesDeleteRequest; res: EmptyResponse };
|
||||
'drive/files/find': { req: DriveFilesFindRequest; res: DriveFilesFindResponse };
|
||||
'drive/files/find-by-hash': { req: DriveFilesFindByHashRequest; res: DriveFilesFindByHashResponse };
|
||||
'drive/files/move-bulk': { req: DriveFilesMoveBulkRequest; res: EmptyResponse };
|
||||
'drive/files/show': { req: DriveFilesShowRequest; res: DriveFilesShowResponse };
|
||||
'drive/files/update': { req: DriveFilesUpdateRequest; res: DriveFilesUpdateResponse };
|
||||
'drive/files/upload-from-url': { req: DriveFilesUploadFromUrlRequest; res: EmptyResponse };
|
||||
|
|
|
@ -285,6 +285,7 @@ export type DriveFilesFindRequest = operations['drive___files___find']['requestB
|
|||
export type DriveFilesFindResponse = operations['drive___files___find']['responses']['200']['content']['application/json'];
|
||||
export type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json'];
|
||||
export type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json'];
|
||||
export type DriveFilesMoveBulkRequest = operations['drive___files___move-bulk']['requestBody']['content']['application/json'];
|
||||
export type DriveFilesShowRequest = operations['drive___files___show']['requestBody']['content']['application/json'];
|
||||
export type DriveFilesShowResponse = operations['drive___files___show']['responses']['200']['content']['application/json'];
|
||||
export type DriveFilesUpdateRequest = operations['drive___files___update']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -1799,6 +1799,15 @@ export type paths = {
|
|||
*/
|
||||
post: operations['drive___files___find-by-hash'];
|
||||
};
|
||||
'/drive/files/move-bulk': {
|
||||
/**
|
||||
* drive/files/move-bulk
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:drive*
|
||||
*/
|
||||
post: operations['drive___files___move-bulk'];
|
||||
};
|
||||
'/drive/files/show': {
|
||||
/**
|
||||
* drive/files/show
|
||||
|
@ -16845,6 +16854,59 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* drive/files/move-bulk
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:drive*
|
||||
*/
|
||||
'drive___files___move-bulk': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
fileIds: string[];
|
||||
/** Format: misskey:id */
|
||||
folderId?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* drive/files/show
|
||||
* @description Show the properties of a drive file.
|
||||
|
|
Loading…
Reference in New Issue