enhance: refine uploadFile

This commit is contained in:
kakkokari-gtyih 2025-05-24 19:48:24 +09:00
parent 3ff2e6b299
commit d2371a9943
18 changed files with 146 additions and 42 deletions

View File

@ -51,7 +51,10 @@ if (props.fileId) {
}
function selectButton(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(async (file) => {
if (!file) return;
if (props.validate && !await props.validate(file)) return;

View File

@ -120,7 +120,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 { selectFiles } from '@/utility/drive.js';
import { selectFile } from '@/utility/drive.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
@ -437,7 +437,11 @@ function focus() {
function chooseFileFrom(ev) {
if (props.mock) return;
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: true,
label: i18n.ts.attachFile,
}).then(files_ => {
for (const file of files_) {
files.value.push(file);
}

View File

@ -79,8 +79,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModalWindow>
</template>
<script lang="ts">
export type UploaderDialogFeatures = {
effect?: boolean;
watermark?: boolean;
crop?: boolean;
};
</script>
<script lang="ts" setup>
import { computed, defineAsyncComponent, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
@ -91,7 +99,6 @@ import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
import MkSelect from '@/components/MkSelect.vue';
import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js';
@ -131,10 +138,19 @@ const props = withDefaults(defineProps<{
files: File[];
folderId?: string | null;
multiple?: boolean;
features?: UploaderDialogFeatures;
}>(), {
multiple: true,
});
const uploaderFeatures = computed<Required<UploaderDialogFeatures>>(() => {
return {
effect: props.features?.effect ?? true,
watermark: props.features?.watermark ?? true,
crop: props.features?.crop ?? true,
};
});
const emit = defineEmits<{
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
(ev: 'canceled'): void;
@ -152,7 +168,7 @@ const items = ref<{
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
aborted: boolean;
compressionLevel: number;
compressionLevel: 0 | 1 | 2 | 3;
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
@ -272,7 +288,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
},
});
if (CROPPING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
if (
uploaderFeatures.value.crop &&
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-crop',
text: i18n.ts.cropImage,
@ -292,7 +314,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
});
}
if (IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
if (
uploaderFeatures.value.effect &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-sparkles',
text: i18n.ts._imageEffector.title + ' (BETA)',
@ -318,7 +346,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
});
}
if (WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
if (
uploaderFeatures.value.watermark &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeWatermarkPreset(presetId: string | null) {
item.watermarkPresetId = presetId;
preprocess(item).then(() => {
@ -338,7 +372,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
}, {
type: 'divider',
}, ...prefer.s.watermarkPresets.map(preset => ({
type: 'radioOption',
type: 'radioOption' as const,
text: preset.name,
active: computed(() => item.watermarkPresetId === preset.id),
action: () => changeWatermarkPreset(preset.id),

View File

@ -262,10 +262,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import { ref, onMounted } from 'vue';
import type { WatermarkPreset } from '@/utility/watermark.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -275,7 +274,6 @@ import MkPositionSelector from '@/components/MkPositionSelector.vue';
import * as os from '@/os.js';
import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
@ -294,7 +292,14 @@ onMounted(async () => {
});
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then((file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts.selectFile,
features: {
watermark: false,
},
}).then((file) => {
if (!file.type.startsWith('image')) {
os.alert({
type: 'warning',

View File

@ -124,7 +124,7 @@ function createStripeLayer(): WatermarkPreset['layers'][number] {
angle: 0.5,
frequency: 10,
threshold: 0.1,
black: false,
color: [0, 0, 0],
opacity: 0.75,
};
}
@ -140,7 +140,7 @@ function createPolkadotLayer(): WatermarkPreset['layers'][number] {
majorOpacity: 0.75,
minorOpacity: 0.5,
minorDivisions: 4,
black: false,
color: [0, 0, 0],
opacity: 0.75,
};
}
@ -151,7 +151,7 @@ function createCheckerLayer(): WatermarkPreset['layers'][number] {
type: 'checker',
angle: 0.5,
scale: 3,
black: false,
color: [0, 0, 0],
opacity: 0.75,
};
}
@ -177,6 +177,7 @@ const dialog = useTemplateRef('dialog');
async function cancel() {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
});
if (canceled) return;

View File

@ -13,6 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -836,6 +837,7 @@ export function launchUploader(
options?: {
folderId?: string | null;
multiple?: boolean;
features?: UploaderDialogFeatures;
},
): Promise<Misskey.entities.DriveFile[]> {
return new Promise(async (res, rej) => {
@ -844,6 +846,7 @@ export function launchUploader(
files: markRaw(files),
folderId: options?.folderId,
multiple: options?.multiple,
features: options?.features,
}, {
done: driveFiles => {
if (driveFiles.length === 0) return rej();

View File

@ -174,7 +174,10 @@ function setupGrid(): GridSetting {
{
bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
async customValueEditor(row, col, value, cellElement) {
const file = await selectFile(cellElement);
const file = await selectFile({
anchorElement: cellElement,
multiple: false,
});
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;

View File

@ -188,7 +188,10 @@ async function archive() {
}
function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: false,
}).then(file => {
bannerId.value = file.id;
});
}

View File

@ -168,7 +168,11 @@ function onKeydown(ev: KeyboardEvent) {
}
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts.selectFile,
}).then(selectedFile => {
file.value = selectedFile;
});
}

View File

@ -214,7 +214,10 @@ const menu = (ev: MouseEvent) => {
icon: 'ti ti-upload',
text: i18n.ts.import,
action: async () => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('admin/emoji/import-zip', {
fileId: file.id,
})

View File

@ -121,7 +121,10 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
file.value = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
name.value = candidate;

View File

@ -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/drive.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';
@ -63,8 +63,11 @@ const description = ref<string | null>(null);
const title = ref<string | null>(null);
const isSensitive = ref(false);
function selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: false,
}).then(selected => {
files.value = files.value.concat(selected);
});
}

View File

@ -205,7 +205,10 @@ async function add() {
}
function setEyeCatchingImage(img: Event) {
selectFile(img.currentTarget ?? img.target, null).then(file => {
selectFile({
anchorElement: img.currentTarget ?? img.target,
multiple: false,
}).then(file => {
eyeCatchingImageId.value = file.id;
});
}

View File

@ -233,7 +233,10 @@ const exportAntennas = () => {
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-following', {
fileId: file.id,
withReplies: withReplies.value,
@ -241,22 +244,34 @@ const importFollowing = async (ev) => {
};
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importAntennas = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
};

View File

@ -114,7 +114,10 @@ watch(wallpaper, async () => {
});
function setWallpaper(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, null).then(file => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(file => {
wallpaper.value = file.url;
});
}

View File

@ -94,7 +94,11 @@ const friendlyFileName = computed<string>(() => {
});
function selectSound(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts._soundSettings.driveFile,
}).then(async (file) => {
if (!file.type.startsWith('audio')) {
os.alert({
type: 'warning',

View File

@ -422,7 +422,7 @@ export const PREF_DEF = definePreferences({
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2,
default: 2 as 0 | 1 | 2 | 3,
},
'sound.masterVolume': {

View File

@ -16,6 +16,7 @@ import { instance } from '@/instance.js';
import { globalEvents } from '@/events.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { genId } from '@/utility/id.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
type UploadReturnType = {
filePromise: Promise<Misskey.entities.DriveFile>;
@ -155,6 +156,7 @@ export function uploadFile(file: File | Blob, options: {
export function chooseFileFromPcAndUpload(
options: {
multiple?: boolean;
features?: UploaderDialogFeatures;
folderId?: string | null;
} = {},
): Promise<Misskey.entities.DriveFile[]> {
@ -163,6 +165,7 @@ export function chooseFileFromPcAndUpload(
if (files.length === 0) return;
os.launchUploader(files, {
folderId: options.folderId,
features: options.features,
}).then(driveFiles => {
res(driveFiles);
});
@ -194,7 +197,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
type: 'url',
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled) return;
if (canceled || url == null) return;
const marker = genId();
@ -221,7 +224,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean, features?: UploaderDialogFeatures): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.popupMenu([label ? {
text: label,
@ -229,7 +232,7 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
} : undefined, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
action: () => chooseFileFromPcAndUpload({ multiple, features }).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
@ -242,12 +245,19 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
});
}
export function selectFile(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(anchorElement, label, false).then(files => files[0]);
}
type SelectFileOptions<M extends boolean> = {
anchorElement: HTMLElement | EventTarget | null;
multiple: M;
label?: string | null;
features?: UploaderDialogFeatures;
};
export function selectFiles(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(anchorElement, label, true);
export async function selectFile<
M extends boolean,
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile
>(opts: SelectFileOptions<M>): Promise<MR> {
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
return opts.multiple ? (files as MR) : (files[0]! as MR);
}
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {