{{ i18n.ts.preview }}
+
+
+
@@ -212,6 +215,100 @@ watch(enabled, () => {
renderer.render();
}
});
+
+const fillSquare = ref(false);
+
+function onImagePointerdown(ev: PointerEvent) {
+ if (canvasEl.value == null || imageBitmap == null || !fillSquare.value) 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();
+ layers.push({
+ id,
+ fxId: 'fillSquare',
+ params: {
+ offsetX: 0,
+ offsetY: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ angle: 0,
+ opacity: 1,
+ color: [1, 1, 1],
+ },
+ });
+
+ _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);
+
+ fillSquare.value = false;
+ }
+
+ canvasEl.value.addEventListener('pointermove', move);
+ canvasEl.value.addEventListener('pointerup', up);
+ canvasEl.value.setPointerCapture(ev.pointerId);
+}
diff --git a/packages/frontend/src/composables/use-lowres-time.ts b/packages/frontend/src/composables/use-lowres-time.ts
new file mode 100644
index 0000000000..3c5b561f51
--- /dev/null
+++ b/packages/frontend/src/composables/use-lowres-time.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, readonly, computed } from 'vue';
+
+const time = ref(Date.now());
+
+export const TIME_UPDATE_INTERVAL = 10000; // 10秒
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * ※ マウント前の時刻を返す可能性があるため、通常は`useLowresTime`を使用する
+*/
+export const lowresTime = readonly(time);
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * 必ず現在時刻以降を返すことを保証するコンポーサブル
+ */
+export function useLowresTime() {
+ // lowresTime自体はマウント前の時刻を返す可能性があるため、必ず現在時刻以降を返すことを保証する
+ const now = Date.now();
+ return computed(() => Math.max(time.value, now));
+}
+
+window.setInterval(() => {
+ time.value = Date.now();
+}, TIME_UPDATE_INTERVAL);
diff --git a/packages/frontend/src/composables/use-mkselect.ts b/packages/frontend/src/composables/use-mkselect.ts
new file mode 100644
index 0000000000..7cb470d169
--- /dev/null
+++ b/packages/frontend/src/composables/use-mkselect.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import type { Ref, MaybeRefOrGetter } from 'vue';
+import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
+
+type UnwrapReadonlyItems = T extends readonly (infer U)[] ? U[] : T;
+
+/** 指定したオプション定義をもとに型を狭めたrefを生成するコンポーサブル */
+export function useMkSelect<
+ const TItemsInput extends MaybeRefOrGetter,
+ const TItems extends TItemsInput extends MaybeRefOrGetter ? U : never,
+ TInitialValue extends OptionValue | void = void,
+ TItemsValue = GetMkSelectValueTypesFromDef>,
+ ModelType = TInitialValue extends void
+ ? TItemsValue
+ : (TItemsValue | TInitialValue)
+>(opts: {
+ items: TItemsInput;
+ initialValue?: (TInitialValue | (OptionValue extends TItemsValue ? OptionValue : TInitialValue)) & (
+ TItemsValue extends TInitialValue
+ ? unknown
+ : { 'Error: Type of initialValue must include all types of items': TItemsValue }
+ );
+}): {
+ def: TItemsInput;
+ model: Ref;
+} {
+ const model = ref(opts.initialValue ?? null);
+
+ return {
+ def: opts.items,
+ model: model as Ref,
+ };
+}
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index 649561cd75..8cac1b6d2a 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -24,7 +24,7 @@ export const globalEvents = new EventEmitter();
export function useGlobalEvent(
event: T,
- callback: Events[T],
+ callback: EventEmitter.EventListener,
): void {
globalEvents.on(event, callback);
onBeforeUnmount(() => {
diff --git a/packages/frontend/src/lib/pizzax.ts b/packages/frontend/src/lib/pizzax.ts
index 20d44032df..6dffcf9478 100644
--- a/packages/frontend/src/lib/pizzax.ts
+++ b/packages/frontend/src/lib/pizzax.ts
@@ -94,7 +94,7 @@ export class Pizzax {
private mergeState(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
- const merged = deepMerge(value, def);
+ const merged = deepMerge>(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index c0fe0f2b85..aec1c7ae4c 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -111,7 +111,7 @@ export const navbarItemDef = reactive({
to: '/channels',
},
chat: {
- title: i18n.ts.chat,
+ title: i18n.ts.directMessage_short,
icon: 'ti ti-messages',
to: '/chat',
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 56a2b8d269..6c5f04c6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -14,6 +14,7 @@ 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 { UploaderFeatures } from '@/composables/use-uploader.js';
+import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -35,9 +36,9 @@ import { focusParent } from '@/utility/focus.js';
export const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record;
-export const apiWithDialog = ((
+export const apiWithDialog = ((
endpoint: E,
- data: P,
+ data: Misskey.Endpoints[E]['req'],
token?: string | null | undefined,
customErrors?: ApiWithDialogCustomErrors,
) => {
@@ -502,50 +503,15 @@ export function authenticateDialog(): Promise<{
});
}
-type SelectItem = {
- value: C;
- text: string;
-};
-
-// default が指定されていたら result は null になり得ないことを保証する overload function
-export function select(props: {
+export function select(props: {
title?: string;
text?: string;
- default: string;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
+ default?: D;
+ items: (MkSelectItem | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
- canceled: false; result: C;
-}>;
-export function select(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C | null;
-}>;
-export function select(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C | null;
+ canceled: false; result: Exclude extends null ? C | null : C;
}> {
return new Promise(resolve => {
const { dispose } = popup(MkDialog, {
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index 7e514c5a73..4640812756 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -11,12 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
@@ -42,51 +36,33 @@ import XEmoji from './emojis.emoji.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
+import { customEmojis, customEmojiCategories } from '@/custom-emojis.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
-const customEmojiTags = getCustomEmojiTags();
const q = ref('');
const searchEmojis = ref(null);
-const selectedTags = ref(new Set());
function search() {
- if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
+ if (q.value === '' || q.value == null) {
searchEmojis.value = null;
return;
}
- if (selectedTags.value.size === 0) {
- const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
+ const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
- if (queryarry) {
- searchEmojis.value = customEmojis.value.filter(emoji =>
- queryarry.includes(`:${emoji.name}:`),
- );
- } else {
- searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
- }
+ if (queryarry) {
+ searchEmojis.value = customEmojis.value.filter(emoji =>
+ queryarry.includes(`:${emoji.name}:`),
+ );
} else {
- searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
- }
-}
-
-function toggleTag(tag) {
- if (selectedTags.value.has(tag)) {
- selectedTags.value.delete(tag);
- } else {
- selectedTags.value.add(tag);
+ searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
}
}
watch(q, () => {
search();
});
-
-watch(selectedTags, () => {
- search();
-}, { deep: true });