Merge 5d87c039fb into 218df19d9c
				
					
				
			This commit is contained in:
		
						commit
						1a26a38f82
					
				|  | @ -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: [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; | ||||
|  |  | |||
|  | @ -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