wip
This commit is contained in:
parent
012213fc64
commit
171632365d
|
@ -9584,6 +9584,10 @@ export interface Locale extends ILocale {
|
|||
"disableFederationDescription": string;
|
||||
};
|
||||
"_postForm": {
|
||||
/**
|
||||
* アップロードされていないファイルがありますが、破棄してフォームを閉じますか?
|
||||
*/
|
||||
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
|
||||
/**
|
||||
* このノートに返信...
|
||||
*/
|
||||
|
|
|
@ -2522,6 +2522,7 @@ _visibility:
|
|||
disableFederationDescription: "他サーバーへの配信を行いません"
|
||||
|
||||
_postForm:
|
||||
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
|
||||
replyPlaceholder: "このノートに返信..."
|
||||
quotePlaceholder: "このノートを引用..."
|
||||
channelPlaceholder: "チャンネルに投稿..."
|
||||
|
|
|
@ -214,6 +214,11 @@ const uploader = useUploader({
|
|||
features: props.features,
|
||||
});
|
||||
|
||||
uploader.events.on('itemUploaded', ctx => {
|
||||
files.value.push(ctx.item.uploaded!);
|
||||
uploader.removeItem(ctx.item);
|
||||
});
|
||||
|
||||
const draftKey = computed((): string => {
|
||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
||||
|
||||
|
@ -1151,8 +1156,23 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
async function canClose() {
|
||||
if (!uploader.allItemsUploaded.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm,
|
||||
okText: i18n.ts.yes,
|
||||
cancelText: i18n.ts.no,
|
||||
});
|
||||
if (canceled) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
clear,
|
||||
canClose,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkModal
|
||||
ref="modal"
|
||||
:preferType="'dialog'"
|
||||
@click="modal?.close()"
|
||||
@click="_close()"
|
||||
@closed="onModalClosed()"
|
||||
@esc="modal?.close()"
|
||||
@esc="_close()"
|
||||
>
|
||||
<MkPostForm
|
||||
ref="form"
|
||||
|
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
autofocus
|
||||
freezeAfterPosted
|
||||
@posted="onPosted"
|
||||
@cancel="modal?.close()"
|
||||
@esc="modal?.close()"
|
||||
@cancel="_close()"
|
||||
@esc="_close()"
|
||||
/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
@ -43,6 +43,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const modal = useTemplateRef('modal');
|
||||
const form = useTemplateRef('form');
|
||||
|
||||
function onPosted() {
|
||||
modal.value?.close({
|
||||
|
@ -50,6 +51,12 @@ function onPosted() {
|
|||
});
|
||||
}
|
||||
|
||||
async function _close() {
|
||||
const canClose = await form.value?.canClose();
|
||||
if (!canClose) return;
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
function onModalClosed() {
|
||||
emit('closed');
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ onMounted(() => {
|
|||
const items = uploader.items;
|
||||
|
||||
const firstUploadAttempted = ref(false);
|
||||
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.preprocessing) && items.value.some(item => item.uploaded == null));
|
||||
const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
|
||||
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
||||
const overallProgress = computed(() => {
|
||||
const max = items.value.length;
|
||||
|
@ -151,7 +151,7 @@ async function abortWithConfirm() {
|
|||
}
|
||||
|
||||
async function done() {
|
||||
if (items.value.some(item => item.uploaded == null)) {
|
||||
if (!uploader.allItemsUploaded.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts._uploader.doneConfirm,
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.itemActionWrapper">
|
||||
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }"></div>
|
||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
|
||||
<div :class="$style.itemBody">
|
||||
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
|
||||
<div :class="$style.itemInfo">
|
||||
|
@ -60,6 +60,10 @@ function onContextmenu(item: UploaderItem, ev: MouseEvent) {
|
|||
|
||||
emit('showMenuViaContextmenu', item, ev);
|
||||
}
|
||||
|
||||
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
|
||||
// TODO: preview when item is image
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||
import isAnimated from 'is-file-animated';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
@ -97,6 +98,10 @@ export function useUploader(options: {
|
|||
} = {}) {
|
||||
const $i = ensureSignin();
|
||||
|
||||
const events = new EventEmitter<{
|
||||
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
|
||||
}>();
|
||||
|
||||
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
|
||||
return {
|
||||
effect: options.features?.effect ?? true,
|
||||
|
@ -145,22 +150,28 @@ export function useUploader(options: {
|
|||
function getMenu(item: UploaderItem): MenuItem[] {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
icon: 'ti ti-cursor-text',
|
||||
text: i18n.ts.rename,
|
||||
action: async () => {
|
||||
const { result, canceled } = await os.inputText({
|
||||
type: 'text',
|
||||
title: i18n.ts.rename,
|
||||
placeholder: item.name,
|
||||
default: item.name,
|
||||
});
|
||||
if (canceled) return;
|
||||
if (result.trim() === '') return;
|
||||
if (
|
||||
!item.preprocessing &&
|
||||
!item.uploading &&
|
||||
!item.uploaded
|
||||
) {
|
||||
menu.push({
|
||||
icon: 'ti ti-cursor-text',
|
||||
text: i18n.ts.rename,
|
||||
action: async () => {
|
||||
const { result, canceled } = await os.inputText({
|
||||
type: 'text',
|
||||
title: i18n.ts.rename,
|
||||
placeholder: item.name,
|
||||
default: item.name,
|
||||
});
|
||||
if (canceled) return;
|
||||
if (result.trim() === '') return;
|
||||
|
||||
item.name = result;
|
||||
},
|
||||
});
|
||||
item.name = result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
uploaderFeatures.value.crop &&
|
||||
|
@ -332,6 +343,12 @@ export function useUploader(options: {
|
|||
if (!item.preprocessing && !item.uploading && !item.uploaded) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
icon: 'ti ti-upload',
|
||||
text: i18n.ts.upload,
|
||||
action: () => {
|
||||
uploadOne(item);
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-x',
|
||||
text: i18n.ts.remove,
|
||||
|
@ -357,6 +374,45 @@ export function useUploader(options: {
|
|||
return menu;
|
||||
}
|
||||
|
||||
async function uploadOne(item: UploaderItem): Promise<void> {
|
||||
item.uploadFailed = false;
|
||||
item.uploading = true;
|
||||
|
||||
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
|
||||
name: item.uploadName ?? item.name,
|
||||
folderId: options.folderId,
|
||||
onProgress: (progress) => {
|
||||
if (item.progress == null) {
|
||||
item.progress = { max: progress.total, value: progress.loaded };
|
||||
} else {
|
||||
item.progress.value = progress.loaded;
|
||||
item.progress.max = progress.total;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
item.abort = () => {
|
||||
item.abort = null;
|
||||
abort();
|
||||
item.uploading = false;
|
||||
item.uploadFailed = true;
|
||||
};
|
||||
|
||||
await filePromise.then((file) => {
|
||||
item.uploaded = file;
|
||||
item.abort = null;
|
||||
events.emit('itemUploaded', { item });
|
||||
}).catch(err => {
|
||||
item.uploadFailed = true;
|
||||
item.progress = null;
|
||||
if (!(err instanceof UploadAbortedError)) {
|
||||
throw err;
|
||||
}
|
||||
}).finally(() => {
|
||||
item.uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
||||
items.value = items.value.map(item => ({
|
||||
...item,
|
||||
|
@ -371,41 +427,7 @@ export function useUploader(options: {
|
|||
continue;
|
||||
}
|
||||
|
||||
item.uploadFailed = false;
|
||||
item.uploading = true;
|
||||
|
||||
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
|
||||
name: item.uploadName ?? item.name,
|
||||
folderId: options.folderId,
|
||||
onProgress: (progress) => {
|
||||
if (item.progress == null) {
|
||||
item.progress = { max: progress.total, value: progress.loaded };
|
||||
} else {
|
||||
item.progress.value = progress.loaded;
|
||||
item.progress.max = progress.total;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
item.abort = () => {
|
||||
item.abort = null;
|
||||
abort();
|
||||
item.uploading = false;
|
||||
item.uploadFailed = true;
|
||||
};
|
||||
|
||||
await filePromise.then((file) => {
|
||||
item.uploaded = file;
|
||||
item.abort = null;
|
||||
}).catch(err => {
|
||||
item.uploadFailed = true;
|
||||
item.progress = null;
|
||||
if (!(err instanceof UploadAbortedError)) {
|
||||
throw err;
|
||||
}
|
||||
}).finally(() => {
|
||||
item.uploading = false;
|
||||
});
|
||||
await uploadOne(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,7 +527,9 @@ export function useUploader(options: {
|
|||
upload,
|
||||
getMenu,
|
||||
uploading: computed(() => items.value.some(item => item.uploading)),
|
||||
readyForUpload: computed(() => items.value.length > 0 && !items.value.some(item => item.uploading || item.preprocessing)),
|
||||
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
|
||||
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue