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 commite06f37a7d4
. * Revert "fix: glslのバージョン表記は最初の行になければならない" This reverts commitafcc37d886
. * Revert "refactor: シェーダーファイルをlazy-loadingできるように" This reverts commita1ab2fa38c
. * 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>
This commit is contained in:
parent
e3b57a118d
commit
b43dfa260b
|
@ -12049,6 +12049,14 @@ export interface Locale extends ILocale {
|
|||
* 保存せずに終了しますか?
|
||||
*/
|
||||
"quitWithoutSaveConfirm": string;
|
||||
/**
|
||||
* このファイルは対応していません
|
||||
*/
|
||||
"driveFileTypeWarn": string;
|
||||
/**
|
||||
* 画像ファイルを選択してください
|
||||
*/
|
||||
"driveFileTypeWarnDescription": string;
|
||||
/**
|
||||
* ウォーターマークの編集
|
||||
*/
|
||||
|
|
|
@ -3227,6 +3227,8 @@ defaultPreset: "デフォルトのプリセット"
|
|||
_watermarkEditor:
|
||||
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
|
||||
quitWithoutSaveConfirm: "保存せずに終了しますか?"
|
||||
driveFileTypeWarn: "このファイルは対応していません"
|
||||
driveFileTypeWarnDescription: "画像ファイルを選択してください"
|
||||
title: "ウォーターマークの編集"
|
||||
cover: "全体に被せる"
|
||||
repeat: "敷き詰める"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue