diff options
Diffstat (limited to 'packages/frontend')
21 files changed, 186 insertions, 69 deletions
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index a11075c342..182ff3ccf5 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -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; diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 997dd4d528..42502ba449 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -77,6 +77,7 @@ const dialog = useTemplateRef('dialog'); async function cancel() { if (layers.length > 0) { const { canceled } = await os.confirm({ + type: 'warning', text: i18n.ts._imageEffector.discardChangesConfirm, }); if (canceled) return; @@ -132,7 +133,7 @@ function onLayerDelete(layer: ImageEffectorLayer) { const canvasEl = useTemplateRef('canvasEl'); -let renderer: ImageEffector | null = null; +let renderer: ImageEffector<typeof FXS> | null = null; let imageBitmap: ImageBitmap | null = null; onMounted(async () => { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 982ed88003..cd4fabea02 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -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); } diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index b2e4896ed3..4c4bc26bfd 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -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,17 +138,26 @@ 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; (ev: 'closed'): void; }>(); -const items = ref<{ +type UploaderItem = { id: string; name: string; uploadName?: string; @@ -152,13 +168,15 @@ 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; watermarkPresetId: string | null; abort?: (() => void) | null; -}[]>([]); +}; + +const items = ref<UploaderItem[]>([]); const dialog = useTemplateRef('dialog'); @@ -252,7 +270,7 @@ async function done() { dialog.value?.close(); } -function showMenu(ev: MouseEvent, item: typeof items.value[0]) { +function showMenu(ev: MouseEvent, item: UploaderItem) { const menu: MenuItem[] = []; menu.push({ @@ -272,7 +290,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 +316,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 +348,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,13 +374,13 @@ 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), - })), { - type: 'divider', - }, { + })), ...(prefer.s.watermarkPresets.length > 0 ? [{ + type: 'divider' as const, + }] : []), { type: 'button', icon: 'ti ti-plus', text: i18n.ts.add, @@ -397,8 +433,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) { text: i18n.ts.high, active: computed(() => item.compressionLevel === 3), action: () => changeCompressionLevel(3), - }, - ], + }], }); } @@ -590,9 +625,9 @@ function initializeFile(file: File) { uploaded: null, uploadFailed: false, compressionLevel: prefer.s.defaultImageCompressionLevel, - watermarkPresetId: prefer.s.defaultWatermarkPresetId, + watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null, file: markRaw(file), - }; + } satisfies UploaderItem; items.value.push(item); preprocess(item).then(() => { triggerRef(items); diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue index 10de04c16a..11ae091d90 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue @@ -262,10 +262,10 @@ 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 * as Misskey from 'misskey-js'; 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,11 +275,10 @@ 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 }); -const driveFile = ref(); +const driveFile = ref<Misskey.entities.DriveFile | null>(null); const driveFileError = ref(false); onMounted(async () => { if (layer.value.type === 'image' && layer.value.imageId != null) { @@ -294,7 +293,15 @@ 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 (layer.value.type !== 'image') return; if (!file.type.startsWith('image')) { os.alert({ type: 'warning', diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue index 4cfb4a72bc..206298b194 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -124,7 +124,7 @@ function createStripeLayer(): WatermarkPreset['layers'][number] { angle: 0.5, frequency: 10, threshold: 0.1, - black: false, + color: [1, 1, 1], opacity: 0.75, }; } @@ -140,7 +140,7 @@ function createPolkadotLayer(): WatermarkPreset['layers'][number] { majorOpacity: 0.75, minorOpacity: 0.5, minorDivisions: 4, - black: false, + color: [1, 1, 1], opacity: 0.75, }; } @@ -151,7 +151,7 @@ function createCheckerLayer(): WatermarkPreset['layers'][number] { type: 'checker', angle: 0.5, scale: 3, - black: false, + color: [1, 1, 1], 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; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index be247f96c4..83ad0ebdf9 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -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(); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index 4c2c26ec45..a380bd133e 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -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; diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 355b5464a1..72281ea882 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -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; }); } diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 7e3be67230..17b68d6eb9 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -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; }); } diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 5aba0f68a3..36d638b210 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -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, }) diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 41de457427..b4fc4a46d9 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -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; diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 1b8c14a156..9c0078e15a 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="name">{{ file.name }}</div> <button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button> </div> - <MkButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton> + <MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton> </div> <MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch> @@ -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: true, + }).then(selected => { files.value = files.value.concat(selected); }); } diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 05eb0bd9d2..8a9b9a9b08 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -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; }); } diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue index d175c0dc32..5a00d7a9d7 100644 --- a/packages/frontend/src/pages/settings/account-data.vue +++ b/packages/frontend/src/pages/settings/account-data.vue @@ -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); }; diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index 22bd8cbc80..ae882d1ee2 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -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; }); } diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index ffbbefa122..7aad43b1d0 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -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', diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 727b79e045..a83a3153d0 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -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': { diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts index 0e10f80145..bc1813f48c 100644 --- a/packages/frontend/src/utility/drive.ts +++ b/packages/frontend/src/utility/drive.ts @@ -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: { diff --git a/packages/frontend/src/utility/image-effector/ImageEffector.ts b/packages/frontend/src/utility/image-effector/ImageEffector.ts index fe253017e5..80e3ff65de 100644 --- a/packages/frontend/src/utility/image-effector/ImageEffector.ts +++ b/packages/frontend/src/utility/image-effector/ImageEffector.ts @@ -57,7 +57,7 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a return params[k]; } -export class ImageEffector { +export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> { private gl: WebGL2RenderingContext; private canvas: HTMLCanvasElement | null = null; private renderTextureProgram: WebGLProgram; @@ -70,7 +70,7 @@ export class ImageEffector { private shaderCache: Map<string, WebGLProgram> = new Map(); private perLayerResultTextures: Map<string, WebGLTexture> = new Map(); private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map(); - private fxs: ImageEffectorFx[]; + private fxs: [...IEX]; private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map(); constructor(options: { @@ -78,7 +78,7 @@ export class ImageEffector { renderWidth: number; renderHeight: number; image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; - fxs: ImageEffectorFx[]; + fxs: [...IEX]; }) { this.canvas = options.canvas; this.renderWidth = options.renderWidth; @@ -230,7 +230,7 @@ export class ImageEffector { gl: gl, program: shaderProgram, params: Object.fromEntries( - Object.entries(fx.params).map(([key, param]) => { + Object.entries(fx.params as ImageEffectorFxParamDefs).map(([key, param]) => { return [key, layer.params[key] ?? param.default]; }), ), @@ -238,7 +238,7 @@ export class ImageEffector { width: this.renderWidth, height: this.renderHeight, textures: Object.fromEntries( - Object.entries(fx.params).map(([k, v]) => { + Object.entries(fx.params as ImageEffectorFxParamDefs).map(([k, v]) => { if (v.type !== 'texture') return [k, null]; const param = getValue<typeof v.type>(layer.params, k); if (param == null) return [k, null]; @@ -329,7 +329,7 @@ export class ImageEffector { unused.delete(textureKey); if (this.paramTextures.has(textureKey)) continue; - console.log(`Baking texture of <${textureKey}>...`); + if (_DEV_) console.log(`Baking texture of <${textureKey}>...`); const texture = v.type === 'text' ? await createTextureFromText(this.gl, v.text) : v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) : null; if (texture == null) continue; @@ -339,7 +339,7 @@ export class ImageEffector { } for (const k of unused) { - console.log(`Dispose unused texture <${k}>...`); + if (_DEV_) console.log(`Dispose unused texture <${k}>...`); this.gl.deleteTexture(this.paramTextures.get(k)!.texture); this.paramTextures.delete(k); } diff --git a/packages/frontend/src/utility/watermark.ts b/packages/frontend/src/utility/watermark.ts index 8ee93181a6..f0b38684f0 100644 --- a/packages/frontend/src/utility/watermark.ts +++ b/packages/frontend/src/utility/watermark.ts @@ -3,13 +3,20 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js'; -import { FX_stripe } from './image-effector/fxs/stripe.js'; -import { FX_polkadot } from './image-effector/fxs/polkadot.js'; -import { FX_checker } from './image-effector/fxs/checker.js'; -import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; +import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js'; +import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js'; +import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js'; +import { FX_checker } from '@/utility/image-effector/fxs/checker.js'; +import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; +const WATERMARK_FXS = [ + FX_watermarkPlacement, + FX_stripe, + FX_polkadot, + FX_checker, +] as const satisfies ImageEffectorFx<string, any>[]; + export type WatermarkPreset = { id: string; name: string; @@ -64,7 +71,7 @@ export type WatermarkPreset = { }; export class WatermarkRenderer { - private effector: ImageEffector; + private effector: ImageEffector<typeof WATERMARK_FXS>; private layers: WatermarkPreset['layers'] = []; constructor(options: { @@ -78,7 +85,7 @@ export class WatermarkRenderer { renderWidth: options.renderWidth, renderHeight: options.renderHeight, image: options.image, - fxs: [FX_watermarkPlacement, FX_stripe, FX_polkadot, FX_checker], + fxs: WATERMARK_FXS, }); } @@ -157,6 +164,8 @@ export class WatermarkRenderer { opacity: layer.opacity, }, }; + } else { + throw new Error(`Unknown layer type`); } }); } |