summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/components/MkFormDialog.file.vue5
-rw-r--r--packages/frontend/src/components/MkImageEffectorDialog.vue3
-rw-r--r--packages/frontend/src/components/MkPostForm.vue8
-rw-r--r--packages/frontend/src/components/MkUploaderDialog.vue69
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue17
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue7
-rw-r--r--packages/frontend/src/os.ts3
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue5
-rw-r--r--packages/frontend/src/pages/channel-editor.vue5
-rw-r--r--packages/frontend/src/pages/chat/room.form.vue6
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue5
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue5
-rw-r--r--packages/frontend/src/pages/gallery/edit.vue11
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue5
-rw-r--r--packages/frontend/src/pages/settings/account-data.vue25
-rw-r--r--packages/frontend/src/pages/settings/deck.vue5
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue6
-rw-r--r--packages/frontend/src/preferences/def.ts2
-rw-r--r--packages/frontend/src/utility/drive.ts26
-rw-r--r--packages/frontend/src/utility/image-effector/ImageEffector.ts14
-rw-r--r--packages/frontend/src/utility/watermark.ts23
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`);
}
});
}