@@ -212,6 +215,129 @@ watch(enabled, () => {
renderer.render();
}
});
+
+const penMode = ref<'fill' | 'blur' | null>(null);
+
+function showPenMenu(ev: MouseEvent) {
+ os.popupMenu([{
+ text: i18n.ts._imageEffector._fxs.fill,
+ action: () => {
+ penMode.value = 'fill';
+ },
+ }, {
+ text: i18n.ts._imageEffector._fxs.blur,
+ action: () => {
+ penMode.value = 'blur';
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
+function onImagePointerdown(ev: PointerEvent) {
+ if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return;
+
+ const AW = canvasEl.value.clientWidth;
+ const AH = canvasEl.value.clientHeight;
+ const BW = imageBitmap.width;
+ const BH = imageBitmap.height;
+
+ let xOffset = 0;
+ let yOffset = 0;
+
+ if (AW / AH < BW / BH) { // 横長
+ yOffset = AH - BH * (AW / BW);
+ } else { // 縦長
+ xOffset = AW - BW * (AH / BH);
+ }
+
+ xOffset /= 2;
+ yOffset /= 2;
+
+ let startX = ev.offsetX - xOffset;
+ let startY = ev.offsetY - yOffset;
+
+ if (AW / AH < BW / BH) { // 横長
+ startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
+ startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
+ } else { // 縦長
+ startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
+ startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
+ }
+
+ const id = genId();
+ if (penMode.value === 'fill') {
+ layers.push({
+ id,
+ fxId: 'fill',
+ params: {
+ offsetX: 0,
+ offsetY: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ angle: 0,
+ opacity: 1,
+ color: [1, 1, 1],
+ },
+ });
+ } else if (penMode.value === 'blur') {
+ layers.push({
+ id,
+ fxId: 'blur',
+ params: {
+ offsetX: 0,
+ offsetY: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ angle: 0,
+ radius: 3,
+ },
+ });
+ }
+
+ _move(ev.offsetX, ev.offsetY);
+
+ function _move(pointerX: number, pointerY: number) {
+ let x = pointerX - xOffset;
+ let y = pointerY - yOffset;
+
+ if (AW / AH < BW / BH) { // 横長
+ x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
+ y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
+ } else { // 縦長
+ x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
+ y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
+ }
+
+ const scaleX = Math.abs(x - startX);
+ const scaleY = Math.abs(y - startY);
+
+ const layerIndex = layers.findIndex((l) => l.id === id);
+ const layer = layerIndex !== -1 ? layers[layerIndex] : null;
+ if (layer != null) {
+ layer.params.offsetX = (x + startX) - 1;
+ layer.params.offsetY = (y + startY) - 1;
+ layer.params.scaleX = scaleX;
+ layer.params.scaleY = scaleY;
+ layers[layerIndex] = layer;
+ }
+ }
+
+ function move(ev: PointerEvent) {
+ _move(ev.offsetX, ev.offsetY);
+ }
+
+ function up() {
+ canvasEl.value?.removeEventListener('pointermove', move);
+ canvasEl.value?.removeEventListener('pointerup', up);
+ canvasEl.value?.removeEventListener('pointercancel', up);
+ canvasEl.value?.releasePointerCapture(ev.pointerId);
+
+ penMode.value = null;
+ }
+
+ canvasEl.value.addEventListener('pointermove', move);
+ canvasEl.value.addEventListener('pointerup', up);
+ canvasEl.value.setPointerCapture(ev.pointerId);
+}
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 359ee08812..76c65397ae 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index bf332e706e..ba8d3a7210 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -54,6 +54,7 @@ function onPosted() {
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
+ form.value?.abortUploader();
modal.value?.close();
}
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 9c37eb5e72..697346020c 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -90,7 +90,7 @@ function subscribe() {
publickey: encode(subscription.getKey('p256dh')),
});
}, async err => { // When subscribe failed
- // 通知が許可されていなかったとき
+ // 通知が許可されていなかったとき
if (err?.name === 'NotAllowedError') {
console.info('User denied the notification permission request.');
return;
@@ -114,14 +114,13 @@ async function unsubscribe() {
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
- i: $i.token,
endpoint,
- });
+ }, $i.token);
} else {
pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
- });
+ }, null);
pushSubscription.value = null;
}
}
@@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) {
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
-function urlBase64ToUint8Array(base64String: string): Uint8Array {
+function urlBase64ToUint8Array(base64String: string): BufferSource {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 15149b3f0c..8e5cbde8c3 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ role.name }}
-
+
{{ role.usersCount }} users
? users
@@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
- role: Misskey.entities.Role;
+ role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
forModeration: boolean;
detailed?: boolean;
}>(), {
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
index f1cc98def4..937804703d 100644
--- a/packages/frontend/src/components/MkRoleSelectDialog.vue
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -102,12 +102,12 @@ async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
- .map(r => ({ text: r.name, value: r }));
+ .map(r => ({ label: r.name, value: r.id }));
- const { canceled, result: role } = await os.select({ items });
- if (canceled || role == null) return;
+ const { canceled, result: roleId } = await os.select({ items });
+ if (canceled || roleId == null) return;
- selectedRoleIds.value.push(role.id);
+ selectedRoleIds.value.push(roleId);
}
async function removeRole(roleId: string) {
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 9cbaf676c7..e79236fe54 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
-
diff --git a/packages/frontend/src/pages/qr.show.vue b/packages/frontend/src/pages/qr.show.vue
new file mode 100644
index 0000000000..28f80e0963
--- /dev/null
+++ b/packages/frontend/src/pages/qr.show.vue
@@ -0,0 +1,234 @@
+
+
+
+
+
+
+
+
![Misskey Logo]()
+
![Misskey Logo]()
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/qr.vue b/packages/frontend/src/pages/qr.vue
new file mode 100644
index 0000000000..2e5629f232
--- /dev/null
+++ b/packages/frontend/src/pages/qr.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+ {{ i18n.ts._qr.showTabTitle }}
+ {{ i18n.ts._qr.readTabTitle }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 69429728d0..aae638641a 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -164,7 +164,7 @@ const $i = ensureSignin();
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
- connection?: Misskey.ChannelConnection
| null;
+ connection?: Misskey.IChannelConnection | null;
}>();
const showBoardLabels = ref(false);
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 8392384963..1e01496bbb 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
- connection: Misskey.ChannelConnection;
+ connection: Misskey.IChannelConnection;
}>();
const shareWhenStart = defineModel('shareWhenStart', { default: false });
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index a447572cc0..b1ba4da247 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -33,7 +33,7 @@ const props = defineProps<{
}>();
const game = shallowRef(null);
-const connection = shallowRef(null);
+const connection = shallowRef | null>(null);
const shareWhenStart = ref(false);
watch(() => props.gameId, () => {
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index ca404b43c4..2cc13744b1 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -196,6 +196,7 @@ async function addSecurityKey() {
if (auth.canceled) return;
const registrationOptions = parseCreationOptionsFromJSON({
+ // @ts-expect-error misskey-js側に型がない
publicKey: await os.apiWithDialog('i/2fa/register-key', {
password: auth.result.password,
token: auth.result.token,
@@ -226,6 +227,7 @@ async function addSecurityKey() {
password: auth.result.password,
token: auth.result.token,
name: name.result,
+ // @ts-expect-error misskey-js側に型がない
credential: credential.toJSON(),
});
}
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 63b3c95233..57192c0fb7 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -5,9 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.sort }}
-
@@ -60,6 +59,7 @@ import { i18n } from '@/i18n.js';
import bytes from '@/filters/bytes.js';
import { definePage } from '@/page.js';
import MkSelect from '@/components/MkSelect.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
@@ -69,15 +69,19 @@ const paginator = markRaw(new Paginator('drive/files', {
computedParams: computed(() => ({ sort: sortMode.value })),
}));
-const sortOptions = [
- { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc },
- { value: 'createdAtAsc', displayName: i18n.ts._drivecleaner.orderByCreatedAtAsc },
-];
-
const capacity = ref(0);
const usage = ref(0);
const fetching = ref(true);
-const sortModeSelect = ref('sizeDesc');
+const {
+ model: sortModeSelect,
+ def: sortModeSelectDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._drivecleaner.orderBySizeDesc, value: 'sizeDesc' },
+ { label: i18n.ts._drivecleaner.orderByCreatedAtAsc, value: 'createdAtAsc' },
+ ],
+ initialValue: 'sizeDesc',
+});
fetchDriveInfo();
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index cfa4df18fc..f58ff4c78c 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.uploadFolder }}
{{ uploadFolder ? uploadFolder.name : '-' }}
-
+
@@ -129,13 +129,37 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.defaultImageCompressionLevel }}
-
+ {{ i18n.ts.defaultCompressionLevel }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.video }}
+
+
+
+
+
+ {{ i18n.ts.defaultCompressionLevel }}
+
@@ -196,6 +220,7 @@ const meterStyle = computed(() => {
const keepOriginalFilename = prefer.model('keepOriginalFilename');
const defaultWatermarkPresetId = prefer.model('defaultWatermarkPresetId');
const defaultImageCompressionLevel = prefer.model('defaultImageCompressionLevel');
+const defaultVideoCompressionLevel = prefer.model('defaultVideoCompressionLevel');
const watermarkPresetsSyncEnabled = ref(prefer.isSyncEnabled('watermarkPresets'));
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index 5ff5f45a2f..9c70461847 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -36,20 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts._emojiPalette.paletteForMain }}
-
-
-
+
{{ i18n.ts._emojiPalette.paletteForReaction }}
-
-
@@ -68,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
@@ -99,12 +97,15 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.style }}
{{ i18n.ts.needReloadToApply }}
-
-
-
@@ -119,8 +120,9 @@ SPDX-License-Identifier: AGPL-3.0-only