diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-06-04 16:22:09 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-04 16:22:09 +0900 |
| commit | b43dfa260b1416da153d28bcd46a8bcbce02c18d (patch) | |
| tree | 1e4e08092b9f8870db15d267d20b0dd4c9828f95 /packages/frontend/src/components | |
| parent | 🎨 (diff) | |
| download | misskey-b43dfa260b1416da153d28bcd46a8bcbce02c18d.tar.gz misskey-b43dfa260b1416da153d28bcd46a8bcbce02c18d.tar.bz2 misskey-b43dfa260b1416da153d28bcd46a8bcbce02c18d.zip | |
fix/refactor(frontend): 画像編集機能の修正・型強化 (#16156)
* enhance: refine uploadFile
* fix: missing locale
* refactor: harden types
* refactor: シェーダーファイルをlazy-loadingできるように
* fix(frontend): omit console.log in production environment
* fix: glslのバージョン表記は最初の行になければならない
* fix: シェーダーの読み込みが完了してからレンダリングを行うように
* fix merge failure
* fix: ウォーターマークのプリセットがない場合にdividerが2重に表示される問題を修正
* fix: アップローダーダイアログの機能設定でウォーターマークが無効な場合でもデフォルトのプリセットが適用されてしまう問題を修正
* fix lint
* Revert "fix: シェーダーの読み込みが完了してからレンダリングを行うように"
This reverts commit e06f37a7d453ca581858252eae422d8a9e470dc3.
* Revert "fix: glslのバージョン表記は最初の行になければならない"
This reverts commit afcc37d886106c4acd545e4c2922e67f94e1037b.
* Revert "refactor: シェーダーファイルをlazy-loadingできるように"
This reverts commit a1ab2fa38c2b7485c069f9cd089bc7de59416c9d.
* fix: ウォーターマークのFX定義を分ける
* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue
* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue
* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/components')
6 files changed, 80 insertions, 29 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; |