Compare commits
11 Commits
655bb5c2bd
...
542ecffadb
| Author | SHA1 | Date |
|---|---|---|
|
|
542ecffadb | |
|
|
490e0a7382 | |
|
|
be35fe468b | |
|
|
4b9b3ced01 | |
|
|
4c959152de | |
|
|
ef51d7b9a1 | |
|
|
171632365d | |
|
|
012213fc64 | |
|
|
5db01c1c06 | |
|
|
c8d2b36ec3 | |
|
|
3098c24d28 |
|
|
@ -9584,6 +9584,14 @@ export interface Locale extends ILocale {
|
|||
"disableFederationDescription": string;
|
||||
};
|
||||
"_postForm": {
|
||||
/**
|
||||
* アップロードされていないファイルがありますが、破棄してフォームを閉じますか?
|
||||
*/
|
||||
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
|
||||
/**
|
||||
* ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。
|
||||
*/
|
||||
"uploaderTip": string;
|
||||
/**
|
||||
* このノートに返信...
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2522,6 +2522,8 @@ _visibility:
|
|||
disableFederationDescription: "他サーバーへの配信を行いません"
|
||||
|
||||
_postForm:
|
||||
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
|
||||
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
|
||||
replyPlaceholder: "このノートに返信..."
|
||||
quotePlaceholder: "このノートを引用..."
|
||||
channelPlaceholder: "チャンネルに投稿..."
|
||||
|
|
|
|||
|
|
@ -73,7 +73,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<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"/>
|
||||
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
|
||||
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => uploader.showMenu(ev, item)"/>
|
||||
<MkTip k="postFormUploader">
|
||||
{{ i18n.ts._postForm.uploaderTip }}
|
||||
</MkTip>
|
||||
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
|
||||
</div>
|
||||
<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"/>
|
||||
|
|
@ -81,19 +84,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<footer :class="$style.footer">
|
||||
<div :class="$style.footerLeft">
|
||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||
</div>
|
||||
<div :class="$style.footerRight">
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
|
||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||
</div>
|
||||
</footer>
|
||||
<datalist id="hashtags">
|
||||
|
|
@ -114,6 +115,7 @@ import type { ShallowRef } from 'vue';
|
|||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||
import type { UploaderItem } from '@/composables/use-uploader.js';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
|
||||
|
|
@ -125,7 +127,7 @@ 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 { selectFile } from '@/utility/drive.js';
|
||||
import { chooseDriveFile } from '@/utility/drive.js';
|
||||
import { store } from '@/store.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -208,9 +210,12 @@ const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(
|
|||
const postFormActions = getPluginHandlers('post_form_action');
|
||||
|
||||
const uploader = useUploader({
|
||||
multiple: props.multiple,
|
||||
folderId: props.folderId,
|
||||
features: props.features,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
uploader.events.on('itemUploaded', ctx => {
|
||||
files.value.push(ctx.item.uploaded!);
|
||||
uploader.removeItem(ctx.item);
|
||||
});
|
||||
|
||||
const draftKey = computed((): string => {
|
||||
|
|
@ -270,7 +275,7 @@ const cwTextLength = computed((): number => {
|
|||
const maxCwTextLength = 100;
|
||||
|
||||
const canPost = computed((): boolean => {
|
||||
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value &&
|
||||
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && uploader.readyForUpload.value &&
|
||||
(
|
||||
1 <= textLength.value ||
|
||||
1 <= files.value.length ||
|
||||
|
|
@ -456,6 +461,14 @@ function chooseFileFromPc(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
function chooseFileFromDrive(ev: MouseEvent) {
|
||||
if (props.mock) return;
|
||||
|
||||
chooseDriveFile({ multiple: true }).then(driveFiles => {
|
||||
files.value.push(...driveFiles);
|
||||
});
|
||||
}
|
||||
|
||||
function detachFile(id) {
|
||||
files.value = files.value.filter(x => x.id !== id);
|
||||
}
|
||||
|
|
@ -579,6 +592,10 @@ function showOtherSettings() {
|
|||
toggleReactionAcceptance();
|
||||
},
|
||||
}, { type: 'divider' }, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.preview,
|
||||
ref: showPreview,
|
||||
}, {
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.reset,
|
||||
danger: true,
|
||||
|
|
@ -1064,6 +1081,16 @@ function openAccountMenu(ev: MouseEvent) {
|
|||
}, ev);
|
||||
}
|
||||
|
||||
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
|
||||
const menu = uploader.getMenu(item);
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||
const menu = uploader.getMenu(item);
|
||||
os.contextMenu(menu, ev);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus) {
|
||||
focus();
|
||||
|
|
@ -1132,8 +1159,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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts._uploader.tip }}
|
||||
</MkTip>
|
||||
|
||||
<MkUploaderItems :items="items" @showMenu="(item, ev) => uploader.showMenu(ev, item)"/>
|
||||
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
|
||||
|
||||
<div v-if="props.multiple">
|
||||
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
||||
|
|
@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template #footer>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
||||
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
||||
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
||||
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @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>
|
||||
|
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||
import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
@ -93,8 +93,7 @@ onMounted(() => {
|
|||
const items = uploader.items;
|
||||
|
||||
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.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;
|
||||
|
|
@ -152,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,
|
||||
|
|
@ -170,6 +169,16 @@ async function chooseFile(ev: MouseEvent) {
|
|||
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
||||
uploader.addFiles(newFiles);
|
||||
}
|
||||
|
||||
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
|
||||
const menu = uploader.getMenu(item);
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||
const menu = uploader.getMenu(item);
|
||||
os.contextMenu(menu, ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-panel
|
||||
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
|
||||
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
|
||||
@contextmenu.prevent.stop="onContextmenu(item, $event)"
|
||||
>
|
||||
<div :class="$style.itemInner">
|
||||
<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">
|
||||
|
|
@ -38,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import type { UploaderItem } from '@/composables/use-uploader.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
@ -49,7 +51,19 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'showMenu', item: UploaderItem, event: MouseEvent): void;
|
||||
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
function onContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
|
||||
emit('showMenuViaContextmenu', item, ev);
|
||||
}
|
||||
|
||||
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
|
||||
// TODO: preview when item is image
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
|
||||
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
|
||||
<div><slot></slot></div>
|
||||
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
<div>
|
||||
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -15,19 +18,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { store } from '@/store.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
k: keyof (typeof store['s']['tips']);
|
||||
k: typeof TIPS[number];
|
||||
warn?: boolean;
|
||||
}>(), {
|
||||
warn: false,
|
||||
});
|
||||
|
||||
function closeTip() {
|
||||
store.set('tips', {
|
||||
...store.r.tips.value,
|
||||
[props.k]: true,
|
||||
});
|
||||
function _closeTip() {
|
||||
closeTip(props.k);
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
icon: 'ti ti-bulb-off',
|
||||
text: i18n.ts.hideAllTips,
|
||||
danger: true,
|
||||
action: () => {
|
||||
hideAllTips();
|
||||
os.success();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -142,25 +147,31 @@ export function useUploader(options: {
|
|||
items.value.splice(items.value.indexOf(item), 1);
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent, item: UploaderItem) {
|
||||
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,
|
||||
|
|
@ -354,7 +371,46 @@ export function useUploader(options: {
|
|||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
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() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,8 +525,11 @@ export function useUploader(options: {
|
|||
removeItem,
|
||||
abortAll,
|
||||
upload,
|
||||
showMenu,
|
||||
getMenu,
|
||||
uploading: computed(() => items.value.some(item => item.uploading)),
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ import { prefer } from '@/preferences.js';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { signout } from '@/signout.js';
|
||||
import { migrateOldSettings } from '@/pref-migrate.js';
|
||||
import { store, TIPS } from '@/store.js';
|
||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
|
@ -205,16 +205,12 @@ function migrate() {
|
|||
}
|
||||
|
||||
function resetAllTips() {
|
||||
store.set('tips', {});
|
||||
_resetAllTips();
|
||||
os.success();
|
||||
}
|
||||
|
||||
function hideAllTips() {
|
||||
const v = {};
|
||||
for (const k of TIPS) {
|
||||
v[k] = true;
|
||||
}
|
||||
store.set('tips', v);
|
||||
_hideAllTips();
|
||||
os.success();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,22 +10,11 @@ import darkTheme from '@@/themes/d-green-lime.json5';
|
|||
import { hemisphere } from '@@/js/intl-const.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
import type { TIPS } from '@/tips.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { Pizzax } from '@/lib/pizzax.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
|
||||
export const TIPS = [
|
||||
'drive',
|
||||
'uploader',
|
||||
'clips',
|
||||
'userLists',
|
||||
'tl.home',
|
||||
'tl.local',
|
||||
'tl.social',
|
||||
'tl.global',
|
||||
'abuses',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 「状態」を管理するストア(not「設定」)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { store } from '@/store.js';
|
||||
|
||||
export const TIPS = [
|
||||
'drive',
|
||||
'uploader',
|
||||
'postFormUploader',
|
||||
'clips',
|
||||
'userLists',
|
||||
'tl.home',
|
||||
'tl.local',
|
||||
'tl.social',
|
||||
'tl.global',
|
||||
'abuses',
|
||||
] as const;
|
||||
|
||||
export function closeTip(tip: typeof TIPS[number]) {
|
||||
store.set('tips', {
|
||||
...store.r.tips.value,
|
||||
[tip]: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetAllTips() {
|
||||
store.set('tips', {});
|
||||
}
|
||||
|
||||
export function hideAllTips() {
|
||||
const v = {};
|
||||
for (const k of TIPS) {
|
||||
v[k] = true;
|
||||
}
|
||||
store.set('tips', v);
|
||||
}
|
||||
Loading…
Reference in New Issue