This commit is contained in:
syuilo 2025-06-06 17:48:27 +09:00
parent 012213fc64
commit 171632365d
7 changed files with 118 additions and 58 deletions

4
locales/index.d.ts vendored
View File

@ -9584,6 +9584,10 @@ export interface Locale extends ILocale {
"disableFederationDescription": string; "disableFederationDescription": string;
}; };
"_postForm": { "_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/** /**
* ... * ...
*/ */

View File

@ -2522,6 +2522,7 @@ _visibility:
disableFederationDescription: "他サーバーへの配信を行いません" disableFederationDescription: "他サーバーへの配信を行いません"
_postForm: _postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
replyPlaceholder: "このノートに返信..." replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..." quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..." channelPlaceholder: "チャンネルに投稿..."

View File

@ -214,6 +214,11 @@ const uploader = useUploader({
features: props.features, features: props.features,
}); });
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
});
const draftKey = computed((): string => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; 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({ defineExpose({
clear, clear,
canClose,
}); });
</script> </script>

View File

@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal <MkModal
ref="modal" ref="modal"
:preferType="'dialog'" :preferType="'dialog'"
@click="modal?.close()" @click="_close()"
@closed="onModalClosed()" @closed="onModalClosed()"
@esc="modal?.close()" @esc="_close()"
> >
<MkPostForm <MkPostForm
ref="form" ref="form"
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
autofocus autofocus
freezeAfterPosted freezeAfterPosted
@posted="onPosted" @posted="onPosted"
@cancel="modal?.close()" @cancel="_close()"
@esc="modal?.close()" @esc="_close()"
/> />
</MkModal> </MkModal>
</template> </template>
@ -43,6 +43,7 @@ const emit = defineEmits<{
}>(); }>();
const modal = useTemplateRef('modal'); const modal = useTemplateRef('modal');
const form = useTemplateRef('form');
function onPosted() { function onPosted() {
modal.value?.close({ 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() { function onModalClosed() {
emit('closed'); emit('closed');
} }

View File

@ -93,7 +93,7 @@ onMounted(() => {
const items = uploader.items; const items = uploader.items;
const firstUploadAttempted = ref(false); 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 canDone = computed(() => items.value.some(item => item.uploaded != null));
const overallProgress = computed(() => { const overallProgress = computed(() => {
const max = items.value.length; const max = items.value.length;
@ -151,7 +151,7 @@ async function abortWithConfirm() {
} }
async function done() { async function done() {
if (items.value.some(item => item.uploaded == null)) { if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
text: i18n.ts._uploader.doneConfirm, text: i18n.ts._uploader.doneConfirm,

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.itemActionWrapper"> <div :class="$style.itemActionWrapper">
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton> <MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
</div> </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 :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div> <div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo"> <div :class="$style.itemInfo">
@ -60,6 +60,10 @@ function onContextmenu(item: UploaderItem, ev: MouseEvent) {
emit('showMenuViaContextmenu', item, ev); emit('showMenuViaContextmenu', item, ev);
} }
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
// TODO: preview when item is image
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -6,6 +6,7 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import isAnimated from 'is-file-animated'; import isAnimated from 'is-file-animated';
import { EventEmitter } from 'eventemitter3';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue'; import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
@ -97,6 +98,10 @@ export function useUploader(options: {
} = {}) { } = {}) {
const $i = ensureSignin(); const $i = ensureSignin();
const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>();
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => { const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return { return {
effect: options.features?.effect ?? true, effect: options.features?.effect ?? true,
@ -145,6 +150,11 @@ export function useUploader(options: {
function getMenu(item: UploaderItem): MenuItem[] { function getMenu(item: UploaderItem): MenuItem[] {
const menu: MenuItem[] = []; const menu: MenuItem[] = [];
if (
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({ menu.push({
icon: 'ti ti-cursor-text', icon: 'ti ti-cursor-text',
text: i18n.ts.rename, text: i18n.ts.rename,
@ -161,6 +171,7 @@ export function useUploader(options: {
item.name = result; item.name = result;
}, },
}); });
}
if ( if (
uploaderFeatures.value.crop && uploaderFeatures.value.crop &&
@ -332,6 +343,12 @@ export function useUploader(options: {
if (!item.preprocessing && !item.uploading && !item.uploaded) { if (!item.preprocessing && !item.uploading && !item.uploaded) {
menu.push({ menu.push({
type: 'divider', type: 'divider',
}, {
icon: 'ti ti-upload',
text: i18n.ts.upload,
action: () => {
uploadOne(item);
},
}, { }, {
icon: 'ti ti-x', icon: 'ti ti-x',
text: i18n.ts.remove, text: i18n.ts.remove,
@ -357,20 +374,7 @@ export function useUploader(options: {
return menu; return menu;
} }
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる async function uploadOne(item: UploaderItem): Promise<void> {
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// アップロード処理途中で値が変わる場合途中で全キャンセルされたりなどもあるので、Array filterではなくここでチェック
if (item.aborted) {
continue;
}
item.uploadFailed = false; item.uploadFailed = false;
item.uploading = true; item.uploading = true;
@ -397,6 +401,7 @@ export function useUploader(options: {
await filePromise.then((file) => { await filePromise.then((file) => {
item.uploaded = file; item.uploaded = file;
item.abort = null; item.abort = null;
events.emit('itemUploaded', { item });
}).catch(err => { }).catch(err => {
item.uploadFailed = true; item.uploadFailed = true;
item.progress = null; item.progress = null;
@ -407,6 +412,23 @@ export function useUploader(options: {
item.uploading = false; item.uploading = false;
}); });
} }
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// アップロード処理途中で値が変わる場合途中で全キャンセルされたりなどもあるので、Array filterではなくここでチェック
if (item.aborted) {
continue;
}
await uploadOne(item);
}
} }
function abortAll() { function abortAll() {
@ -505,7 +527,9 @@ export function useUploader(options: {
upload, upload,
getMenu, getMenu,
uploading: computed(() => items.value.some(item => item.uploading)), 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,
}; };
} }