Merge branch 'refactor/types' into 'develop'
Refactor/types Co-authored-by: Lhcfl <Lhcfl@outlook.com> See merge request firefish/firefish!10737
This commit is contained in:
commit
bce88ec199
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": false
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -21,7 +21,8 @@
|
||||||
"useImportType": "warn",
|
"useImportType": "warn",
|
||||||
"useShorthandFunctionType": "warn",
|
"useShorthandFunctionType": "warn",
|
||||||
"useTemplate": "warn",
|
"useTemplate": "warn",
|
||||||
"noNonNullAssertion": "off"
|
"noNonNullAssertion": "off",
|
||||||
|
"useNodejsImportProtocol": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
|
||||||
import { packedEmojiSchema } from "@/models/schema/emoji.js";
|
import { packedEmojiSchema } from "@/models/schema/emoji.js";
|
||||||
import { packedNoteEdit } from "@/models/schema/note-edit.js";
|
import { packedNoteEdit } from "@/models/schema/note-edit.js";
|
||||||
import { packedNoteFileSchema } from "@/models/schema/note-file.js";
|
import { packedNoteFileSchema } from "@/models/schema/note-file.js";
|
||||||
|
import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
|
||||||
|
|
||||||
export const refs = {
|
export const refs = {
|
||||||
|
AbuseUserReport: packedAbuseUserReportSchema,
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
|
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
|
||||||
MeDetailedOnly: packedMeDetailedOnlySchema,
|
MeDetailedOnly: packedMeDetailedOnlySchema,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js";
|
||||||
import { Users } from "../index.js";
|
import { Users } from "../index.js";
|
||||||
import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
|
import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
|
||||||
import { awaitAll } from "@/prelude/await-all.js";
|
import { awaitAll } from "@/prelude/await-all.js";
|
||||||
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
|
||||||
export const AbuseUserReportRepository = db
|
export const AbuseUserReportRepository = db
|
||||||
.getRepository(AbuseUserReport)
|
.getRepository(AbuseUserReport)
|
||||||
|
@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db
|
||||||
const report =
|
const report =
|
||||||
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return await awaitAll({
|
const packed: Packed<"AbuseUserReport"> = await awaitAll({
|
||||||
id: report.id,
|
id: report.id,
|
||||||
createdAt: report.createdAt.toISOString(),
|
createdAt: report.createdAt.toISOString(),
|
||||||
comment: report.comment,
|
comment: report.comment,
|
||||||
|
@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db
|
||||||
: null,
|
: null,
|
||||||
forwarded: report.forwarded,
|
forwarded: report.forwarded,
|
||||||
});
|
});
|
||||||
|
return packed;
|
||||||
},
|
},
|
||||||
|
|
||||||
packMany(reports: any[]) {
|
packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) {
|
||||||
return Promise.all(reports.map((x) => this.pack(x)));
|
return Promise.all(reports.map((x) => this.pack(x)));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
description: channel.description,
|
description: channel.description,
|
||||||
userId: channel.userId,
|
userId: channel.userId,
|
||||||
|
bannerId: channel.bannerId,
|
||||||
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
|
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
|
||||||
usersCount: channel.usersCount,
|
usersCount: channel.usersCount,
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
|
|
|
@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
|
||||||
createdAt: post.createdAt.toISOString(),
|
createdAt: post.createdAt.toISOString(),
|
||||||
updatedAt: post.updatedAt.toISOString(),
|
updatedAt: post.updatedAt.toISOString(),
|
||||||
userId: post.userId,
|
userId: post.userId,
|
||||||
user: Users.pack(post.user || post.userId, me),
|
user: Users.pack(post.user || post.userId, me, {
|
||||||
|
detail: true,
|
||||||
|
}),
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.description,
|
description: post.description,
|
||||||
fileIds: post.fileIds,
|
fileIds: post.fileIds,
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
export const packedAbuseUserReportSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
format: "id",
|
||||||
|
example: "xxxxxxxxxx",
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
format: "date-time",
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
resolved: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
reporterId: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
format: "id",
|
||||||
|
},
|
||||||
|
targetUserId: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
format: "id",
|
||||||
|
},
|
||||||
|
assigneeId: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
format: "id",
|
||||||
|
},
|
||||||
|
reporter: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "UserDetailed",
|
||||||
|
},
|
||||||
|
targetUser: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "UserDetailed",
|
||||||
|
},
|
||||||
|
assignee: {
|
||||||
|
type: "object",
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
ref: "UserDetailed",
|
||||||
|
},
|
||||||
|
forwarded: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -36,6 +36,13 @@ export const packedChannelSchema = {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
|
bannerId: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
format: "id",
|
||||||
|
example: "xxxxxxxxxx",
|
||||||
|
},
|
||||||
notesCount: {
|
notesCount: {
|
||||||
type: "number",
|
type: "number",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
@ -57,5 +64,10 @@ export const packedChannelSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
format: "id",
|
format: "id",
|
||||||
},
|
},
|
||||||
|
hasUnreadNote: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const packedGalleryPostSchema = {
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
type: "object",
|
type: "object",
|
||||||
ref: "UserLite",
|
ref: "UserDetailed",
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
@ -79,5 +79,15 @@ export const packedGalleryPostSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
isLiked: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
likedCount: {
|
||||||
|
type: "number",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -16,68 +16,7 @@ export const meta = {
|
||||||
type: "object",
|
type: "object",
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
properties: {
|
ref: "AbuseUserReport",
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
format: "id",
|
|
||||||
example: "xxxxxxxxxx",
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
format: "date-time",
|
|
||||||
},
|
|
||||||
comment: {
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
},
|
|
||||||
resolved: {
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
example: false,
|
|
||||||
},
|
|
||||||
reporterId: {
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
format: "id",
|
|
||||||
},
|
|
||||||
targetUserId: {
|
|
||||||
type: "string",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
format: "id",
|
|
||||||
},
|
|
||||||
assigneeId: {
|
|
||||||
type: "string",
|
|
||||||
nullable: true,
|
|
||||||
optional: false,
|
|
||||||
format: "id",
|
|
||||||
},
|
|
||||||
reporter: {
|
|
||||||
type: "object",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
ref: "User",
|
|
||||||
},
|
|
||||||
targetUser: {
|
|
||||||
type: "object",
|
|
||||||
nullable: false,
|
|
||||||
optional: false,
|
|
||||||
ref: "User",
|
|
||||||
},
|
|
||||||
assignee: {
|
|
||||||
type: "object",
|
|
||||||
nullable: true,
|
|
||||||
optional: true,
|
|
||||||
ref: "User",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
await Channels.update(channel.id, {
|
await Channels.update(channel.id, {
|
||||||
...(ps.name !== undefined ? { name: ps.name } : {}),
|
...(ps.name !== undefined ? { name: ps.name } : {}),
|
||||||
...(ps.description !== undefined ? { description: ps.description } : {}),
|
...(ps.description !== undefined ? { description: ps.description } : {}),
|
||||||
...(banner ? { bannerId: banner.id } : {}),
|
...(banner ? { bannerId: banner.id } : { bannerId: null }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Channels.pack(channel.id, me);
|
return await Channels.pack(channel.id, me);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny:
|
||||||
type FIXME = any;
|
type FIXME = any;
|
||||||
|
|
||||||
declare const _LANGS_: string[][];
|
declare const _LANGS_: string[][];
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__misskey_input_ref__?: HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type {};
|
|
@ -72,13 +72,14 @@ import MkSwitch from "@/components/form/switch.vue";
|
||||||
import MkKeyValue from "@/components/MkKeyValue.vue";
|
import MkKeyValue from "@/components/MkKeyValue.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
report: any;
|
report: entities.AbuseUserReport;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "resolved", reportId: string): void;
|
resolved: [reportId: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const forward = ref(props.report.forwarded);
|
const forward = ref(props.report.forwarded);
|
||||||
|
|
|
@ -18,8 +18,8 @@ import { initChart } from "@/scripts/init-chart";
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLDivElement>();
|
const rootEl = shallowRef<HTMLDivElement | null>(null);
|
||||||
const chartEl = shallowRef<HTMLCanvasElement>();
|
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart | null = null;
|
let chartInstance: Chart | null = null;
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
@ -33,8 +33,8 @@ async function renderActiveUsersChart() {
|
||||||
chartInstance.destroy();
|
chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const wide = rootEl.value.offsetWidth > 700;
|
const wide = rootEl.value!.offsetWidth > 700;
|
||||||
const narrow = rootEl.value.offsetWidth < 400;
|
const narrow = rootEl.value!.offsetWidth < 400;
|
||||||
|
|
||||||
const weeks = wide ? 50 : narrow ? 10 : 25;
|
const weeks = wide ? 50 : narrow ? 10 : 25;
|
||||||
const chartLimit = 7 * weeks;
|
const chartLimit = 7 * weeks;
|
||||||
|
|
|
@ -35,9 +35,10 @@ import MkSparkle from "@/components/MkSparkle.vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
announcement: Announcement;
|
announcement: entities.Announcement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||||
|
@ -45,7 +46,7 @@ const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const gotIt = () => {
|
const gotIt = () => {
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
os.api("i/read-announcement", { announcementId: id });
|
os.api("i/read-announcement", { announcementId: id });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
||||||
<span
|
<span
|
||||||
class="name"
|
class="name"
|
||||||
v-html="emoji.name.replace(q, `<b>${q}</b>`)"
|
v-html="q ? emoji.name.replace(q, `<b>${q}</b>`) : emoji.name"
|
||||||
></span>
|
></span>
|
||||||
<span v-if="emoji.aliasOf" class="alias"
|
<span v-if="emoji.aliasOf" class="alias"
|
||||||
>({{ emoji.aliasOf }})</span
|
>({{ emoji.aliasOf }})</span
|
||||||
|
@ -107,7 +107,7 @@ interface EmojiDef {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
name: string;
|
name: string;
|
||||||
aliasOf?: string;
|
aliasOf?: string;
|
||||||
url?: string;
|
url: string;
|
||||||
isCustomEmoji?: boolean;
|
isCustomEmoji?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const users = ref([]);
|
const users = ref<entities.UserDetailed[]>([]);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await os.api("users/show", {
|
users.value = await os.api("users/show", {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
v-else
|
v-else
|
||||||
class="bghgjjyj _button"
|
class="bghgjjyj _button"
|
||||||
:class="{ inline, primary, gradate, danger, rounded, full, mini }"
|
:class="{ inline, primary, gradate, danger, rounded, full, mini }"
|
||||||
:to="to"
|
:to="to!"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
>
|
>
|
||||||
<div ref="ripples" class="ripples"></div>
|
<div ref="ripples" class="ripples"></div>
|
||||||
|
@ -36,6 +36,7 @@ const props = defineProps<{
|
||||||
gradate?: boolean;
|
gradate?: boolean;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
|
// FIXME: if `link`, `to` is necessary
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
to?: string;
|
to?: string;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
@ -47,7 +48,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "click", payload: MouseEvent): void;
|
click: [payload: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const el = ref<HTMLElement | null>(null);
|
const el = ref<HTMLElement | null>(null);
|
||||||
|
@ -61,11 +62,19 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function distance(p, q): number {
|
function distance(
|
||||||
|
p: { x: number; y: number },
|
||||||
|
q: { x: number; y: number },
|
||||||
|
): number {
|
||||||
return Math.hypot(p.x - q.x, p.y - q.y);
|
return Math.hypot(p.x - q.x, p.y - q.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number {
|
function calcCircleScale(
|
||||||
|
boxW: number,
|
||||||
|
boxH: number,
|
||||||
|
circleCenterX: number,
|
||||||
|
circleCenterY: number,
|
||||||
|
): number {
|
||||||
const origin = { x: circleCenterX, y: circleCenterY };
|
const origin = { x: circleCenterX, y: circleCenterY };
|
||||||
const dist1 = distance({ x: 0, y: 0 }, origin);
|
const dist1 = distance({ x: 0, y: 0 }, origin);
|
||||||
const dist2 = distance({ x: boxW, y: 0 }, origin);
|
const dist2 = distance({ x: boxW, y: 0 }, origin);
|
||||||
|
@ -79,8 +88,8 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
|
|
||||||
const ripple = document.createElement("div");
|
const ripple = document.createElement("div");
|
||||||
ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px";
|
ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`;
|
||||||
ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px";
|
ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`;
|
||||||
|
|
||||||
ripples.value!.appendChild(ripple);
|
ripples.value!.appendChild(ripple);
|
||||||
|
|
||||||
|
@ -97,7 +106,7 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
vibrate(10);
|
vibrate(10);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
ripple.style.transform = "scale(" + scale / 2 + ")";
|
ripple.style.transform = `scale(${scale / 2})`;
|
||||||
}, 1);
|
}, 1);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
ripple.style.transition = "all 1s ease";
|
ripple.style.transition = "all 1s ease";
|
||||||
|
|
|
@ -50,7 +50,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "update:modelValue", v: string | null): void;
|
"update:modelValue": [v: string | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const available = ref(false);
|
const available = ref(false);
|
||||||
|
@ -93,7 +93,9 @@ if (loaded) {
|
||||||
src: src.value,
|
src: src.value,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
).addEventListener("load", () => (available.value = true));
|
)
|
||||||
|
// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
|
||||||
|
.addEventListener("load", () => (available.value = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
|
|
@ -27,10 +27,11 @@ import { ref } from "vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: entities.Channel;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
@ -38,7 +39,7 @@ const props = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFollowing = ref<boolean>(props.channel.isFollowing);
|
const isFollowing = ref<boolean>(props.channel.isFollowing ?? false);
|
||||||
const wait = ref(false);
|
const wait = ref(false);
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items }">
|
<template #default="{ items }: { items: entities.Channel[] }">
|
||||||
<MkChannelPreview
|
<MkChannelPreview
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
@ -29,14 +29,15 @@ import type { PagingOf } from "@/components/MkPagination.vue";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pagination: PagingOf<entities.Channel>;
|
pagination: PagingOf<entities.Channel>;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
extractor?: (item: any) => any;
|
// TODO: this function is not used and may can be removed
|
||||||
|
extractor?: (item: entities.Channel) => entities.Channel;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
extractor: (item) => item,
|
extractor: (item: entities.Channel) => item,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -54,9 +54,10 @@
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: entities.Channel;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const bannerStyle = computed(() => {
|
const bannerStyle = computed(() => {
|
||||||
|
|
|
@ -100,9 +100,9 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||||
const negate = (arr) => arr.map((x) => -x);
|
const negate = (arr) => arr.map((x) => -x);
|
||||||
const alpha = (hex, a) => {
|
const alpha = (hex, a) => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||||
const r = parseInt(result[1], 16);
|
const r = Number.parseInt(result[1], 16);
|
||||||
const g = parseInt(result[2], 16);
|
const g = Number.parseInt(result[2], 16);
|
||||||
const b = parseInt(result[3], 16);
|
const b = Number.parseInt(result[3], 16);
|
||||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,11 +28,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {} from "vue";
|
import type { Ref } from "vue";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: boolean;
|
showing: Ref<boolean>;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
:class="{
|
:class="{
|
||||||
isMe: isMe(message),
|
isMe: isMe(message),
|
||||||
isRead: message.groupId
|
isRead: message.groupId
|
||||||
? message.reads.includes(me?.id)
|
? message.reads.includes(me!.id)
|
||||||
: message.isRead,
|
: message.isRead,
|
||||||
}"
|
}"
|
||||||
:to="
|
:to="
|
||||||
message.groupId
|
message.groupId
|
||||||
? `/my/messaging/group/${message.groupId}`
|
? `/my/messaging/group/${message.groupId}`
|
||||||
: `/my/messaging/${acct.toString(
|
: `/my/messaging/${acct.toString(
|
||||||
isMe(message) ? message.recipient : message.user,
|
isMe(message) ? message.recipient! : message.user,
|
||||||
)}`
|
)}`
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
@ -22,27 +22,27 @@
|
||||||
message.groupId
|
message.groupId
|
||||||
? message.user
|
? message.user
|
||||||
: isMe(message)
|
: isMe(message)
|
||||||
? message.recipient
|
? message.recipient!
|
||||||
: message.user
|
: message.user
|
||||||
"
|
"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
disable-link
|
disable-link
|
||||||
/>
|
/>
|
||||||
<header v-if="message.groupId">
|
<header v-if="message.groupId">
|
||||||
<span class="name">{{ message.group.name }}</span>
|
<span class="name">{{ message.group!.name }}</span>
|
||||||
<MkTime :time="message.createdAt" class="time" />
|
<MkTime :time="message.createdAt" class="time" />
|
||||||
</header>
|
</header>
|
||||||
<header v-else>
|
<header v-else>
|
||||||
<span class="name"
|
<span class="name"
|
||||||
><MkUserName
|
><MkUserName
|
||||||
:user="
|
:user="
|
||||||
isMe(message) ? message.recipient : message.user
|
isMe(message) ? message.recipient! : message.user
|
||||||
"
|
"
|
||||||
/></span>
|
/></span>
|
||||||
<span class="username"
|
<span class="username"
|
||||||
>@{{
|
>@{{
|
||||||
acct.toString(
|
acct.toString(
|
||||||
isMe(message) ? message.recipient : message.user,
|
isMe(message) ? message.recipient! : message.user,
|
||||||
)
|
)
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
|
@ -65,16 +65,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { acct } from "firefish-js";
|
import { acct, type entities } from "firefish-js";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { me } from "@/me";
|
import { me } from "@/me";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
message: Record<string, any>;
|
message: entities.MessagingMessage;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function isMe(message): boolean {
|
function isMe(message: entities.MessagingMessage): boolean {
|
||||||
return message.userId === me?.id;
|
return message.userId === me!.id;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) {
|
||||||
const { lang } = props;
|
const { lang } = props;
|
||||||
loadLanguage(props.lang).then(
|
loadLanguage(props.lang).then(
|
||||||
// onLoaded
|
// onLoaded
|
||||||
|
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
|
||||||
() => (prismLang.value = lang),
|
() => (prismLang.value = lang),
|
||||||
// onError
|
// onError
|
||||||
() => {},
|
() => {},
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
scrollable,
|
scrollable,
|
||||||
closed: !showBody,
|
closed: !showBody,
|
||||||
}"
|
}"
|
||||||
|
ref="el"
|
||||||
>
|
>
|
||||||
<header v-if="showHeader" ref="header">
|
<header v-if="showHeader" ref="header">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
|
@ -59,123 +60,110 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(
|
||||||
props: {
|
defineProps<{
|
||||||
showHeader: {
|
showHeader?: boolean;
|
||||||
type: Boolean,
|
thin?: boolean;
|
||||||
required: false,
|
naked?: boolean;
|
||||||
default: true,
|
foldable?: boolean;
|
||||||
},
|
expanded?: boolean;
|
||||||
thin: {
|
scrollable?: boolean;
|
||||||
type: Boolean,
|
maxHeight?: number | null;
|
||||||
required: false,
|
}>(),
|
||||||
default: false,
|
{
|
||||||
},
|
showHeader: true,
|
||||||
naked: {
|
thin: false,
|
||||||
type: Boolean,
|
naked: false,
|
||||||
required: false,
|
foldable: false,
|
||||||
default: false,
|
expanded: true,
|
||||||
},
|
scrollable: false,
|
||||||
foldable: {
|
maxHeight: null,
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
expanded: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
scrollable: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
maxHeight: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
);
|
||||||
return {
|
|
||||||
showBody: this.expanded,
|
|
||||||
omitted: null,
|
|
||||||
ignoreOmit: false,
|
|
||||||
i18n,
|
|
||||||
icon,
|
|
||||||
defaultStore,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$watch(
|
|
||||||
"showBody",
|
|
||||||
(showBody) => {
|
|
||||||
const headerHeight = this.showHeader
|
|
||||||
? this.$refs.header.offsetHeight
|
|
||||||
: 0;
|
|
||||||
this.$el.style.minHeight = `${headerHeight}px`;
|
|
||||||
if (showBody) {
|
|
||||||
this.$el.style.flexBasis = "auto";
|
|
||||||
} else {
|
|
||||||
this.$el.style.flexBasis = `${headerHeight}px`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.$el.style.setProperty("--maxHeight", this.maxHeight + "px");
|
const showBody = ref(props.expanded);
|
||||||
|
const omitted = ref<boolean | null>(null);
|
||||||
|
const ignoreOmit = ref(false);
|
||||||
|
const el = ref<HTMLElement | null>(null);
|
||||||
|
const header = ref<HTMLElement | null>(null);
|
||||||
|
const content = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const calcOmit = () => {
|
function toggleContent(show: boolean) {
|
||||||
if (
|
if (!props.foldable) return;
|
||||||
this.omitted ||
|
showBody.value = show;
|
||||||
this.ignoreOmit ||
|
}
|
||||||
this.maxHeight == null ||
|
|
||||||
this.$refs.content == null
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const height = this.$refs.content.offsetHeight;
|
|
||||||
this.omitted = height > this.maxHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
function enter(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = 0;
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = `${elementHeight}px`;
|
||||||
|
}
|
||||||
|
function afterEnter(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
function leave(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = `${elementHeight}px`;
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
}
|
||||||
|
function afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watch(
|
||||||
|
showBody,
|
||||||
|
(showBody) => {
|
||||||
|
const headerHeight = props.showHeader ? header.value!.offsetHeight : 0;
|
||||||
|
el.value!.style.minHeight = `${headerHeight}px`;
|
||||||
|
if (showBody) {
|
||||||
|
el.value!.style.flexBasis = "auto";
|
||||||
|
} else {
|
||||||
|
el.value!.style.flexBasis = `${headerHeight}px`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props.maxHeight != null) {
|
||||||
|
el.value!.style.setProperty("--maxHeight", `${props.maxHeight}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcOmit = () => {
|
||||||
|
if (
|
||||||
|
omitted.value ||
|
||||||
|
ignoreOmit.value ||
|
||||||
|
props.maxHeight == null ||
|
||||||
|
content.value == null
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const height = content.value.offsetHeight;
|
||||||
|
omitted.value = height > props.maxHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
calcOmit();
|
||||||
|
|
||||||
|
new ResizeObserver((_entries, _observer) => {
|
||||||
calcOmit();
|
calcOmit();
|
||||||
new ResizeObserver((entries, observer) => {
|
}).observe(content.value!);
|
||||||
calcOmit();
|
});
|
||||||
}).observe(this.$refs.content);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleContent(show: boolean) {
|
|
||||||
if (!this.foldable) return;
|
|
||||||
this.showBody = show;
|
|
||||||
},
|
|
||||||
|
|
||||||
enter(el) {
|
defineExpose({
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
toggleContent,
|
||||||
el.style.height = 0;
|
enter,
|
||||||
el.offsetHeight; // reflow
|
afterEnter,
|
||||||
el.style.height = elementHeight + "px";
|
leave,
|
||||||
},
|
afterLeave,
|
||||||
afterEnter(el) {
|
|
||||||
el.style.height = null;
|
|
||||||
},
|
|
||||||
leave(el) {
|
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
|
||||||
el.style.height = elementHeight + "px";
|
|
||||||
el.offsetHeight; // reflow
|
|
||||||
el.style.height = 0;
|
|
||||||
},
|
|
||||||
afterLeave(el) {
|
|
||||||
el.style.height = null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ const emit = defineEmits<{
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const rootEl = ref<HTMLDivElement>();
|
const rootEl = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const zIndex = ref<number>(os.claimZIndex("high"));
|
const zIndex = ref<number>(os.claimZIndex("high"));
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@ onMounted(() => {
|
||||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||||
|
|
||||||
const width = rootEl.value.offsetWidth;
|
const width = rootEl.value!.offsetWidth;
|
||||||
const height = rootEl.value.offsetHeight;
|
const height = rootEl.value!.offsetHeight;
|
||||||
|
|
||||||
if (left + width - window.scrollX > window.innerWidth) {
|
if (left + width - window.scrollX > window.innerWidth) {
|
||||||
left = window.innerWidth - width + window.scrollX;
|
left = window.innerWidth - width + window.scrollX;
|
||||||
|
@ -55,8 +55,8 @@ onMounted(() => {
|
||||||
left = 0;
|
left = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
rootEl.value.style.top = `${top}px`;
|
rootEl.value!.style.top = `${top}px`;
|
||||||
rootEl.value.style.left = `${left}px`;
|
rootEl.value!.style.left = `${left}px`;
|
||||||
|
|
||||||
document.body.addEventListener("mousedown", onMousedown);
|
document.body.addEventListener("mousedown", onMousedown);
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,40 +68,48 @@ let cropper: Cropper | null = null;
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
const ok = async () => {
|
const ok = async () => {
|
||||||
const promise = new Promise<entities.DriveFile>(async (res) => {
|
async function UploadCroppedImg(): Promise<entities.DriveFile> {
|
||||||
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
||||||
croppedCanvas.toBlob((blob) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", blob);
|
|
||||||
if (defaultStore.state.uploadFolder) {
|
|
||||||
formData.append("folderId", defaultStore.state.uploadFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(apiUrl + "/drive/files/create", {
|
const blob = await new Promise<Blob | null>((resolve) =>
|
||||||
method: "POST",
|
croppedCanvas!.toBlob((blob) => resolve(blob)),
|
||||||
body: formData,
|
);
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${me.token}`,
|
// MDN says `null` may be passed if the image cannot be created for any reason.
|
||||||
},
|
// But I don't think this is reachable for normal case.
|
||||||
})
|
if (blob == null) {
|
||||||
.then((response) => response.json())
|
throw "Cropping image failed.";
|
||||||
.then((f) => {
|
}
|
||||||
res(f);
|
|
||||||
});
|
const formData = new FormData();
|
||||||
|
formData.append("file", blob);
|
||||||
|
if (defaultStore.state.uploadFolder) {
|
||||||
|
formData.append("folderId", defaultStore.state.uploadFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiUrl}/drive/files/create`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${me!.token}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = UploadCroppedImg();
|
||||||
|
|
||||||
os.promiseDialog(promise);
|
os.promiseDialog(promise);
|
||||||
|
|
||||||
const f = await promise;
|
const f = await promise;
|
||||||
|
|
||||||
emit("ok", f);
|
emit("ok", f);
|
||||||
dialogEl.value.close();
|
dialogEl.value!.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit("cancel");
|
emit("cancel");
|
||||||
dialogEl.value.close();
|
dialogEl.value!.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImageLoad = () => {
|
const onImageLoad = () => {
|
||||||
|
@ -114,7 +122,7 @@ const onImageLoad = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
cropper = new Cropper(imgEl.value, {});
|
cropper = new Cropper(imgEl.value!, {});
|
||||||
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
@ -127,13 +135,13 @@ onMounted(() => {
|
||||||
selection.outlined = true;
|
selection.outlined = true;
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center("contain");
|
cropper!.getCropperImage()!.$center("contain");
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// モーダルオープンアニメーションが終わったあとで再度調整
|
// モーダルオープンアニメーションが終わったあとで再度調整
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center("contain");
|
cropper!.getCropperImage()!.$center("contain");
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,7 +48,7 @@ const toggle = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
el.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea
|
<MkTextarea
|
||||||
v-if="input && input.type === 'paragraph'"
|
v-if="input && input.type === 'paragraph'"
|
||||||
v-model="inputValue"
|
v-model="(inputValue as string)"
|
||||||
autofocus
|
autofocus
|
||||||
type="paragraph"
|
type="paragraph"
|
||||||
:placeholder="input.placeholder || undefined"
|
:placeholder="input.placeholder || undefined"
|
||||||
|
@ -204,28 +204,44 @@ import { i18n } from "@/i18n";
|
||||||
import iconify from "@/scripts/icon";
|
import iconify from "@/scripts/icon";
|
||||||
|
|
||||||
interface Input {
|
interface Input {
|
||||||
type: HTMLInputElement["type"];
|
type?:
|
||||||
|
| "text"
|
||||||
|
| "number"
|
||||||
|
| "password"
|
||||||
|
| "email"
|
||||||
|
| "url"
|
||||||
|
| "date"
|
||||||
|
| "time"
|
||||||
|
| "search"
|
||||||
|
| "paragraph";
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
default: string | number | null;
|
default?: string | number | null;
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Select {
|
type Select = {
|
||||||
items: {
|
default?: string | null;
|
||||||
value: string;
|
} & (
|
||||||
text: string;
|
| {
|
||||||
}[];
|
items: {
|
||||||
groupedItems: {
|
value: string;
|
||||||
label: string;
|
text: string;
|
||||||
items: {
|
}[];
|
||||||
value: string;
|
groupedItems?: undefined;
|
||||||
text: string;
|
}
|
||||||
}[];
|
| {
|
||||||
}[];
|
items?: undefined;
|
||||||
default: string | null;
|
groupedItems: {
|
||||||
}
|
label: string;
|
||||||
|
items: {
|
||||||
|
value: string;
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -237,8 +253,8 @@ const props = withDefaults(
|
||||||
| "question"
|
| "question"
|
||||||
| "waiting"
|
| "waiting"
|
||||||
| "search";
|
| "search";
|
||||||
title: string;
|
title?: string | null;
|
||||||
text?: string;
|
text?: string | null;
|
||||||
isPlaintext?: boolean;
|
isPlaintext?: boolean;
|
||||||
input?: Input;
|
input?: Input;
|
||||||
select?: Select;
|
select?: Select;
|
||||||
|
@ -246,7 +262,7 @@ const props = withDefaults(
|
||||||
actions?: {
|
actions?: {
|
||||||
text: string;
|
text: string;
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
callback: (...args: any[]) => void;
|
callback: () => void;
|
||||||
}[];
|
}[];
|
||||||
showOkButton?: boolean;
|
showOkButton?: boolean;
|
||||||
showCancelButton?: boolean;
|
showCancelButton?: boolean;
|
||||||
|
@ -268,7 +284,10 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "done", v: { canceled: boolean; result: any }): void;
|
(
|
||||||
|
ev: "done",
|
||||||
|
v: { canceled: boolean; result?: string | number | boolean | null },
|
||||||
|
): void;
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -306,7 +325,7 @@ const okButtonDisabled = computed<boolean>(() => {
|
||||||
|
|
||||||
const inputEl = ref<typeof MkInput>();
|
const inputEl = ref<typeof MkInput>();
|
||||||
|
|
||||||
function done(canceled: boolean, result?) {
|
function done(canceled: boolean, result?: string | number | boolean | null) {
|
||||||
emit("done", { canceled, result });
|
emit("done", { canceled, result });
|
||||||
modal.value?.close(null);
|
modal.value?.close(null);
|
||||||
}
|
}
|
||||||
|
@ -342,12 +361,12 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateToYYYYMMDD(date) {
|
// function formatDateToYYYYMMDD(date) {
|
||||||
const year = date.getFullYear();
|
// const year = date.getFullYear();
|
||||||
const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
// const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
||||||
const day = ("0" + (date.getDate() + 1)).slice(-2);
|
// const day = ("0" + (date.getDate() + 1)).slice(-2);
|
||||||
return `${year}-${month}-${day}`;
|
// return `${year}-${month}-${day}`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends a new search parameter to the value in the input field.
|
* Appends a new search parameter to the value in the input field.
|
||||||
|
@ -355,18 +374,18 @@ function formatDateToYYYYMMDD(date) {
|
||||||
* begin typing a new criteria.
|
* begin typing a new criteria.
|
||||||
* @param value The value to append.
|
* @param value The value to append.
|
||||||
*/
|
*/
|
||||||
function appendFilter(value: string) {
|
// function appendFilter(value: string) {
|
||||||
return (
|
// return (
|
||||||
[
|
// [
|
||||||
typeof inputValue.value === "string"
|
// typeof inputValue.value === "string"
|
||||||
? inputValue.value.trim()
|
// ? inputValue.value.trim()
|
||||||
: inputValue.value,
|
// : inputValue.value,
|
||||||
value,
|
// value,
|
||||||
]
|
// ]
|
||||||
.join(" ")
|
// .join(" ")
|
||||||
.trim() + " "
|
// .trim() + " "
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener("keydown", onKeydown);
|
document.addEventListener("keydown", onKeydown);
|
||||||
|
|
|
@ -26,7 +26,7 @@ const props = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let intervalId;
|
let intervalId: number;
|
||||||
const hh = ref("");
|
const hh = ref("");
|
||||||
const mm = ref("");
|
const mm = ref("");
|
||||||
const ss = ref("");
|
const ss = ref("");
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<MkButton
|
<MkButton
|
||||||
v-if="instance.donationLink"
|
v-if="instance.donationLink"
|
||||||
gradate
|
gradate
|
||||||
@click="openExternal(instance.donationLink)"
|
@click="openExternal(instance.donationLink!)"
|
||||||
>{{
|
>{{
|
||||||
i18n.t("_aboutFirefish.donateHost", {
|
i18n.t("_aboutFirefish.donateHost", {
|
||||||
host: hostname,
|
host: hostname,
|
||||||
|
@ -73,7 +73,8 @@ const emit = defineEmits<{
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hostname = instance.name?.length < 38 ? instance.name : host;
|
const hostname =
|
||||||
|
instance.name?.length && instance.name?.length < 38 ? instance.name : host;
|
||||||
|
|
||||||
const zIndex = os.claimZIndex("low");
|
const zIndex = os.claimZIndex("low");
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ function neverShow() {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openExternal(link) {
|
function openExternal(link: string) {
|
||||||
window.open(link, "_blank");
|
window.open(link, "_blank");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -47,6 +47,7 @@ import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { me } from "@/me";
|
import { me } from "@/me";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { MenuItem } from "@/types/menu";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -72,7 +73,7 @@ const title = computed(
|
||||||
() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
|
() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu(): MenuItem[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: i18n.ts.rename,
|
text: i18n.ts.rename,
|
||||||
|
@ -180,12 +181,15 @@ function describe() {
|
||||||
image: props.file,
|
image: props.file,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
done: (result) => {
|
done: (result: {
|
||||||
|
canceled: boolean;
|
||||||
|
result?: string | null;
|
||||||
|
}) => {
|
||||||
if (!result || result.canceled) return;
|
if (!result || result.canceled) return;
|
||||||
const comment = result.result;
|
const comment = result.result;
|
||||||
os.api("drive/files/update", {
|
os.api("drive/files/update", {
|
||||||
fileId: props.file.id,
|
fileId: props.file.id,
|
||||||
comment: comment.length === 0 ? null : comment,
|
comment: comment || null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -253,7 +253,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
||||||
removeFolder(folderId);
|
removeFolder(folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragover(ev: DragEvent): any {
|
function onDragover(ev: DragEvent) {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
|
||||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||||
|
@ -285,7 +285,7 @@ function onDragleave() {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(ev: DragEvent): any {
|
function onDrop(ev: DragEvent) {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
|
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
goRoot();
|
goRoot();
|
||||||
return;
|
return;
|
||||||
} else if (typeof target === "object") {
|
|
||||||
target = target.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
os.api("drive/folders/show", {
|
os.api("drive/folders/show", {
|
||||||
folderId: target,
|
folderId: target.id,
|
||||||
}).then((folderToMove) => {
|
}).then((folderToMove) => {
|
||||||
folder.value = folderToMove;
|
folder.value = folderToMove;
|
||||||
hierarchyFolders.value = [];
|
hierarchyFolders.value = [];
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
class="_button"
|
class="_button"
|
||||||
@click.stop="
|
@click.stop="
|
||||||
applyUnicodeSkinTone(
|
applyUnicodeSkinTone(
|
||||||
props.skinTones.indexOf(skinTone) + 1,
|
props.skinTones!.indexOf(skinTone) + 1,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|
|
@ -180,6 +180,11 @@ import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
|
||||||
|
// FIXME: This variable doesn't seem to be used at all. I don't know why it was here.
|
||||||
|
const isActive = ref<boolean>();
|
||||||
|
|
||||||
|
type EmojiDef = string | entities.CustomEmoji | UnicodeEmojiDef;
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -193,7 +198,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "chosen", v: string, ev: MouseEvent): void;
|
chosen: [v: string, ev?: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const search = ref<HTMLInputElement>();
|
const search = ref<HTMLInputElement>();
|
||||||
|
@ -410,13 +415,17 @@ function reset() {
|
||||||
q.value = "";
|
q.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKey(
|
function getKey(emoji: EmojiDef): string {
|
||||||
emoji: string | entities.CustomEmoji | UnicodeEmojiDef,
|
if (typeof emoji === "string") {
|
||||||
): string {
|
return emoji;
|
||||||
return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
|
}
|
||||||
|
if ("emoji" in emoji) {
|
||||||
|
return emoji.emoji;
|
||||||
|
}
|
||||||
|
return `:${emoji.name}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen(emoji: any, ev?: MouseEvent) {
|
function chosen(emoji: EmojiDef, ev?: MouseEvent) {
|
||||||
const el =
|
const el =
|
||||||
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
@ -432,22 +441,33 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!pinned.value.includes(key)) {
|
if (!pinned.value.includes(key)) {
|
||||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||||
recents = recents.filter((emoji: any) => emoji !== key);
|
recents = recents.filter((emoji) => emoji !== key);
|
||||||
recents.unshift(key);
|
recents.unshift(key);
|
||||||
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
|
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function paste(event: ClipboardEvent) {
|
async function paste(event: ClipboardEvent) {
|
||||||
const paste = (event.clipboardData || window.clipboardData).getData("text");
|
let pasteStr: string | null = null;
|
||||||
if (done(paste)) {
|
if (event.clipboardData) {
|
||||||
|
pasteStr = event.clipboardData.getData("text");
|
||||||
|
} else {
|
||||||
|
// Use native api
|
||||||
|
try {
|
||||||
|
pasteStr = await window.navigator.clipboard.readText();
|
||||||
|
} catch (_err) {
|
||||||
|
// Reading the clipboard requires permission, and the user did not give it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (done(pasteStr)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function done(query?: any): boolean | void {
|
function done(query?: string | null): boolean {
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentially
|
||||||
if (query == null) query = q.value;
|
if (query == null) query = q.value;
|
||||||
if (query == null || typeof query !== "string") return;
|
if (query == null || typeof query !== "string") return false;
|
||||||
|
|
||||||
const q2 = query.replaceAll(":", "");
|
const q2 = query.replaceAll(":", "");
|
||||||
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
|
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
|
||||||
|
@ -470,6 +490,7 @@ function done(query?: any): boolean | void {
|
||||||
chosen(searchResultUnicode.value[0]);
|
chosen(searchResultUnicode.value[0]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -51,7 +51,7 @@ withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "done", v: any): void;
|
(ev: "done", v: string): void;
|
||||||
(ev: "close"): void;
|
(ev: "close"): void;
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -64,7 +64,7 @@ function checkForShift(ev?: MouseEvent) {
|
||||||
modal.value?.close(ev);
|
modal.value?.close(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen(emoji: any, ev: MouseEvent) {
|
function chosen(emoji: string, ev?: MouseEvent) {
|
||||||
emit("done", emoji);
|
emit("done", emoji);
|
||||||
checkForShift(ev);
|
checkForShift(ev);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,72 +31,76 @@
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { getUniqueId } from "@/os";
|
import { getUniqueId } from "@/os";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
// import icon from "@/scripts/icon";
|
// import icon from "@/scripts/icon";
|
||||||
|
|
||||||
const localStoragePrefix = "ui:folder:";
|
const localStoragePrefix = "ui:folder:";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(
|
||||||
props: {
|
defineProps<{
|
||||||
expanded: {
|
expanded?: boolean;
|
||||||
type: Boolean,
|
persistKey?: string | null;
|
||||||
required: false,
|
}>(),
|
||||||
default: true,
|
{
|
||||||
},
|
expanded: true,
|
||||||
persistKey: {
|
persistKey: null,
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
);
|
||||||
return {
|
|
||||||
bodyId: getUniqueId(),
|
|
||||||
showBody:
|
|
||||||
this.persistKey &&
|
|
||||||
localStorage.getItem(localStoragePrefix + this.persistKey)
|
|
||||||
? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
|
|
||||||
: this.expanded,
|
|
||||||
animation: defaultStore.state.animation,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showBody() {
|
|
||||||
if (this.persistKey) {
|
|
||||||
localStorage.setItem(
|
|
||||||
localStoragePrefix + this.persistKey,
|
|
||||||
this.showBody ? "t" : "f",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleContent(show: boolean) {
|
|
||||||
this.showBody = show;
|
|
||||||
},
|
|
||||||
|
|
||||||
enter(el) {
|
const bodyId = ref(getUniqueId());
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
|
||||||
el.style.height = 0;
|
const showBody = ref(
|
||||||
el.offsetHeight; // reflow
|
props.persistKey &&
|
||||||
el.style.height = elementHeight + "px";
|
localStorage.getItem(localStoragePrefix + props.persistKey)
|
||||||
},
|
? localStorage.getItem(localStoragePrefix + props.persistKey) === "t"
|
||||||
afterEnter(el) {
|
: props.expanded,
|
||||||
el.style.height = null;
|
);
|
||||||
},
|
|
||||||
leave(el) {
|
const animation = defaultStore.state.animation;
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
|
||||||
el.style.height = elementHeight + "px";
|
watch(showBody, () => {
|
||||||
el.offsetHeight; // reflow
|
if (props.persistKey) {
|
||||||
el.style.height = 0;
|
localStorage.setItem(
|
||||||
},
|
localStoragePrefix + props.persistKey,
|
||||||
afterLeave(el) {
|
showBody.value ? "t" : "f",
|
||||||
el.style.height = null;
|
);
|
||||||
},
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
|
function toggleContent(show: boolean) {
|
||||||
|
showBody.value = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enter(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = 0;
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
// biome-ignore lint/style/useTemplate: <explanation>
|
||||||
|
el.style.height = elementHeight + "px";
|
||||||
|
}
|
||||||
|
function afterEnter(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
function leave(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
// biome-ignore lint/style/useTemplate: <explanation>
|
||||||
|
el.style.height = elementHeight + "px";
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
}
|
||||||
|
function afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleContent,
|
||||||
|
enter,
|
||||||
|
afterEnter,
|
||||||
|
leave,
|
||||||
|
afterLeave,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<i :class="icon('ph-dots-three-outline')"></i>
|
<i :class="icon('ph-dots-three-outline')"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!hideFollowButton && isSignedIn && me.id != user.id"
|
v-if="!hideFollowButton && isSignedIn && me!.id != user.id"
|
||||||
v-tooltip="full ? null : `${state} ${user.name || user.username}`"
|
v-tooltip="full ? null : `${state} ${user.name || user.username}`"
|
||||||
class="kpoogebi _button follow-button"
|
class="kpoogebi _button follow-button"
|
||||||
:class="{
|
:class="{
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="370"
|
:width="370"
|
||||||
:height="400"
|
:height="400"
|
||||||
@close="dialog.close()"
|
@close="dialog!.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||||
|
@ -76,7 +76,7 @@ const emit = defineEmits<{
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog: InstanceType<typeof XModalWindow> = ref();
|
const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
|
||||||
|
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const email = ref("");
|
const email = ref("");
|
||||||
|
@ -89,7 +89,7 @@ async function onSubmit() {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
});
|
});
|
||||||
emit("done");
|
emit("done");
|
||||||
dialog.value.close();
|
dialog.value!.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
@ -17,86 +17,107 @@
|
||||||
<MkSpacer :margin-min="20" :margin-max="32">
|
<MkSpacer :margin-min="20" :margin-max="32">
|
||||||
<div class="_formRoot">
|
<div class="_formRoot">
|
||||||
<template
|
<template
|
||||||
v-for="item in Object.keys(form).filter(
|
v-for="[formItem, formItemName] in unHiddenForms()"
|
||||||
(item) => !form[item].hidden,
|
|
||||||
)"
|
|
||||||
>
|
>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-if="form[item].type === 'number'"
|
v-if="formItem.type === 'number'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
type="number"
|
type="number"
|
||||||
:step="form[item].step || 1"
|
:step="formItem.step || 1"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-else-if="
|
v-else-if="
|
||||||
form[item].type === 'string' &&
|
formItem.type === 'string' &&
|
||||||
!form[item].multiline
|
!formItem.multiline
|
||||||
"
|
"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
type="text"
|
type="text"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
|
}}</template>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput
|
||||||
|
v-else-if="
|
||||||
|
formItem.type === 'email' ||
|
||||||
|
formItem.type === 'password' ||
|
||||||
|
formItem.type === 'url' ||
|
||||||
|
formItem.type === 'date' ||
|
||||||
|
formItem.type === 'time' ||
|
||||||
|
formItem.type === 'search'
|
||||||
|
"
|
||||||
|
v-model="values[formItemName]"
|
||||||
|
:type="formItem.type"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="formItem.label || formItemName"></span
|
||||||
|
><span v-if="formItem.required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<template v-if="formItem.description" #caption>{{
|
||||||
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormTextarea
|
<FormTextarea
|
||||||
v-else-if="
|
v-else-if="
|
||||||
form[item].type === 'string' && form[item].multiline
|
formItem.type === 'string' && formItem.multiline
|
||||||
"
|
"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
<FormSwitch
|
<FormSwitch
|
||||||
v-else-if="form[item].type === 'boolean'"
|
v-else-if="formItem.type === 'boolean'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<span v-text="form[item].label || item"></span>
|
<span v-text="formItem.label || formItemName"></span>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormSwitch>
|
</FormSwitch>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
v-else-if="form[item].type === 'enum'"
|
v-else-if="formItem.type === 'enum'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label>
|
||||||
><span v-text="form[item].label || item"></span
|
<span v-text="formItem.label || formItemName"></span>
|
||||||
><span v-if="form[item].required === false">
|
<span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
|
||||||
>
|
>
|
||||||
|
</template>
|
||||||
<option
|
<option
|
||||||
v-for="item in form[item].enum"
|
v-for="item in formItem.enum"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
>
|
>
|
||||||
|
@ -104,18 +125,18 @@
|
||||||
</option>
|
</option>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<FormRadios
|
<FormRadios
|
||||||
v-else-if="form[item].type === 'radio'"
|
v-else-if="formItem.type === 'radio'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="item in form[item].options"
|
v-for="item in formItem.options"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
>
|
>
|
||||||
|
@ -123,30 +144,30 @@
|
||||||
</option>
|
</option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
<FormRange
|
<FormRange
|
||||||
v-else-if="form[item].type === 'range'"
|
v-else-if="formItem.type === 'range'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
:min="form[item].min"
|
:min="formItem.min"
|
||||||
:max="form[item].max"
|
:max="formItem.max"
|
||||||
:step="form[item].step"
|
:step="formItem.step"
|
||||||
:text-converter="form[item].textConverter"
|
:text-converter="formItem.textConverter"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormRange>
|
</FormRange>
|
||||||
<MkButton
|
<MkButton
|
||||||
v-else-if="form[item].type === 'button'"
|
v-else-if="formItem.type === 'button'"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
@click="form[item].action($event, values)"
|
@click="formItem.action($event, values)"
|
||||||
>
|
>
|
||||||
<span v-text="form[item].content || item"></span>
|
<span v-text="formItem.content || formItemName"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,8 +175,8 @@
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from "vue";
|
import { ref } from "vue";
|
||||||
import FormInput from "./form/input.vue";
|
import FormInput from "./form/input.vue";
|
||||||
import FormTextarea from "./form/textarea.vue";
|
import FormTextarea from "./form/textarea.vue";
|
||||||
import FormSwitch from "./form/switch.vue";
|
import FormSwitch from "./form/switch.vue";
|
||||||
|
@ -165,59 +186,50 @@ import MkButton from "./MkButton.vue";
|
||||||
import FormRadios from "./form/radios.vue";
|
import FormRadios from "./form/radios.vue";
|
||||||
import XModalWindow from "@/components/MkModalWindow.vue";
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import type { FormItemType } from "@/types/form";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
title: string;
|
||||||
XModalWindow,
|
form: Record<string, FormItemType>;
|
||||||
FormInput,
|
}>();
|
||||||
FormTextarea,
|
|
||||||
FormSwitch,
|
|
||||||
FormSelect,
|
|
||||||
FormRange,
|
|
||||||
MkButton,
|
|
||||||
FormRadios,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
// biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
|
||||||
title: {
|
type ValueType = Record<string, any>;
|
||||||
type: String,
|
|
||||||
required: true,
|
const emit = defineEmits<{
|
||||||
|
done: [
|
||||||
|
status: {
|
||||||
|
result?: Record<string, FormItemType["default"]>;
|
||||||
|
canceled?: true;
|
||||||
},
|
},
|
||||||
form: {
|
];
|
||||||
type: Object,
|
closed: [];
|
||||||
required: true,
|
}>();
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ["done"],
|
const values = ref<ValueType>({});
|
||||||
|
const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
|
||||||
|
|
||||||
data() {
|
for (const item in props.form) {
|
||||||
return {
|
values.value[item] = props.form[item].default ?? null;
|
||||||
values: {},
|
}
|
||||||
i18n,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
function unHiddenForms(): [FormItemType, string][] {
|
||||||
for (const item in this.form) {
|
return Object.keys(props.form)
|
||||||
this.values[item] = this.form[item].default ?? null;
|
.filter((itemName) => !props.form[itemName].hidden)
|
||||||
}
|
.map((itemName) => [props.form[itemName], itemName]);
|
||||||
},
|
}
|
||||||
|
|
||||||
methods: {
|
function ok() {
|
||||||
ok() {
|
emit("done", {
|
||||||
this.$emit("done", {
|
result: values.value,
|
||||||
result: this.values,
|
});
|
||||||
});
|
dialog.value!.close();
|
||||||
this.$refs.dialog.close();
|
}
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
function cancel() {
|
||||||
this.$emit("done", {
|
emit("done", {
|
||||||
canceled: true,
|
canceled: true,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
dialog.value!.close();
|
||||||
},
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -19,11 +19,11 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
compiledFormula(): any {
|
compiledFormula() {
|
||||||
return katex.renderToString(this.formula, {
|
return katex.renderToString(this.formula, {
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
displayMode: this.block,
|
displayMode: this.block,
|
||||||
} as any);
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,10 +2,24 @@
|
||||||
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
|
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<ImgWithBlurhash
|
<ImgWithBlurhash
|
||||||
|
v-if="post.files && post.files.length > 0"
|
||||||
class="img"
|
class="img"
|
||||||
:src="post.files[0].thumbnailUrl"
|
:src="post.files[0].thumbnailUrl"
|
||||||
:hash="post.files[0].blurhash"
|
:hash="post.files[0].blurhash"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="_fullinfo"
|
||||||
|
>
|
||||||
|
<!-- If there is no picture
|
||||||
|
This can happen if the user deletes the image in the drive
|
||||||
|
-->
|
||||||
|
<img
|
||||||
|
src="/static-assets/badges/not-found.webp"
|
||||||
|
class="img"
|
||||||
|
:alt="i18n.ts.notFound"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
|
@ -20,9 +34,11 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
post: any;
|
post: entities.GalleryPost;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
<MkModal
|
<MkModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:z-priority="'middle'"
|
:z-priority="'middle'"
|
||||||
@click="modal.close()"
|
@click="modal!.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<div class="xubzgfga">
|
<div class="xubzgfga">
|
||||||
<header>{{ image.name }}</header>
|
<header>{{ image.name }}</header>
|
||||||
<img
|
<img
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:alt="image.comment"
|
:alt="image.comment || undefined"
|
||||||
:title="image.comment"
|
:title="image.comment || undefined"
|
||||||
@click="modal.close()"
|
@click="modal!.close()"
|
||||||
/>
|
/>
|
||||||
<footer>
|
<footer>
|
||||||
<span>{{ image.type }}</span>
|
<span>{{ image.type }}</span>
|
||||||
|
@ -33,7 +33,7 @@ import bytes from "@/filters/bytes";
|
||||||
import number from "@/filters/number";
|
import number from "@/filters/number";
|
||||||
import MkModal from "@/components/MkModal.vue";
|
import MkModal from "@/components/MkModal.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
image: entities.DriveFile;
|
image: entities.DriveFile;
|
||||||
}>(),
|
}>(),
|
||||||
|
@ -41,10 +41,10 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "closed"): void;
|
closed: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = ref<InstanceType<typeof MkModal> | null>(null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -4,20 +4,20 @@
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
:width="size"
|
:width="size"
|
||||||
:height="size"
|
:height="size"
|
||||||
:title="title"
|
:title="title || undefined"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="src"
|
v-if="src"
|
||||||
:src="src"
|
:src="src"
|
||||||
:title="title"
|
:title="title || undefined"
|
||||||
:type="type"
|
:type="type"
|
||||||
:alt="alt"
|
:alt="alt || undefined"
|
||||||
:class="{
|
:class="{
|
||||||
cover,
|
cover,
|
||||||
wide: largestDimension === 'width',
|
wide: largestDimension === 'width',
|
||||||
tall: largestDimension === 'height',
|
tall: largestDimension === 'height',
|
||||||
}"
|
}"
|
||||||
:style="{ 'object-fit': cover ? 'cover' : null }"
|
:style="{ 'object-fit': cover ? 'cover' : undefined }"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,17 +23,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import * as os from "@/os";
|
|
||||||
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
instance: entities.Instance;
|
instance: entities.Instance;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getInstanceIcon(instance): string {
|
function getInstanceIcon(instance: entities.Instance): string {
|
||||||
return (
|
return (
|
||||||
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
||||||
getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
|
getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
|
||||||
|
|
|
@ -65,14 +65,14 @@ import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "ok", selected: entities.Instance): void;
|
ok: [selected: entities.Instance];
|
||||||
(ev: "cancel"): void;
|
cancel: [];
|
||||||
(ev: "closed"): void;
|
closed: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hostname = ref("");
|
const hostname = ref("");
|
||||||
const instances = ref<entities.Instance[]>([]);
|
const instances = ref<entities.Instance[]>([]);
|
||||||
const selected = ref<entities.Instance | null>();
|
const selected = ref<entities.Instance | null>(null);
|
||||||
const dialogEl = ref<InstanceType<typeof XModalWindow>>();
|
const dialogEl = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
let searchOrderLatch = 0;
|
let searchOrderLatch = 0;
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { i18n } from "@/i18n";
|
||||||
import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue";
|
import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue";
|
||||||
import MkFolder from "@/components/MkFolder.vue";
|
import MkFolder from "@/components/MkFolder.vue";
|
||||||
import { initChart } from "@/scripts/init-chart";
|
import { initChart } from "@/scripts/init-chart";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
|
@ -67,7 +68,18 @@ const { handler: externalTooltipHandler2 } = useChartTooltip({
|
||||||
position: "middle",
|
position: "middle",
|
||||||
});
|
});
|
||||||
|
|
||||||
function createDoughnut(chartEl, tooltip, data) {
|
interface ColorData {
|
||||||
|
name: string;
|
||||||
|
color: string | undefined;
|
||||||
|
value: number;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDoughnut(
|
||||||
|
chartEl: HTMLCanvasElement,
|
||||||
|
tooltip: typeof externalTooltipHandler1,
|
||||||
|
data: ColorData[],
|
||||||
|
) {
|
||||||
const chartInstance = new Chart(chartEl, {
|
const chartInstance = new Chart(chartEl, {
|
||||||
type: "doughnut",
|
type: "doughnut",
|
||||||
data: {
|
data: {
|
||||||
|
@ -96,13 +108,13 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
},
|
},
|
||||||
onClick: (ev) => {
|
onClick: (ev) => {
|
||||||
const hit = chartInstance.getElementsAtEventForMode(
|
const hit = chartInstance.getElementsAtEventForMode(
|
||||||
ev,
|
ev as unknown as Event,
|
||||||
"nearest",
|
"nearest",
|
||||||
{ intersect: true },
|
{ intersect: true },
|
||||||
false,
|
false,
|
||||||
)[0];
|
)[0];
|
||||||
if (hit && data[hit.index].onClick) {
|
if (hit) {
|
||||||
data[hit.index].onClick();
|
data[hit.index].onClick?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -124,48 +136,41 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
return chartInstance;
|
return chartInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function instance2ColorData(x: entities.Instance): ColorData {
|
||||||
|
return {
|
||||||
|
name: x.host,
|
||||||
|
color: x.themeColor || undefined,
|
||||||
|
value: x.followersCount,
|
||||||
|
onClick: () => {
|
||||||
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
|
os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
|
||||||
createDoughnut(
|
createDoughnut(
|
||||||
subDoughnutEl.value,
|
subDoughnutEl.value!,
|
||||||
externalTooltipHandler1,
|
externalTooltipHandler1,
|
||||||
fedStats.topSubInstances
|
fedStats.topSubInstances.map(instance2ColorData).concat([
|
||||||
.map((x) => ({
|
{
|
||||||
name: x.host,
|
name: "(other)",
|
||||||
color: x.themeColor,
|
color: "#80808080",
|
||||||
value: x.followersCount,
|
value: fedStats.otherFollowersCount,
|
||||||
onClick: () => {
|
},
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
]),
|
||||||
},
|
|
||||||
}))
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
name: "(other)",
|
|
||||||
color: "#80808080",
|
|
||||||
value: fedStats.otherFollowersCount,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
createDoughnut(
|
createDoughnut(
|
||||||
pubDoughnutEl.value,
|
pubDoughnutEl.value!,
|
||||||
externalTooltipHandler2,
|
externalTooltipHandler2,
|
||||||
fedStats.topPubInstances
|
fedStats.topPubInstances.map(instance2ColorData).concat([
|
||||||
.map((x) => ({
|
{
|
||||||
name: x.host,
|
name: "(other)",
|
||||||
color: x.themeColor,
|
color: "#80808080",
|
||||||
value: x.followingCount,
|
value: fedStats.otherFollowingCount,
|
||||||
onClick: () => {
|
},
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
]),
|
||||||
},
|
|
||||||
}))
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
name: "(other)",
|
|
||||||
color: "#80808080",
|
|
||||||
value: fedStats.otherFollowingCount,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,27 +20,22 @@ import { ref } from "vue";
|
||||||
import { instanceName, version } from "@/config";
|
import { instanceName, version } from "@/config";
|
||||||
import { instance as Instance } from "@/instance";
|
import { instance as Instance } from "@/instance";
|
||||||
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance?: {
|
instance?: entities.InstanceLite;
|
||||||
faviconUrl?: string;
|
|
||||||
name: string;
|
|
||||||
themeColor?: string;
|
|
||||||
softwareName?: string;
|
|
||||||
softwareVersion?: string;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const ticker = ref<HTMLElement | null>(null);
|
const ticker = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// if no instance data is given, this is for the local instance
|
// if no instance data is given, this is for the local instance
|
||||||
const instance = props.instance ?? {
|
const instance = props.instance ?? {
|
||||||
faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
|
faviconUrl: Instance.iconUrl || "/favicon.ico",
|
||||||
name: instanceName,
|
name: instanceName,
|
||||||
themeColor: (
|
themeColor: (
|
||||||
document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
|
document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
|
||||||
)?.content,
|
)?.content,
|
||||||
softwareName: Instance.softwareName ?? "Firefish",
|
softwareName: "Firefish",
|
||||||
softwareVersion: version,
|
softwareVersion: version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,7 +62,7 @@ const commonNames = new Map<string, string>([
|
||||||
["wxwclub", "wxwClub"],
|
["wxwclub", "wxwClub"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const capitalize = (s: string) => {
|
const capitalize = (s?: string | null) => {
|
||||||
if (s == null) return "Unknown";
|
if (s == null) return "Unknown";
|
||||||
if (commonNames.has(s)) return commonNames.get(s);
|
if (commonNames.has(s)) return commonNames.get(s);
|
||||||
return s[0].toUpperCase() + s.slice(1);
|
return s[0].toUpperCase() + s.slice(1);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:anchor="anchor"
|
:anchor="anchor"
|
||||||
:transparent-bg="true"
|
:transparent-bg="true"
|
||||||
:src="src"
|
:src="src"
|
||||||
@click="modal.close()"
|
@click="modal!.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -73,7 +73,10 @@ import { deviceKind } from "@/scripts/device-kind";
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
anchor?: { x: string; y: string };
|
anchor?: {
|
||||||
|
x: "left" | "center" | "right";
|
||||||
|
y: "top" | "center" | "bottom";
|
||||||
|
};
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
anchor: () => ({ x: "right", y: "center" }),
|
anchor: () => ({ x: "right", y: "center" }),
|
||||||
|
@ -109,7 +112,7 @@ const items = Object.keys(navbarItemDef)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
|
||||||
os.popup(
|
os.popup(
|
||||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||||
{
|
{
|
||||||
showing,
|
showing: showing.value,
|
||||||
url: props.url,
|
url: props.url,
|
||||||
source: el.value,
|
source: el.value,
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const checkAnnouncements = () => {
|
const checkAnnouncements = () => {
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
location.href = "/announcements";
|
location.href = "/announcements";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
:poster="media.thumbnailUrl"
|
:poster="media.thumbnailUrl"
|
||||||
:aria-label="media.comment"
|
:aria-label="media.comment || undefined"
|
||||||
preload="none"
|
preload="none"
|
||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
|
|
|
@ -64,7 +64,7 @@ import "vue-plyr/dist/vue-plyr.css";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
|
||||||
const props = withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
media: entities.DriveFile;
|
media: entities.DriveFile;
|
||||||
}>(),
|
}>(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
|
<MkModal ref="modal" @click="done(true)" @closed="emit('closed')">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="fullwidth top-caption">
|
<div class="fullwidth top-caption">
|
||||||
<div class="mk-dialog">
|
<div class="mk-dialog">
|
||||||
|
@ -48,9 +48,9 @@
|
||||||
<img
|
<img
|
||||||
id="imgtocaption"
|
id="imgtocaption"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:alt="image.comment"
|
:alt="image.comment || undefined"
|
||||||
:title="image.comment"
|
:title="image.comment || undefined"
|
||||||
@click="$refs.modal.close()"
|
@click="modal!.close()"
|
||||||
/>
|
/>
|
||||||
<footer>
|
<footer>
|
||||||
<span>{{ image.type }}</span>
|
<span>{{ image.type }}</span>
|
||||||
|
@ -65,8 +65,8 @@
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
import insertTextAtCursor from "insert-text-at-cursor";
|
import insertTextAtCursor from "insert-text-at-cursor";
|
||||||
import { length } from "stringz";
|
import { length } from "stringz";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
|
@ -76,122 +76,100 @@ import bytes from "@/filters/bytes";
|
||||||
import number from "@/filters/number";
|
import number from "@/filters/number";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { instance } from "@/instance";
|
import { instance } from "@/instance";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(
|
||||||
components: {
|
defineProps<{
|
||||||
MkModal,
|
image: entities.DriveFile;
|
||||||
MkButton,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
image: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
input: {
|
input: {
|
||||||
required: true,
|
placeholder: string;
|
||||||
},
|
default: string;
|
||||||
showOkButton: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showCaptionButton: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showCancelButton: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
cancelableByBgClick: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ["done", "closed"],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
inputValue: this.input.default ? this.input.default : null,
|
|
||||||
i18n,
|
|
||||||
};
|
};
|
||||||
|
title?: string;
|
||||||
|
showOkButton?: boolean;
|
||||||
|
showCaptionButton?: boolean;
|
||||||
|
showCancelButton?: boolean;
|
||||||
|
cancelableByBgClick?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showOkButton: true,
|
||||||
|
showCaptionButton: true,
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelableByBgClick: true,
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
computed: {
|
const emit = defineEmits<{
|
||||||
remainingLength(): number {
|
done: [result: { canceled: boolean; result?: string | null }];
|
||||||
const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
|
closed: [];
|
||||||
if (typeof this.inputValue !== "string") return maxCaptionLength;
|
}>();
|
||||||
return maxCaptionLength - length(this.inputValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
const modal = ref<InstanceType<typeof MkModal> | null>(null);
|
||||||
document.addEventListener("keydown", this.onKeydown);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
const inputValue = ref(props.input.default ? props.input.default : null);
|
||||||
document.removeEventListener("keydown", this.onKeydown);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const remainingLength = computed(() => {
|
||||||
bytes,
|
const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
|
||||||
number,
|
if (typeof inputValue.value !== "string") return maxCaptionLength;
|
||||||
|
return maxCaptionLength - length(inputValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
done(canceled, result?) {
|
function done(canceled: boolean, result?: string | null) {
|
||||||
this.$emit("done", { canceled, result });
|
emit("done", { canceled, result });
|
||||||
this.$refs.modal.close();
|
modal.value!.close();
|
||||||
},
|
}
|
||||||
|
|
||||||
async ok() {
|
async function ok() {
|
||||||
if (!this.showOkButton) return;
|
if (!props.showOkButton) return;
|
||||||
|
|
||||||
const result = this.inputValue;
|
const result = inputValue.value;
|
||||||
this.done(false, result);
|
done(false, result);
|
||||||
},
|
}
|
||||||
|
|
||||||
cancel() {
|
function cancel() {
|
||||||
this.done(true);
|
done(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
onBgClick() {
|
// function onBgClick() {
|
||||||
if (this.cancelableByBgClick) {
|
// if (props.cancelableByBgClick) {
|
||||||
this.cancel();
|
// cancel();
|
||||||
}
|
// }
|
||||||
},
|
// }
|
||||||
|
|
||||||
onKeydown(evt) {
|
function onKeydown(evt) {
|
||||||
if (evt.which === 27) {
|
if (evt.which === 27) {
|
||||||
// ESC
|
// ESC
|
||||||
this.cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onInputKeydown(evt) {
|
function onInputKeydown(evt) {
|
||||||
if (evt.which === 13) {
|
if (evt.which === 13) {
|
||||||
// Enter
|
// Enter
|
||||||
if (evt.ctrlKey) {
|
if (evt.ctrlKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
this.ok();
|
ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
caption() {
|
function caption() {
|
||||||
const img = document.getElementById("imgtocaption") as HTMLImageElement;
|
const img = document.getElementById("imgtocaption") as HTMLImageElement;
|
||||||
const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
|
const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
|
||||||
os.api("drive/files/caption-image", {
|
os.api("drive/files/caption-image", {
|
||||||
url: img.src,
|
url: img.src,
|
||||||
}).then((text) => {
|
}).then((text) => {
|
||||||
insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
|
insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
},
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("keydown", onKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("keydown", onKeydown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
media.type.startsWith('video') ||
|
media.type.startsWith('video') ||
|
||||||
media.type.startsWith('image')
|
media.type.startsWith('image')
|
||||||
"
|
"
|
||||||
:key="media.id"
|
:key="`m-${media.id}`"
|
||||||
:class="{ image: media.type.startsWith('image') }"
|
:class="{ image: media.type.startsWith('image') }"
|
||||||
:data-id="media.id"
|
:data-id="media.id"
|
||||||
:media="media"
|
:media="media"
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
/>
|
/>
|
||||||
<XModPlayer
|
<XModPlayer
|
||||||
v-else-if="isModule(media)"
|
v-else-if="isModule(media)"
|
||||||
:key="media.id"
|
:key="`p-${media.id}`"
|
||||||
:module="media"
|
:module="media"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -48,7 +48,7 @@ import "photoswipe/style.css";
|
||||||
import XBanner from "@/components/MkMediaBanner.vue";
|
import XBanner from "@/components/MkMediaBanner.vue";
|
||||||
import XMedia from "@/components/MkMedia.vue";
|
import XMedia from "@/components/MkMedia.vue";
|
||||||
import XModPlayer from "@/components/MkModPlayer.vue";
|
import XModPlayer from "@/components/MkModPlayer.vue";
|
||||||
import * as os from "@/os";
|
// import * as os from "@/os";
|
||||||
import {
|
import {
|
||||||
FILE_EXT_TRACKER_MODULES,
|
FILE_EXT_TRACKER_MODULES,
|
||||||
FILE_TYPE_BROWSERSAFE,
|
FILE_TYPE_BROWSERSAFE,
|
||||||
|
@ -61,8 +61,8 @@ const props = defineProps<{
|
||||||
inDm?: boolean;
|
inDm?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const gallery = ref(null);
|
const gallery = ref<HTMLElement | null>(null);
|
||||||
const pswpZIndex = os.claimZIndex("middle");
|
// const pswpZIndex = os.claimZIndex("middle");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const lightbox = new PhotoSwipeLightbox({
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
|
@ -79,7 +79,7 @@ onMounted(() => {
|
||||||
src: media.url,
|
src: media.url,
|
||||||
w: media.properties.width,
|
w: media.properties.width,
|
||||||
h: media.properties.height,
|
h: media.properties.height,
|
||||||
alt: media.comment,
|
alt: media.comment || undefined,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
media.properties.orientation != null &&
|
media.properties.orientation != null &&
|
||||||
|
@ -89,7 +89,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}),
|
}),
|
||||||
gallery: gallery.value,
|
gallery: gallery.value || undefined,
|
||||||
children: ".image",
|
children: ".image",
|
||||||
thumbSelector: ".image img",
|
thumbSelector: ".image img",
|
||||||
loop: false,
|
loop: false,
|
||||||
|
@ -119,9 +119,13 @@ onMounted(() => {
|
||||||
// element is children
|
// element is children
|
||||||
const { element } = itemData;
|
const { element } = itemData;
|
||||||
|
|
||||||
|
if (element == null) return;
|
||||||
|
|
||||||
const id = element.dataset.id;
|
const id = element.dataset.id;
|
||||||
const file = props.mediaList.find((media) => media.id === id);
|
const file = props.mediaList.find((media) => media.id === id);
|
||||||
|
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
itemData.src = file.url;
|
itemData.src = file.url;
|
||||||
itemData.w = Number(file.properties.width);
|
itemData.w = Number(file.properties.width);
|
||||||
itemData.h = Number(file.properties.height);
|
itemData.h = Number(file.properties.height);
|
||||||
|
@ -132,12 +136,12 @@ onMounted(() => {
|
||||||
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
||||||
}
|
}
|
||||||
itemData.msrc = file.thumbnailUrl;
|
itemData.msrc = file.thumbnailUrl;
|
||||||
itemData.alt = file.comment;
|
itemData.alt = file.comment || undefined;
|
||||||
itemData.thumbCropped = true;
|
itemData.thumbCropped = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on("uiRegister", () => {
|
lightbox.on("uiRegister", () => {
|
||||||
lightbox.pswp.ui.registerElement({
|
lightbox.pswp?.ui?.registerElement({
|
||||||
name: "altText",
|
name: "altText",
|
||||||
className: "pwsp__alt-text-container",
|
className: "pwsp__alt-text-container",
|
||||||
appendTo: "wrapper",
|
appendTo: "wrapper",
|
||||||
|
@ -146,7 +150,7 @@ onMounted(() => {
|
||||||
textBox.className = "pwsp__alt-text";
|
textBox.className = "pwsp__alt-text";
|
||||||
el.appendChild(textBox);
|
el.appendChild(textBox);
|
||||||
|
|
||||||
const preventProp = function (ev: Event): void {
|
const preventProp = (ev: Event): void => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,7 +162,7 @@ onMounted(() => {
|
||||||
el.onpointermove = preventProp;
|
el.onpointermove = preventProp;
|
||||||
|
|
||||||
pwsp.on("change", () => {
|
pwsp.on("change", () => {
|
||||||
textBox.textContent = pwsp.currSlide.data.alt?.trim();
|
textBox.textContent = pwsp.currSlide?.data.alt?.trim() ?? null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -168,7 +172,7 @@ onMounted(() => {
|
||||||
history.pushState(null, "", location.href);
|
history.pushState(null, "", location.href);
|
||||||
addEventListener("popstate", close);
|
addEventListener("popstate", close);
|
||||||
// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
|
// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
|
||||||
lightbox.pswp.element.focus();
|
lightbox.pswp?.element?.focus();
|
||||||
});
|
});
|
||||||
lightbox.on("close", () => {
|
lightbox.on("close", () => {
|
||||||
removeEventListener("popstate", close);
|
removeEventListener("popstate", close);
|
||||||
|
@ -180,7 +184,7 @@ onMounted(() => {
|
||||||
function close() {
|
function close() {
|
||||||
removeEventListener("popstate", close);
|
removeEventListener("popstate", close);
|
||||||
history.forward();
|
history.forward();
|
||||||
lightbox.pswp.close();
|
lightbox.pswp?.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => {
|
||||||
return (
|
return (
|
||||||
FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
|
FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
|
||||||
FILE_EXT_TRACKER_MODULES.some((ext) => {
|
FILE_EXT_TRACKER_MODULES.some((ext) => {
|
||||||
return file.name.toLowerCase().endsWith("." + ext);
|
return file.name.toLowerCase().endsWith(`.${ext}`);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
:href="url"
|
:href="url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
:style="{ background: bgCss }"
|
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<span class="main">
|
<span class="main">
|
||||||
|
@ -54,7 +53,7 @@ const url = `/${canonical}`;
|
||||||
const isMe =
|
const isMe =
|
||||||
isSignedIn &&
|
isSignedIn &&
|
||||||
`@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
|
`@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
|
||||||
`@${me.username}@${toUnicode(localHost)}`.toLowerCase();
|
`@${me!.username}@${toUnicode(localHost)}`.toLowerCase();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -37,8 +37,8 @@ function setPosition() {
|
||||||
const rect = props.targetElement.getBoundingClientRect();
|
const rect = props.targetElement.getBoundingClientRect();
|
||||||
const left = props.targetElement.offsetWidth;
|
const left = props.targetElement.offsetWidth;
|
||||||
const top = rect.top - rootRect.top - 8;
|
const top = rect.top - rootRect.top - 8;
|
||||||
el.value.style.left = left + "px";
|
el.value!.style.left = `${left}px`;
|
||||||
el.value.style.top = top + "px";
|
el.value!.style.top = `${top}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChildClosed(actioned?: boolean) {
|
function onChildClosed(actioned?: boolean) {
|
||||||
|
@ -58,7 +58,7 @@ onMounted(() => {
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
checkHit: (ev: MouseEvent) => {
|
checkHit: (ev: MouseEvent) => {
|
||||||
return ev.target === el.value || el.value.contains(ev.target);
|
return ev.target === el.value || el.value?.contains(ev.target as Node);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -89,7 +89,8 @@
|
||||||
></span>
|
></span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'user' && !items.hidden"
|
v-else-if="item.type === 'user'"
|
||||||
|
v-show="!item.hidden"
|
||||||
class="_button item"
|
class="_button item"
|
||||||
:class="{ active: item.active }"
|
:class="{ active: item.active }"
|
||||||
:disabled="item.active"
|
:disabled="item.active"
|
||||||
|
@ -201,6 +202,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
|
type Ref,
|
||||||
defineAsyncComponent,
|
defineAsyncComponent,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
@ -213,6 +215,7 @@ import type {
|
||||||
InnerMenuItem,
|
InnerMenuItem,
|
||||||
MenuAction,
|
MenuAction,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
MenuParent,
|
||||||
MenuPending,
|
MenuPending,
|
||||||
} from "@/types/menu";
|
} from "@/types/menu";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
|
@ -234,21 +237,29 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "close", actioned?: boolean): void;
|
close: [actioned?: boolean];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const itemsEl = ref<HTMLDivElement>();
|
const itemsEl = ref<HTMLDivElement>();
|
||||||
|
|
||||||
const items2: InnerMenuItem[] = ref([]);
|
/**
|
||||||
|
* Strictly speaking, this type conversion is wrong
|
||||||
|
* because `ref` will deeply unpack the `ref` in `MenuSwitch`.
|
||||||
|
* But it performs correctly, so who cares?
|
||||||
|
*/
|
||||||
|
const items2 = ref([]) as Ref<InnerMenuItem[]>;
|
||||||
|
|
||||||
const child = ref<InstanceType<typeof XChild>>();
|
const child = ref<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
const childShowingItem = ref<MenuItem | null>();
|
const childShowingItem = ref<MenuItem | null>();
|
||||||
|
|
||||||
|
// FIXME: this is not used
|
||||||
|
const isActive = ref();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.items,
|
() => props.items,
|
||||||
() => {
|
() => {
|
||||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(
|
const items: (MenuItem | MenuPending)[] = props.items.filter(
|
||||||
(item) => item !== undefined,
|
(item) => item !== undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -288,29 +299,29 @@ function onGlobalMousedown(event: MouseEvent) {
|
||||||
if (
|
if (
|
||||||
childTarget.value &&
|
childTarget.value &&
|
||||||
(event.target === childTarget.value ||
|
(event.target === childTarget.value ||
|
||||||
childTarget.value.contains(event.target))
|
childTarget.value.contains(event.target as Node))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (child.value && child.value.checkHit(event)) return;
|
if (child.value?.checkHit(event)) return;
|
||||||
closeChild();
|
closeChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
let childCloseTimer: null | number = null;
|
let childCloseTimer: null | number = null;
|
||||||
function onItemMouseEnter(item) {
|
function onItemMouseEnter(_item) {
|
||||||
childCloseTimer = window.setTimeout(() => {
|
childCloseTimer = window.setTimeout(() => {
|
||||||
closeChild();
|
closeChild();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
function onItemMouseLeave(item) {
|
function onItemMouseLeave(_item) {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildren(item: MenuItem, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
if (props.asDrawer) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(item.children, ev.currentTarget ?? ev.target);
|
os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
|
||||||
close();
|
close();
|
||||||
} else {
|
} else {
|
||||||
childTarget.value = ev.currentTarget ?? ev.target;
|
childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
|
||||||
childMenu.value = item.children;
|
childMenu.value = item.children;
|
||||||
childShowingItem.value = item;
|
childShowingItem.value = item;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
:stroke="color"
|
:stroke="color"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
<circle :cx="headX" :cy="headY" r="3" :fill="color" />
|
<circle :cx="headX ?? undefined" :cy="headY ?? undefined" r="3" :fill="color" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ const patternShow = ref(false);
|
||||||
const modPattern = ref<HTMLDivElement>();
|
const modPattern = ref<HTMLDivElement>();
|
||||||
const progress = ref<typeof FormRange>();
|
const progress = ref<typeof FormRange>();
|
||||||
const position = ref(0);
|
const position = ref(0);
|
||||||
const patData = shallowRef([] as ModRow[][]);
|
const patData = shallowRef<readonly ModRow[][]>([]);
|
||||||
const currentPattern = ref(0);
|
const currentPattern = ref(0);
|
||||||
const nbChannels = ref(0);
|
const nbChannels = ref(0);
|
||||||
const length = ref(1);
|
const length = ref(1);
|
||||||
|
@ -159,7 +159,7 @@ function load() {
|
||||||
error.value = false;
|
error.value = false;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
})
|
})
|
||||||
.catch((e: any) => {
|
.catch((e: unknown) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
error.value = true;
|
error.value = true;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
|
@ -293,12 +293,13 @@ function isRowActive(i: number) {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexText(i: number) {
|
function indexText(i: number) {
|
||||||
let rowText = i.toString(16);
|
let rowText = i.toString(16);
|
||||||
if (rowText.length === 1) {
|
if (rowText.length === 1) {
|
||||||
rowText = "0" + rowText;
|
rowText = `0${rowText}`;
|
||||||
}
|
}
|
||||||
return rowText;
|
return rowText;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,8 +108,11 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
anchor?: { x: string; y: string };
|
anchor?: {
|
||||||
src?: HTMLElement;
|
x: "left" | "center" | "right";
|
||||||
|
y: "top" | "center" | "bottom";
|
||||||
|
};
|
||||||
|
src?: HTMLElement | null;
|
||||||
preferType?: ModalTypes | "auto";
|
preferType?: ModalTypes | "auto";
|
||||||
zPriority?: "low" | "middle" | "high";
|
zPriority?: "low" | "middle" | "high";
|
||||||
noOverlap?: boolean;
|
noOverlap?: boolean;
|
||||||
|
@ -118,7 +121,7 @@ const props = withDefaults(
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
src: undefined,
|
src: null,
|
||||||
anchor: () => ({ x: "center", y: "bottom" }),
|
anchor: () => ({ x: "center", y: "bottom" }),
|
||||||
preferType: "auto",
|
preferType: "auto",
|
||||||
zPriority: "low",
|
zPriority: "low",
|
||||||
|
@ -139,6 +142,9 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
provide("modal", true);
|
provide("modal", true);
|
||||||
|
|
||||||
|
// FIXME: this may not used
|
||||||
|
const isActive = ref();
|
||||||
|
|
||||||
const maxHeight = ref<number>();
|
const maxHeight = ref<number>();
|
||||||
const fixed = ref(false);
|
const fixed = ref(false);
|
||||||
const transformOrigin = ref("center");
|
const transformOrigin = ref("center");
|
||||||
|
@ -189,8 +195,8 @@ const transitionDuration = computed(() =>
|
||||||
|
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
const focusedElement = document.activeElement;
|
const focusedElement = document.activeElement as HTMLElement;
|
||||||
function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
|
function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
|
||||||
// removeEventListener("popstate", close);
|
// removeEventListener("popstate", close);
|
||||||
// if (props.preferType == "dialog") {
|
// if (props.preferType == "dialog") {
|
||||||
// history.forward();
|
// history.forward();
|
||||||
|
@ -204,7 +210,7 @@ function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
|
||||||
showing.value = false;
|
showing.value = false;
|
||||||
emit("close");
|
emit("close");
|
||||||
if (!props.noReturnFocus) {
|
if (!props.noReturnFocus) {
|
||||||
focusedElement.focus();
|
focusedElement?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,8 +241,8 @@ const align = () => {
|
||||||
const width = content.value!.offsetWidth;
|
const width = content.value!.offsetWidth;
|
||||||
const height = content.value!.offsetHeight;
|
const height = content.value!.offsetHeight;
|
||||||
|
|
||||||
let left: number;
|
let left = 0;
|
||||||
let top: number;
|
let top = MARGIN;
|
||||||
|
|
||||||
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
|
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
|
||||||
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
|
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<button
|
<button
|
||||||
class="_button"
|
class="_button"
|
||||||
:aria-label="i18n.ts.close"
|
:aria-label="i18n.ts.close"
|
||||||
@click="$refs.modal.close()"
|
@click="modal!.close()"
|
||||||
>
|
>
|
||||||
<i :class="icon('ph-x')"></i>
|
<i :class="icon('ph-x')"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -65,6 +65,7 @@ import type { PageMetadata } from "@/scripts/page-metadata";
|
||||||
import { provideMetadataReceiver } from "@/scripts/page-metadata";
|
import { provideMetadataReceiver } from "@/scripts/page-metadata";
|
||||||
import { Router } from "@/nirax";
|
import { Router } from "@/nirax";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { MenuItem } from "@/types/menu";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
|
@ -81,11 +82,11 @@ router.addListener("push", (ctx) => {});
|
||||||
|
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
const rootEl = ref();
|
const rootEl = ref();
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = ref<InstanceType<typeof MkModal> | null>(null);
|
||||||
const path = ref(props.initialPath);
|
const path = ref(props.initialPath);
|
||||||
const width = ref(860);
|
const width = ref(860);
|
||||||
const height = ref(660);
|
const height = ref(660);
|
||||||
const history = [];
|
const history: string[] = [];
|
||||||
|
|
||||||
provide("router", router);
|
provide("router", router);
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
|
@ -95,7 +96,7 @@ provide("shouldOmitHeaderTitle", true);
|
||||||
provide("shouldHeaderThin", true);
|
provide("shouldHeaderThin", true);
|
||||||
|
|
||||||
const pageUrl = computed(() => url + path.value);
|
const pageUrl = computed(() => url + path.value);
|
||||||
const contextmenu = computed(() => {
|
const contextmenu = computed((): MenuItem[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "label",
|
type: "label",
|
||||||
|
@ -117,7 +118,7 @@ const contextmenu = computed(() => {
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(pageUrl.value, "_blank");
|
window.open(pageUrl.value, "_blank");
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -130,23 +131,26 @@ const contextmenu = computed(() => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
function navigate(path, record = true) {
|
function navigate(path: string, record = true) {
|
||||||
if (record) history.push(router.getCurrentPath());
|
if (record) history.push(router.getCurrentPath());
|
||||||
router.push(path);
|
router.push(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
navigate(history.pop(), false);
|
const backTo = history.pop();
|
||||||
|
if (backTo) {
|
||||||
|
navigate(backTo, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
mainRouter.push(path.value);
|
mainRouter.push(path.value);
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function popout() {
|
function popout() {
|
||||||
_popout(path.value, rootEl.value);
|
_popout(path.value, rootEl.value);
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent) {
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
height: scroll
|
height: scroll
|
||||||
? height
|
? height
|
||||||
? `${props.height}px`
|
? `${props.height}px`
|
||||||
: null
|
: undefined
|
||||||
: height
|
: height
|
||||||
? `min(${props.height}px, 100%)`
|
? `min(${props.height}px, 100%)`
|
||||||
: '100%',
|
: '100%',
|
||||||
|
@ -54,7 +54,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<slot></slot>
|
<slot
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
@ -62,7 +65,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef } from "vue";
|
import { ref, shallowRef } from "vue";
|
||||||
|
|
||||||
import { FocusTrap } from "focus-trap-vue";
|
import { FocusTrap } from "focus-trap-vue";
|
||||||
import MkModal from "./MkModal.vue";
|
import MkModal from "./MkModal.vue";
|
||||||
|
@ -93,11 +96,14 @@ const emit = defineEmits<{
|
||||||
(event: "ok"): void;
|
(event: "ok"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// FIXME: seems that this is not used
|
||||||
|
const isActive = ref();
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const rootEl = shallowRef<HTMLElement>();
|
const rootEl = shallowRef<HTMLElement>();
|
||||||
const headerEl = shallowRef<HTMLElement>();
|
const headerEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const close = (ev) => {
|
const close = (ev?) => {
|
||||||
modal.value?.close(ev);
|
modal.value?.close(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
v-vibrate="5"
|
v-vibrate="5"
|
||||||
:aria-label="accessibleLabel"
|
:aria-label="accessibleLabel"
|
||||||
class="tkcbzcuz note-container"
|
class="tkcbzcuz note-container"
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : undefined"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
>
|
>
|
||||||
<MkNoteSub
|
<MkNoteSub
|
||||||
|
@ -112,9 +112,9 @@
|
||||||
:note="appearNote"
|
:note="appearNote"
|
||||||
:detailed="true"
|
:detailed="true"
|
||||||
:detailed-view="detailedView"
|
:detailed-view="detailedView"
|
||||||
:parent-id="appearNote.parentId"
|
:parent-id="appearNote.id"
|
||||||
@push="(e) => router.push(notePage(e))"
|
@push="(e) => router.push(notePage(e))"
|
||||||
@focusfooter="footerEl.focus()"
|
@focusfooter="footerEl!.focus()"
|
||||||
@expanded="(e) => setPostExpanded(e)"
|
@expanded="(e) => setPostExpanded(e)"
|
||||||
></MkSubNoteContent>
|
></MkSubNoteContent>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
|
@ -312,11 +312,17 @@ import { notePage } from "@/filters/note";
|
||||||
import { deepClone } from "@/scripts/clone";
|
import { deepClone } from "@/scripts/clone";
|
||||||
import { getNoteSummary } from "@/scripts/get-note-summary";
|
import { getNoteSummary } from "@/scripts/get-note-summary";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { NoteTranslation } from "@/types/note";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
type NoteType = entities.Note & {
|
||||||
|
_featuredId_?: string;
|
||||||
|
_prId_?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: entities.Note;
|
note: NoteType;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
detailedView?: boolean;
|
detailedView?: boolean;
|
||||||
collapsedReply?: boolean;
|
collapsedReply?: boolean;
|
||||||
|
@ -354,18 +360,18 @@ const isRenote =
|
||||||
note.value.fileIds.length === 0 &&
|
note.value.fileIds.length === 0 &&
|
||||||
note.value.poll == null;
|
note.value.poll == null;
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement | null>(null);
|
||||||
const footerEl = ref<HTMLElement>();
|
const footerEl = ref<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
|
||||||
const renoteTime = ref<HTMLElement>();
|
const renoteTime = ref<HTMLElement>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement | null>(null);
|
||||||
const appearNote = computed(() =>
|
const appearNote = computed(() =>
|
||||||
isRenote ? (note.value.renote as entities.Note) : note.value,
|
isRenote ? (note.value.renote as NoteType) : note.value,
|
||||||
);
|
);
|
||||||
const isMyRenote = isSignedIn && me.id === note.value.userId;
|
const isMyRenote = isSignedIn && me!.id === note.value.userId;
|
||||||
const showContent = ref(false);
|
// const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(
|
const muted = ref(
|
||||||
getWordSoftMute(
|
getWordSoftMute(
|
||||||
|
@ -375,7 +381,7 @@ const muted = ref(
|
||||||
defaultStore.state.mutedLangs,
|
defaultStore.state.mutedLangs,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const translation = ref(null);
|
const translation = ref<NoteTranslation | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
||||||
|
@ -391,7 +397,7 @@ const isForeignLanguage: boolean =
|
||||||
return postLang !== "" && postLang !== targetLang;
|
return postLang !== "" && postLang !== targetLang;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function translate_(noteId, targetLang: string) {
|
async function translate_(noteId: string, targetLang: string) {
|
||||||
return await os.api("notes/translate", {
|
return await os.api("notes/translate", {
|
||||||
noteId,
|
noteId,
|
||||||
targetLang,
|
targetLang,
|
||||||
|
@ -421,12 +427,13 @@ async function translate() {
|
||||||
const keymap = {
|
const keymap = {
|
||||||
r: () => reply(true),
|
r: () => reply(true),
|
||||||
"e|a|plus": () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
q: () => renoteButton.value.renote(true),
|
q: () => renoteButton.value!.renote(true),
|
||||||
"up|k": focusBefore,
|
"up|k": focusBefore,
|
||||||
"down|j": focusAfter,
|
"down|j": focusAfter,
|
||||||
esc: blur,
|
esc: blur,
|
||||||
"m|o": () => menu(true),
|
"m|o": () => menu(true),
|
||||||
s: () => showContent.value !== showContent.value,
|
// FIXME: What's this?
|
||||||
|
// s: () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (appearNote.value.historyId == null) {
|
if (appearNote.value.historyId == null) {
|
||||||
|
@ -437,12 +444,12 @@ if (appearNote.value.historyId == null) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post(
|
os.post(
|
||||||
{
|
{
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
animation: !viaKeyboard,
|
// animation: !viaKeyboard,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
focus();
|
focus();
|
||||||
|
@ -450,11 +457,11 @@ function reply(viaKeyboard = false): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(
|
reactionPicker.show(
|
||||||
reactButton.value,
|
reactButton.value!,
|
||||||
(reaction) => {
|
(reaction) => {
|
||||||
os.api("notes/reactions/create", {
|
os.api("notes/reactions/create", {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.value.id,
|
||||||
|
@ -467,7 +474,7 @@ function react(viaKeyboard = false): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
function undoReact(note: NoteType): void {
|
||||||
const oldReaction = note.myReaction;
|
const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
os.api("notes/reactions/delete", {
|
os.api("notes/reactions/delete", {
|
||||||
|
@ -481,16 +488,17 @@ const currentClipPage = inject<Ref<entities.Clip> | null>(
|
||||||
);
|
);
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement): boolean => {
|
||||||
if (el.tagName === "A") return true;
|
if (el.tagName === "A") return true;
|
||||||
// The Audio element's context menu is the browser default, such as for selecting playback speed.
|
// The Audio element's context menu is the browser default, such as for selecting playback speed.
|
||||||
if (el.tagName === "AUDIO") return true;
|
if (el.tagName === "AUDIO") return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target as HTMLElement)) return;
|
||||||
if (window.getSelection().toString() !== "") return;
|
if (window.getSelection()?.toString() !== "") return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -509,7 +517,7 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
os.pageWindow(notePage(appearNote.value));
|
os.pageWindow(notePage(appearNote.value));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notePage(appearNote.value) != location.pathname
|
notePage(appearNote.value) !== location.pathname
|
||||||
? {
|
? {
|
||||||
icon: `${icon("ph-arrows-out-simple")}`,
|
icon: `${icon("ph-arrows-out-simple")}`,
|
||||||
text: i18n.ts.showInPage,
|
text: i18n.ts.showInPage,
|
||||||
|
@ -589,11 +597,11 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
el.value!.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
el.value!.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusBefore() {
|
function focusBefore() {
|
||||||
|
@ -605,12 +613,12 @@ function focusAfter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollIntoView() {
|
function scrollIntoView() {
|
||||||
el.value.scrollIntoView();
|
el.value!.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteClick(e) {
|
function noteClick(e) {
|
||||||
if (
|
if (
|
||||||
document.getSelection().type === "Range" ||
|
document.getSelection()?.type === "Range" ||
|
||||||
props.detailedView ||
|
props.detailedView ||
|
||||||
!expandOnNoteClick
|
!expandOnNoteClick
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
v-size="{ max: [500, 350, 300] }"
|
v-size="{ max: [500, 350, 300] }"
|
||||||
class="lxwezrsl _block"
|
class="lxwezrsl _block"
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : undefined"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
>
|
>
|
||||||
<MkNoteSub
|
<MkNoteSub
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</option>
|
</option>
|
||||||
<option v-if="directQuotes?.length > 0" value="quotes">
|
<option v-if="directQuotes && directQuotes.length > 0" value="quotes">
|
||||||
<!-- <i :class="icon('ph-quotes')"></i> -->
|
<!-- <i :class="icon('ph-quotes')"></i> -->
|
||||||
{{
|
{{
|
||||||
wordWithCount(
|
wordWithCount(
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
:detailed-view="true"
|
:detailed-view="true"
|
||||||
:parent-id="note.id"
|
:parent-id="note.id"
|
||||||
/>
|
/>
|
||||||
<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" />
|
<MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" />
|
||||||
|
|
||||||
<!-- <MkPagination
|
<!-- <MkPagination
|
||||||
v-if="tab === 'renotes'"
|
v-if="tab === 'renotes'"
|
||||||
|
@ -225,12 +225,12 @@ if (noteViewInterruptors.length > 0) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement | null>(null);
|
||||||
const noteEl = ref();
|
const noteEl = ref();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement>();
|
||||||
const showContent = ref(false);
|
// const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(
|
const muted = ref(
|
||||||
getWordSoftMute(
|
getWordSoftMute(
|
||||||
|
@ -248,7 +248,8 @@ const directReplies = ref<null | entities.Note[]>([]);
|
||||||
const directQuotes = ref<null | entities.Note[]>([]);
|
const directQuotes = ref<null | entities.Note[]>([]);
|
||||||
const clips = ref();
|
const clips = ref();
|
||||||
const renotes = ref();
|
const renotes = ref();
|
||||||
let isScrolling;
|
const isRenote = ref(note.value.renoteId != null);
|
||||||
|
let isScrolling: boolean;
|
||||||
|
|
||||||
const reactionsCount = Object.values(props.note.reactions).reduce(
|
const reactionsCount = Object.values(props.note.reactions).reduce(
|
||||||
(x, y) => x + y,
|
(x, y) => x + y,
|
||||||
|
@ -258,10 +259,10 @@ const reactionsCount = Object.values(props.note.reactions).reduce(
|
||||||
const keymap = {
|
const keymap = {
|
||||||
r: () => reply(true),
|
r: () => reply(true),
|
||||||
"e|a|plus": () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
q: () => renoteButton.value.renote(true),
|
q: () => renoteButton.value!.renote(true),
|
||||||
esc: blur,
|
esc: blur,
|
||||||
"m|o": () => menu(true),
|
"m|o": () => menu(true),
|
||||||
s: () => showContent.value !== showContent.value,
|
// s: () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
|
@ -270,21 +271,21 @@ useNoteCapture({
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post({
|
||||||
reply: note.value,
|
reply: note.value,
|
||||||
animation: !viaKeyboard,
|
// animation: !viaKeyboard,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(
|
reactionPicker.show(
|
||||||
reactButton.value,
|
reactButton.value!,
|
||||||
(reaction) => {
|
(reaction) => {
|
||||||
os.api("notes/reactions/create", {
|
os.api("notes/reactions/create", {
|
||||||
noteId: note.value.id,
|
noteId: note.value.id,
|
||||||
|
@ -297,13 +298,13 @@ function react(viaKeyboard = false): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
// function undoReact(note): void {
|
||||||
const oldReaction = note.myReaction;
|
// const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
// if (!oldReaction) return;
|
||||||
os.api("notes/reactions/delete", {
|
// os.api("notes/reactions/delete", {
|
||||||
noteId: note.id,
|
// noteId: note.id,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement) => {
|
||||||
|
@ -312,8 +313,8 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target as HTMLElement)) return;
|
||||||
if (window.getSelection().toString() !== "") return;
|
if (window.getSelection()?.toString() !== "") return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -362,12 +363,17 @@ os.api("notes/children", {
|
||||||
limit: 30,
|
limit: 30,
|
||||||
depth: 12,
|
depth: 12,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
res = res.reduce((acc, resNote) => {
|
// biome-ignore lint/style/noParameterAssign: assign it intentially
|
||||||
if (resNote.userId == note.value.userId) {
|
res = res
|
||||||
return [...acc, resNote];
|
.filter((n) => n.userId !== note.value.userId)
|
||||||
}
|
.reverse()
|
||||||
return [resNote, ...acc];
|
.concat(res.filter((n) => n.userId === note.value.userId));
|
||||||
}, []);
|
// res = res.reduce((acc: entities.Note[], resNote) => {
|
||||||
|
// if (resNote.userId === note.value.userId) {
|
||||||
|
// return [...acc, resNote];
|
||||||
|
// }
|
||||||
|
// return [resNote, ...acc];
|
||||||
|
// }, []);
|
||||||
replies.value = res;
|
replies.value = res;
|
||||||
directReplies.value = res
|
directReplies.value = res
|
||||||
.filter((resNote) => resNote.replyId === note.value.id)
|
.filter((resNote) => resNote.replyId === note.value.id)
|
||||||
|
@ -438,7 +444,7 @@ async function onNoteUpdated(
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "replied":
|
case "replied": {
|
||||||
const { id: createdId } = body;
|
const { id: createdId } = body;
|
||||||
const replyNote = await os.api("notes/show", {
|
const replyNote = await os.api("notes/show", {
|
||||||
noteId: createdId,
|
noteId: createdId,
|
||||||
|
@ -446,10 +452,10 @@ async function onNoteUpdated(
|
||||||
|
|
||||||
replies.value.splice(found, 0, replyNote);
|
replies.value.splice(found, 0, replyNote);
|
||||||
if (found === 0) {
|
if (found === 0) {
|
||||||
directReplies.value.push(replyNote);
|
directReplies.value!.push(replyNote);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "deleted":
|
case "deleted":
|
||||||
if (found === 0) {
|
if (found === 0) {
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ min: [350, 500] }" class="fefdfafb">
|
<div v-size="{ min: [350, 500] }" class="fefdfafb">
|
||||||
<MkAvatar class="avatar" :user="me" disable-link />
|
<MkAvatar class="avatar" :user="me!" disable-link />
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<MkUserName :user="me" />
|
<MkUserName :user="me!" />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<article
|
<article
|
||||||
v-if="!muted.muted || muted.what === 'reply'"
|
v-if="!muted.muted || muted.what === 'reply'"
|
||||||
:id="detailedView ? appearNote.id : null"
|
:id="detailedView ? appearNote.id : undefined"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-size="{ max: [450, 500] }"
|
v-size="{ max: [450, 500] }"
|
||||||
class="wrpstxzv"
|
class="wrpstxzv"
|
||||||
|
@ -35,10 +35,10 @@
|
||||||
:parent-id="parentId"
|
:parent-id="parentId"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
:detailed-view="detailedView"
|
:detailed-view="detailedView"
|
||||||
@focusfooter="footerEl.focus()"
|
@focusfooter="footerEl!.focus()"
|
||||||
/>
|
/>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating || translation == null" mini />
|
||||||
<div v-else class="translated">
|
<div v-else class="translated">
|
||||||
<b
|
<b
|
||||||
>{{
|
>{{
|
||||||
|
@ -217,6 +217,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import { deepClone } from "@/scripts/clone";
|
import { deepClone } from "@/scripts/clone";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { NoteTranslation } from "@/types/note";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -256,12 +257,12 @@ const isRenote =
|
||||||
note.value.fileIds.length === 0 &&
|
note.value.fileIds.length === 0 &&
|
||||||
note.value.poll == null;
|
note.value.poll == null;
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement | null>(null);
|
||||||
const footerEl = ref<HTMLElement>();
|
const footerEl = ref<HTMLElement | null>(null);
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton> | null>(null);
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement | null>(null);
|
||||||
const appearNote = computed(() =>
|
const appearNote = computed(() =>
|
||||||
isRenote ? (note.value.renote as entities.Note) : note.value,
|
isRenote ? (note.value.renote as entities.Note) : note.value,
|
||||||
);
|
);
|
||||||
|
@ -274,7 +275,7 @@ const muted = ref(
|
||||||
defaultStore.state.mutedLangs,
|
defaultStore.state.mutedLangs,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const translation = ref(null);
|
const translation = ref<NoteTranslation | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const replies: entities.Note[] =
|
const replies: entities.Note[] =
|
||||||
props.conversation
|
props.conversation
|
||||||
|
@ -330,21 +331,21 @@ useNoteCapture({
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
animation: !viaKeyboard,
|
// animation: !viaKeyboard,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(_viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(
|
reactionPicker.show(
|
||||||
reactButton.value,
|
reactButton.value!,
|
||||||
(reaction) => {
|
(reaction) => {
|
||||||
os.api("notes/reactions/create", {
|
os.api("notes/reactions/create", {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.value.id,
|
||||||
|
@ -388,14 +389,15 @@ function menu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement | null) => {
|
||||||
|
if (el == null) return;
|
||||||
if (el.tagName === "A") return true;
|
if (el.tagName === "A") return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target as HTMLElement | null)) return;
|
||||||
if (window.getSelection().toString() !== "") return;
|
if (window.getSelection()?.toString() !== "") return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -414,7 +416,7 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
os.pageWindow(notePage(appearNote.value));
|
os.pageWindow(notePage(appearNote.value));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notePage(appearNote.value) != location.pathname
|
notePage(appearNote.value) !== location.pathname
|
||||||
? {
|
? {
|
||||||
icon: `${icon("ph-arrows-out-simple")}`,
|
icon: `${icon("ph-arrows-out-simple")}`,
|
||||||
text: i18n.ts.showInPage,
|
text: i18n.ts.showInPage,
|
||||||
|
@ -454,15 +456,15 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
el.value!.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
el.value!.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteClick(e) {
|
function noteClick(e: MouseEvent) {
|
||||||
if (document.getSelection().type === "Range" || !expandOnNoteClick) {
|
if (document.getSelection()?.type === "Range" || !expandOnNoteClick) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
} else {
|
} else {
|
||||||
router.push(notePage(props.note));
|
router.push(notePage(props.note));
|
||||||
|
|
|
@ -40,8 +40,12 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import type {
|
||||||
|
MkPaginationType,
|
||||||
|
PagingKeyOf,
|
||||||
|
PagingOf,
|
||||||
|
} from "@/components/MkPagination.vue";
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import type { PagingOf } from "@/components/MkPagination.vue";
|
|
||||||
import XNote from "@/components/MkNote.vue";
|
import XNote from "@/components/MkNote.vue";
|
||||||
import XList from "@/components/MkDateSeparatedList.vue";
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
|
@ -56,10 +60,14 @@ defineProps<{
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = ref<MkPaginationType<
|
||||||
|
PagingKeyOf<entities.Note>
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
function scrollTop() {
|
function scrollTop() {
|
||||||
scroll(tlEl.value, { top: 0, behavior: "smooth" });
|
if (tlEl.value) {
|
||||||
|
scroll(tlEl.value, { top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
:user="notification.note.user"
|
:user="notification.note.user"
|
||||||
/>
|
/>
|
||||||
<MkAvatar
|
<MkAvatar
|
||||||
v-else-if="notification.user"
|
v-else-if="'user' in notification"
|
||||||
class="icon"
|
class="icon"
|
||||||
:user="notification.user"
|
:user="notification.user"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else-if="notification.icon"
|
v-else-if="'icon' in notification && notification.icon"
|
||||||
class="icon"
|
class="icon"
|
||||||
:src="notification.icon"
|
:src="notification.icon"
|
||||||
alt=""
|
alt=""
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
i18n.ts._notification.pollEnded
|
i18n.ts._notification.pollEnded
|
||||||
}}</span>
|
}}</span>
|
||||||
<MkA
|
<MkA
|
||||||
v-else-if="notification.user"
|
v-else-if="'user' in notification"
|
||||||
v-user-preview="notification.user.id"
|
v-user-preview="notification.user.id"
|
||||||
class="name"
|
class="name"
|
||||||
:to="userPage(notification.user)"
|
:to="userPage(notification.user)"
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
:plain="true"
|
:plain="true"
|
||||||
:nowrap="!full"
|
:nowrap="!full"
|
||||||
:lang="notification.note.lang"
|
:lang="notification.note.lang"
|
||||||
:custom-emojis="notification.note.renote.emojis"
|
:custom-emojis="notification.note.renote!.emojis"
|
||||||
/>
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA
|
<MkA
|
||||||
|
@ -212,6 +212,7 @@
|
||||||
style="opacity: 0.7"
|
style="opacity: 0.7"
|
||||||
>{{ i18n.ts.youGotNewFollower }}
|
>{{ i18n.ts.youGotNewFollower }}
|
||||||
<div v-if="full && !hideFollowButton">
|
<div v-if="full && !hideFollowButton">
|
||||||
|
<!-- FIXME: Provide a UserDetailed here -->
|
||||||
<MkFollowButton
|
<MkFollowButton
|
||||||
:user="notification.user"
|
:user="notification.user"
|
||||||
:full="true"
|
:full="true"
|
||||||
|
@ -269,7 +270,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
import { onMounted, onUnmounted, ref, toRef, watch } from "vue";
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||||
|
@ -284,6 +285,8 @@ import { useTooltip } from "@/scripts/use-tooltip";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import { instance } from "@/instance";
|
import { instance } from "@/instance";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { Connection } from "firefish-js/src/streaming";
|
||||||
|
import type { Channels } from "firefish-js/src/streaming.types";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -299,8 +302,8 @@ const props = withDefaults(
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
|
||||||
const elRef = ref<HTMLElement>(null);
|
const elRef = ref<HTMLElement | null>(null);
|
||||||
const reactionRef = ref(null);
|
const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
|
||||||
|
|
||||||
const hideFollowButton = defaultStore.state.hideFollowButtons;
|
const hideFollowButton = defaultStore.state.hideFollowButtons;
|
||||||
const showEmojiReactions =
|
const showEmojiReactions =
|
||||||
|
@ -311,7 +314,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
|
||||||
: "⭐";
|
: "⭐";
|
||||||
|
|
||||||
let readObserver: IntersectionObserver | undefined;
|
let readObserver: IntersectionObserver | undefined;
|
||||||
let connection;
|
let connection: Connection<Channels["main"]> | null = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.notification.isRead) {
|
if (!props.notification.isRead) {
|
||||||
|
@ -323,13 +326,13 @@ onMounted(() => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
readObserver.observe(elRef.value);
|
readObserver.observe(elRef.value!);
|
||||||
|
|
||||||
connection = stream.useChannel("main");
|
connection = stream.useChannel("main");
|
||||||
connection.on("readAllNotifications", () => readObserver.disconnect());
|
connection.on("readAllNotifications", () => readObserver!.disconnect());
|
||||||
|
|
||||||
watch(props.notification.isRead, () => {
|
watch(toRef(props.notification.isRead), () => {
|
||||||
readObserver.disconnect();
|
readObserver!.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -344,38 +347,47 @@ const groupInviteDone = ref(false);
|
||||||
|
|
||||||
const acceptFollowRequest = () => {
|
const acceptFollowRequest = () => {
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
os.api("following/requests/accept", { userId: props.notification.user.id });
|
os.api("following/requests/accept", {
|
||||||
|
userId: (props.notification as entities.ReceiveFollowRequestNotification)
|
||||||
|
.user.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectFollowRequest = () => {
|
const rejectFollowRequest = () => {
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
os.api("following/requests/reject", { userId: props.notification.user.id });
|
os.api("following/requests/reject", {
|
||||||
|
userId: (props.notification as entities.ReceiveFollowRequestNotification)
|
||||||
|
.user.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptGroupInvitation = () => {
|
const acceptGroupInvitation = () => {
|
||||||
groupInviteDone.value = true;
|
groupInviteDone.value = true;
|
||||||
os.apiWithDialog("users/groups/invitations/accept", {
|
os.apiWithDialog("users/groups/invitations/accept", {
|
||||||
invitationId: props.notification.invitation.id,
|
invitationId: (props.notification as entities.GroupInvitedNotification)
|
||||||
|
.invitation.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectGroupInvitation = () => {
|
const rejectGroupInvitation = () => {
|
||||||
groupInviteDone.value = true;
|
groupInviteDone.value = true;
|
||||||
os.api("users/groups/invitations/reject", {
|
os.api("users/groups/invitations/reject", {
|
||||||
invitationId: props.notification.invitation.id,
|
invitationId: (props.notification as entities.GroupInvitedNotification)
|
||||||
|
.invitation.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useTooltip(reactionRef, (showing) => {
|
useTooltip(reactionRef, (showing) => {
|
||||||
|
const n = props.notification as entities.ReactionNotification;
|
||||||
os.popup(
|
os.popup(
|
||||||
XReactionTooltip,
|
XReactionTooltip,
|
||||||
{
|
{
|
||||||
showing,
|
showing,
|
||||||
reaction: props.notification.reaction
|
reaction: n.reaction
|
||||||
? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:")
|
? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
|
||||||
: props.notification.reaction,
|
: n.reaction,
|
||||||
emojis: props.notification.note.emojis,
|
emojis: n.note.emojis,
|
||||||
targetElement: reactionRef.value.$el,
|
targetElement: reactionRef.value!.$el,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:with-ok-button="true"
|
:with-ok-button="true"
|
||||||
:ok-button-disabled="false"
|
:ok-button-disabled="false"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@close="dialog.close()"
|
@close="dialog!.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||||
|
@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []);
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({});
|
const typesMap = ref({} as Record<(typeof notificationTypes)[number], boolean>);
|
||||||
const useGlobalSetting = ref(
|
const useGlobalSetting = ref(
|
||||||
(includingTypes.value === null || includingTypes.value.length === 0) &&
|
(includingTypes.value === null || includingTypes.value.length === 0) &&
|
||||||
props.showGlobalToggle,
|
props.showGlobalToggle,
|
||||||
|
@ -89,7 +89,7 @@ function ok() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.value.close();
|
dialog.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableAll() {
|
function disableAll() {
|
||||||
|
|
|
@ -19,9 +19,10 @@ import { onMounted, ref } from "vue";
|
||||||
import XNotification from "@/components/MkNotification.vue";
|
import XNotification from "@/components/MkNotification.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
notification: any; // TODO
|
notification: entities.Notification;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -44,7 +44,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||||
import type { StreamTypes, entities, notificationTypes } from "firefish-js";
|
import type { StreamTypes, entities, notificationTypes } from "firefish-js";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination, {
|
||||||
|
type MkPaginationType,
|
||||||
|
} from "@/components/MkPagination.vue";
|
||||||
import XNotification from "@/components/MkNotification.vue";
|
import XNotification from "@/components/MkNotification.vue";
|
||||||
import XList from "@/components/MkDateSeparatedList.vue";
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
import XNote from "@/components/MkNote.vue";
|
import XNote from "@/components/MkNote.vue";
|
||||||
|
@ -59,7 +61,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null);
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: "i/notifications" as const,
|
endpoint: "i/notifications" as const,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
:to="`/@${page.user.username}/pages/${page.name}`"
|
:to="`/@${page.user.username}/pages/${page.name}`"
|
||||||
class="vhpxefrj _block"
|
class="vhpxefrj _block"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:behavior="`${ui === 'deck' ? 'window' : null}`"
|
:behavior="ui === 'deck' ? 'window' : null"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="page.eyeCatchingImage"
|
v-if="page.eyeCatchingImage"
|
||||||
|
@ -36,9 +36,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { userName } from "@/filters/user";
|
import { userName } from "@/filters/user";
|
||||||
import { ui } from "@/config";
|
import { ui } from "@/config";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
page: any;
|
page: entities.Page;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -56,23 +56,22 @@ const router = new Router(routes, props.initialPath);
|
||||||
|
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
const windowEl = ref<InstanceType<typeof XWindow>>();
|
const windowEl = ref<InstanceType<typeof XWindow>>();
|
||||||
const history = ref<{ path: string; key: any }[]>([
|
const history = ref<{ path: string; key: string }[]>([
|
||||||
{
|
{
|
||||||
path: router.getCurrentPath(),
|
path: router.getCurrentPath(),
|
||||||
key: router.getCurrentKey(),
|
key: router.getCurrentKey(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const buttonsLeft = computed(() => {
|
const buttonsLeft = computed(() => {
|
||||||
const buttons = [];
|
|
||||||
|
|
||||||
if (history.value.length > 1) {
|
if (history.value.length > 1) {
|
||||||
buttons.push({
|
return [
|
||||||
icon: `${icon("ph-caret-left")}`,
|
{
|
||||||
onClick: back,
|
icon: `${icon("ph-caret-left")}`,
|
||||||
});
|
onClick: back,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
return buttons;
|
|
||||||
});
|
});
|
||||||
const buttonsRight = computed(() => {
|
const buttonsRight = computed(() => {
|
||||||
const buttons = [
|
const buttons = [
|
||||||
|
@ -114,7 +113,7 @@ const contextmenu = computed(() => [
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(url + router.getCurrentPath(), "_blank");
|
window.open(url + router.getCurrentPath(), "_blank");
|
||||||
windowEl.value.close();
|
windowEl.value!.close();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -135,17 +134,17 @@ function back() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
windowEl.value.close();
|
windowEl.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
mainRouter.push(router.getCurrentPath(), "forcePage");
|
mainRouter.push(router.getCurrentPath(), "forcePage");
|
||||||
windowEl.value.close();
|
windowEl.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function popout() {
|
function popout() {
|
||||||
_popout(router.getCurrentPath(), windowEl.value.$el);
|
_popout(router.getCurrentPath(), windowEl.value!.$el);
|
||||||
windowEl.value.close();
|
windowEl.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup generic="E extends PagingKey">
|
<script lang="ts" setup generic="E extends PagingKey">
|
||||||
import type { ComputedRef } from "vue";
|
import type { ComponentPublicInstance, ComputedRef } from "vue";
|
||||||
import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
|
import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
|
||||||
import type { Endpoints, TypeUtils } from "firefish-js";
|
import type { Endpoints, TypeUtils } from "firefish-js";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
|
@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ref type of MkPagination<E>
|
||||||
|
* Due to Vue's incomplete type support for generic components,
|
||||||
|
* we have to manually maintain this type instead of
|
||||||
|
* using `InstanceType<typeof MkPagination>`
|
||||||
|
*/
|
||||||
|
export type MkPaginationType<
|
||||||
|
E extends PagingKey,
|
||||||
|
Item = Endpoints[E]["res"][number],
|
||||||
|
> = ComponentPublicInstance & {
|
||||||
|
items: Item[];
|
||||||
|
queue: Item[];
|
||||||
|
backed: boolean;
|
||||||
|
reload: () => Promise<void>;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
prepend: (item: Item) => Promise<void>;
|
||||||
|
append: (item: Item) => Promise<void>;
|
||||||
|
removeItem: (finder: (item: Item) => boolean) => boolean;
|
||||||
|
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
||||||
export type PagingKey = TypeUtils.EndpointsOf<any[]>;
|
export type PagingKey = PagingKeyOf<any>;
|
||||||
|
|
||||||
export interface Paging<E extends PagingKey = PagingKey> {
|
export interface Paging<E extends PagingKey = PagingKey> {
|
||||||
endpoint: E;
|
endpoint: E;
|
||||||
|
|
|
@ -84,25 +84,20 @@ import { formatDateTimeString } from "@/scripts/format-time-string";
|
||||||
import { addTime } from "@/scripts/time";
|
import { addTime } from "@/scripts/time";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { PollType } from "@/types/post-form";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: {
|
modelValue: PollType;
|
||||||
expiresAt: string;
|
|
||||||
expiredAfter: number;
|
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
"update:modelValue": [
|
||||||
ev: "update:modelValue",
|
|
||||||
v: {
|
v: {
|
||||||
expiresAt: string;
|
expiresAt?: number;
|
||||||
expiredAfter: number;
|
expiredAfter?: number | null;
|
||||||
choices: string[];
|
choices: string[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
},
|
},
|
||||||
): void;
|
];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const choices = ref(props.modelValue.choices);
|
const choices = ref(props.modelValue.choices);
|
||||||
|
@ -147,19 +142,19 @@ function get() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const calcAfter = () => {
|
const calcAfter = () => {
|
||||||
let base = parseInt(after.value);
|
let base = Number.parseInt(after.value.toString());
|
||||||
switch (unit.value) {
|
switch (unit.value) {
|
||||||
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
|
||||||
case "day":
|
case "day":
|
||||||
base *= 24;
|
base *= 24;
|
||||||
// fallthrough
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
|
||||||
case "hour":
|
case "hour":
|
||||||
base *= 60;
|
base *= 60;
|
||||||
// fallthrough
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
|
||||||
case "minute":
|
case "minute":
|
||||||
base *= 60;
|
base *= 60;
|
||||||
// fallthrough
|
|
||||||
case "second":
|
case "second":
|
||||||
return (base *= 1000);
|
return base * 1000;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ defineProps<{
|
||||||
align?: "center" | string;
|
align?: "center" | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
viaKeyboard?: boolean;
|
||||||
src?: any;
|
src?: HTMLElement | null;
|
||||||
noReturnFocus?;
|
noReturnFocus?;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
class="account _button"
|
class="account _button"
|
||||||
@click="openAccountMenu"
|
@click="openAccountMenu"
|
||||||
>
|
>
|
||||||
<MkAvatar :user="postAccount ?? me" class="avatar" />
|
<MkAvatar :user="postAccount ?? me!" class="avatar" />
|
||||||
</button>
|
</button>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<span
|
<span
|
||||||
|
@ -297,14 +297,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, nextTick, onMounted, ref, watch } from "vue";
|
import {
|
||||||
|
type Ref,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
import * as mfm from "mfm-js";
|
import * as mfm from "mfm-js";
|
||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
import insertTextAtCursor from "insert-text-at-cursor";
|
import insertTextAtCursor from "insert-text-at-cursor";
|
||||||
import { length } from "stringz";
|
import { length } from "stringz";
|
||||||
import { toASCII } from "punycode/";
|
import { toASCII } from "punycode/";
|
||||||
import { acct } from "firefish-js";
|
import { acct } from "firefish-js";
|
||||||
import type { entities, languages } from "firefish-js";
|
import type { ApiTypes, entities, languages } from "firefish-js";
|
||||||
import { throttle } from "throttle-debounce";
|
import { throttle } from "throttle-debounce";
|
||||||
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import XNotePreview from "@/components/MkNotePreview.vue";
|
import XNotePreview from "@/components/MkNotePreview.vue";
|
||||||
|
@ -341,6 +349,7 @@ import type { MenuItem } from "@/types/menu";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue";
|
import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue";
|
||||||
import type { NoteVisibility } from "@/types/note";
|
import type { NoteVisibility } from "@/types/note";
|
||||||
|
import type { NoteDraft, PollType } from "@/types/post-form";
|
||||||
|
|
||||||
const modal = inject("modal");
|
const modal = inject("modal");
|
||||||
|
|
||||||
|
@ -348,16 +357,16 @@ const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
reply?: entities.Note;
|
reply?: entities.Note;
|
||||||
renote?: entities.Note;
|
renote?: entities.Note;
|
||||||
channel?: any; // TODO
|
channel?: entities.Channel;
|
||||||
mention?: entities.User;
|
mention?: entities.User;
|
||||||
specified?: entities.User;
|
specified?: entities.User;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
initialVisibility?: NoteVisibility;
|
initialVisibility?: NoteVisibility;
|
||||||
initialLanguage?: typeof languages;
|
initialLanguage?: (typeof languages)[number];
|
||||||
initialFiles?: entities.DriveFile[];
|
initialFiles?: entities.DriveFile[];
|
||||||
initialLocalOnly?: boolean;
|
initialLocalOnly?: boolean;
|
||||||
initialVisibleUsers?: entities.User[];
|
initialVisibleUsers?: entities.User[];
|
||||||
initialNote?: entities.Note;
|
initialNote?: NoteDraft;
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
@ -390,12 +399,7 @@ const showBigPostButton = defaultStore.state.showBigPostButton;
|
||||||
const posting = ref(false);
|
const posting = ref(false);
|
||||||
const text = ref(props.initialText ?? "");
|
const text = ref(props.initialText ?? "");
|
||||||
const files = ref(props.initialFiles ?? ([] as entities.DriveFile[]));
|
const files = ref(props.initialFiles ?? ([] as entities.DriveFile[]));
|
||||||
const poll = ref<{
|
const poll = ref<PollType | null>(null);
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
expiresAt: string | null;
|
|
||||||
expiredAfter: string | null;
|
|
||||||
} | null>(null);
|
|
||||||
const useCw = ref(false);
|
const useCw = ref(false);
|
||||||
const showPreview = ref(defaultStore.state.showPreviewByDefault);
|
const showPreview = ref(defaultStore.state.showPreviewByDefault);
|
||||||
const cw = ref<string | null>(null);
|
const cw = ref<string | null>(null);
|
||||||
|
@ -411,12 +415,12 @@ const visibility = ref(
|
||||||
: defaultStore.state.defaultNoteVisibility),
|
: defaultStore.state.defaultNoteVisibility),
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibleUsers = ref([]);
|
const visibleUsers = ref<entities.User[]>([]);
|
||||||
if (props.initialVisibleUsers) {
|
if (props.initialVisibleUsers) {
|
||||||
props.initialVisibleUsers.forEach(pushVisibleUser);
|
props.initialVisibleUsers.forEach(pushVisibleUser);
|
||||||
}
|
}
|
||||||
const draghover = ref(false);
|
const draghover = ref(false);
|
||||||
const quoteId = ref(null);
|
const quoteId = ref<string | null>(null);
|
||||||
const hasNotSpecifiedMentions = ref(false);
|
const hasNotSpecifiedMentions = ref(false);
|
||||||
const recentHashtags = ref(
|
const recentHashtags = ref(
|
||||||
JSON.parse(localStorage.getItem("hashtags") || "[]"),
|
JSON.parse(localStorage.getItem("hashtags") || "[]"),
|
||||||
|
@ -500,7 +504,9 @@ const canPost = computed((): boolean => {
|
||||||
const withHashtags = computed(
|
const withHashtags = computed(
|
||||||
defaultStore.makeGetterSetter("postFormWithHashtags"),
|
defaultStore.makeGetterSetter("postFormWithHashtags"),
|
||||||
);
|
);
|
||||||
const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags"));
|
const hashtags = computed(
|
||||||
|
defaultStore.makeGetterSetter("postFormHashtags"),
|
||||||
|
) as Ref<string | null>;
|
||||||
|
|
||||||
watch(text, () => {
|
watch(text, () => {
|
||||||
checkMissingMention();
|
checkMissingMention();
|
||||||
|
@ -525,7 +531,7 @@ if (props.mention) {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.reply &&
|
props.reply &&
|
||||||
(props.reply.user.username !== me.username ||
|
(props.reply.user.username !== me!.username ||
|
||||||
(props.reply.user.host != null && props.reply.user.host !== host))
|
(props.reply.user.host != null && props.reply.user.host !== host))
|
||||||
) {
|
) {
|
||||||
text.value = `@${props.reply.user.username}${
|
text.value = `@${props.reply.user.username}${
|
||||||
|
@ -545,7 +551,7 @@ if (props.reply && props.reply.text != null) {
|
||||||
: `@${x.username}@${toASCII(otherHost)}`;
|
: `@${x.username}@${toASCII(otherHost)}`;
|
||||||
|
|
||||||
// exclude me
|
// exclude me
|
||||||
if (me.username === x.username && (x.host == null || x.host === host))
|
if (me!.username === x.username && (x.host == null || x.host === host))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// remove duplicates
|
// remove duplicates
|
||||||
|
@ -579,7 +585,7 @@ if (
|
||||||
if (props.reply.visibleUserIds) {
|
if (props.reply.visibleUserIds) {
|
||||||
os.api("users/show", {
|
os.api("users/show", {
|
||||||
userIds: props.reply.visibleUserIds.filter(
|
userIds: props.reply.visibleUserIds.filter(
|
||||||
(uid) => uid !== me.id && uid !== props.reply.userId,
|
(uid) => uid !== me!.id && uid !== props.reply!.userId,
|
||||||
),
|
),
|
||||||
}).then((users) => {
|
}).then((users) => {
|
||||||
users.forEach(pushVisibleUser);
|
users.forEach(pushVisibleUser);
|
||||||
|
@ -588,7 +594,7 @@ if (
|
||||||
visibility.value = "private";
|
visibility.value = "private";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply.userId !== me.id) {
|
if (props.reply.userId !== me!.id) {
|
||||||
os.api("users/show", { userId: props.reply.userId }).then((user) => {
|
os.api("users/show", { userId: props.reply.userId }).then((user) => {
|
||||||
pushVisibleUser(user);
|
pushVisibleUser(user);
|
||||||
});
|
});
|
||||||
|
@ -615,7 +621,7 @@ const addRe = (s: string) => {
|
||||||
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
|
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
|
||||||
useCw.value = true;
|
useCw.value = true;
|
||||||
cw.value =
|
cw.value =
|
||||||
props.reply.user.username === me.username
|
props.reply.user.username === me!.username
|
||||||
? props.reply.cw
|
? props.reply.cw
|
||||||
: addRe(props.reply.cw);
|
: addRe(props.reply.cw);
|
||||||
}
|
}
|
||||||
|
@ -894,11 +900,14 @@ function onCompositionEnd(ev: CompositionEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPaste(ev: ClipboardEvent) {
|
async function onPaste(ev: ClipboardEvent) {
|
||||||
|
if (ev.clipboardData == null) return;
|
||||||
|
|
||||||
for (const { item, i } of Array.from(ev.clipboardData.items).map(
|
for (const { item, i } of Array.from(ev.clipboardData.items).map(
|
||||||
(item, i) => ({ item, i }),
|
(item, i) => ({ item, i }),
|
||||||
)) {
|
)) {
|
||||||
if (item.kind === "file") {
|
if (item.kind === "file") {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
|
if (file == null) continue;
|
||||||
const lio = file.name.lastIndexOf(".");
|
const lio = file.name.lastIndexOf(".");
|
||||||
const ext = lio >= 0 ? file.name.slice(lio) : "";
|
const ext = lio >= 0 ? file.name.slice(lio) : "";
|
||||||
const formatted = `${formatTimeString(
|
const formatted = `${formatTimeString(
|
||||||
|
@ -911,7 +920,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
|
|
||||||
const paste = ev.clipboardData?.getData("text") ?? "";
|
const paste = ev.clipboardData?.getData("text") ?? "";
|
||||||
|
|
||||||
if (!props.renote && !quoteId.value && paste.startsWith(url + "/notes/")) {
|
if (!props.renote && !quoteId.value && paste.startsWith(`${url}/notes/`)) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
os.yesno({
|
os.yesno({
|
||||||
|
@ -919,13 +928,13 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
text: i18n.ts.quoteQuestion,
|
text: i18n.ts.quoteQuestion,
|
||||||
}).then(({ canceled }) => {
|
}).then(({ canceled }) => {
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
insertTextAtCursor(textareaEl.value, paste);
|
insertTextAtCursor(textareaEl.value!, paste);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteId.value = paste
|
quoteId.value = paste
|
||||||
.substring(url.length)
|
.substring(url.length)
|
||||||
.match(/^\/notes\/(.+?)\/?$/)[1];
|
.match(/^\/notes\/(.+?)\/?$/)![1];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -956,16 +965,17 @@ function onDragover(ev) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragenter(ev) {
|
function onDragenter(_ev) {
|
||||||
draghover.value = true;
|
draghover.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragleave(ev) {
|
function onDragleave(_ev) {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(ev): void {
|
function onDrop(ev: DragEvent): void {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
|
if (ev.dataTransfer == null) return;
|
||||||
|
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (ev.dataTransfer.files.length > 0) {
|
if (ev.dataTransfer.files.length > 0) {
|
||||||
|
@ -1064,7 +1074,7 @@ async function post() {
|
||||||
|
|
||||||
const processedText = preprocess(text.value);
|
const processedText = preprocess(text.value);
|
||||||
|
|
||||||
let postData = {
|
let postData: ApiTypes.NoteSubmitReq = {
|
||||||
editId: props.editId ? props.editId : undefined,
|
editId: props.editId ? props.editId : undefined,
|
||||||
text: processedText === "" ? undefined : processedText,
|
text: processedText === "" ? undefined : processedText,
|
||||||
fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
|
||||||
|
@ -1092,7 +1102,7 @@ async function post() {
|
||||||
const hashtags_ = hashtags.value
|
const hashtags_ = hashtags.value
|
||||||
.trim()
|
.trim()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((x) => (x.startsWith("#") ? x : "#" + x))
|
.map((x) => (x.startsWith("#") ? x : `#${x}`))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
|
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
|
||||||
}
|
}
|
||||||
|
@ -1104,11 +1114,11 @@ async function post() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let token;
|
let token: string | undefined;
|
||||||
|
|
||||||
if (postAccount.value) {
|
if (postAccount.value) {
|
||||||
const storedAccounts = await getAccounts();
|
const storedAccounts = await getAccounts();
|
||||||
token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token;
|
token = storedAccounts.find((x) => x.id === postAccount.value!.id)?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
posting.value = true;
|
posting.value = true;
|
||||||
|
@ -1119,10 +1129,11 @@ async function post() {
|
||||||
deleteDraft();
|
deleteDraft();
|
||||||
emit("posted");
|
emit("posted");
|
||||||
if (postData.text && postData.text !== "") {
|
if (postData.text && postData.text !== "") {
|
||||||
const hashtags_ = mfm
|
const hashtags_ = (
|
||||||
.parse(postData.text)
|
mfm
|
||||||
.filter((x) => x.type === "hashtag")
|
.parse(postData.text)
|
||||||
.map((x) => x.props.hashtag);
|
.filter((x) => x.type === "hashtag") as mfm.MfmHashtag[]
|
||||||
|
).map((x) => x.props.hashtag);
|
||||||
const history = JSON.parse(
|
const history = JSON.parse(
|
||||||
localStorage.getItem("hashtags") || "[]",
|
localStorage.getItem("hashtags") || "[]",
|
||||||
) as string[];
|
) as string[];
|
||||||
|
@ -1133,14 +1144,14 @@ async function post() {
|
||||||
}
|
}
|
||||||
posting.value = false;
|
posting.value = false;
|
||||||
postAccount.value = null;
|
postAccount.value = null;
|
||||||
nextTick(() => autosize.update(textareaEl.value));
|
nextTick(() => autosize.update(textareaEl.value!));
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err: { message: string; id: string }) => {
|
||||||
posting.value = false;
|
posting.value = false;
|
||||||
os.alert({
|
os.alert({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: err.message + "\n" + (err as any).id,
|
text: `${err.message}\n${err.id}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
vibrate([10, 20, 10, 20, 10, 20, 60]);
|
vibrate([10, 20, 10, 20, 10, 20, 60]);
|
||||||
|
@ -1169,19 +1180,23 @@ function cancel() {
|
||||||
|
|
||||||
function insertMention() {
|
function insertMention() {
|
||||||
os.selectUser().then((user) => {
|
os.selectUser().then((user) => {
|
||||||
insertTextAtCursor(textareaEl.value, "@" + acct.toString(user) + " ");
|
insertTextAtCursor(textareaEl.value!, `@${acct.toString(user)} `);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertEmoji(ev: MouseEvent) {
|
async function insertEmoji(ev: MouseEvent) {
|
||||||
os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl.value);
|
os.openEmojiPicker(
|
||||||
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
|
{},
|
||||||
|
textareaEl.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCheatSheet(ev: MouseEvent) {
|
async function openCheatSheet(ev: MouseEvent) {
|
||||||
os.popup(XCheatSheet, {}, {}, "closed");
|
os.popup(XCheatSheet, {}, {}, "closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showActions(ev) {
|
function showActions(ev: MouseEvent) {
|
||||||
os.popupMenu(
|
os.popupMenu(
|
||||||
postFormActions.map((action) => ({
|
postFormActions.map((action) => ({
|
||||||
text: action.title,
|
text: action.title,
|
||||||
|
@ -1198,7 +1213,7 @@ function showActions(ev) {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
ev.currentTarget ?? ev.target,
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1209,9 +1224,9 @@ function openAccountMenu(ev: MouseEvent) {
|
||||||
{
|
{
|
||||||
withExtraOperation: false,
|
withExtraOperation: false,
|
||||||
includeCurrentAccount: true,
|
includeCurrentAccount: true,
|
||||||
active: postAccount.value != null ? postAccount.value.id : me.id,
|
active: postAccount.value != null ? postAccount.value.id : me!.id,
|
||||||
onChoose: (account) => {
|
onChoose: (account) => {
|
||||||
if (account.id === me.id) {
|
if (account.id === me!.id) {
|
||||||
postAccount.value = null;
|
postAccount.value = null;
|
||||||
} else {
|
} else {
|
||||||
postAccount.value = account;
|
postAccount.value = account;
|
||||||
|
@ -1232,14 +1247,14 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: detach when unmount
|
// TODO: detach when unmount
|
||||||
new Autocomplete(textareaEl.value, text);
|
new Autocomplete(textareaEl.value!, text);
|
||||||
new Autocomplete(cwInputEl.value, cw);
|
new Autocomplete(cwInputEl.value!, cw as Ref<string>);
|
||||||
new Autocomplete(hashtagsInputEl.value, hashtags);
|
new Autocomplete(hashtagsInputEl.value!, hashtags as Ref<string>);
|
||||||
|
|
||||||
autosize(textareaEl.value);
|
autosize(textareaEl.value!);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
autosize(textareaEl.value);
|
autosize(textareaEl.value!);
|
||||||
// 書きかけの投稿を復元
|
// 書きかけの投稿を復元
|
||||||
if (!props.instant && !props.mention && !props.specified) {
|
if (!props.instant && !props.mention && !props.specified) {
|
||||||
const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
|
const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
|
||||||
|
@ -1275,8 +1290,8 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
visibility.value = init.visibility;
|
visibility.value = init.visibility;
|
||||||
localOnly.value = init.localOnly;
|
localOnly.value = init.localOnly ?? false;
|
||||||
language.value = init.lang;
|
language.value = init.lang ?? null;
|
||||||
quoteId.value = init.renote ? init.renote.id : null;
|
quoteId.value = init.renote ? init.renote.id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1289,7 +1304,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => watchForDraft());
|
nextTick(() => watchForDraft());
|
||||||
nextTick(() => autosize.update(textareaEl.value));
|
nextTick(() => autosize.update(textareaEl.value!));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MkModal
|
<MkModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:prefer-type="'dialog'"
|
:prefer-type="'dialog'"
|
||||||
@click="modal.close()"
|
@click="modal!.close()"
|
||||||
@closed="onModalClosed()"
|
@closed="onModalClosed()"
|
||||||
>
|
>
|
||||||
<MkPostForm
|
<MkPostForm
|
||||||
|
@ -12,8 +12,8 @@
|
||||||
autofocus
|
autofocus
|
||||||
freeze-after-posted
|
freeze-after-posted
|
||||||
@posted="onPosted"
|
@posted="onPosted"
|
||||||
@cancel="modal.close()"
|
@cancel="modal!.close()"
|
||||||
@esc="modal.close()"
|
@esc="modal!.close()"
|
||||||
/>
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,20 +25,21 @@ import type { entities, languages } from "firefish-js";
|
||||||
import MkModal from "@/components/MkModal.vue";
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkPostForm from "@/components/MkPostForm.vue";
|
import MkPostForm from "@/components/MkPostForm.vue";
|
||||||
import type { NoteVisibility } from "@/types/note";
|
import type { NoteVisibility } from "@/types/note";
|
||||||
|
import type { NoteDraft } from "@/types/post-form";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reply?: entities.Note;
|
reply?: entities.Note;
|
||||||
renote?: entities.Note;
|
renote?: entities.Note;
|
||||||
channel?: any; // TODO
|
channel?: entities.Channel;
|
||||||
mention?: entities.User;
|
mention?: entities.User;
|
||||||
specified?: entities.User;
|
specified?: entities.User;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
initialVisibility?: NoteVisibility;
|
initialVisibility?: NoteVisibility;
|
||||||
initialLanguage?: typeof languages;
|
initialLanguage?: (typeof languages)[number];
|
||||||
initialFiles?: entities.DriveFile[];
|
initialFiles?: entities.DriveFile[];
|
||||||
initialLocalOnly?: boolean;
|
initialLocalOnly?: boolean;
|
||||||
initialVisibleUsers?: entities.User[];
|
initialVisibleUsers?: entities.User[];
|
||||||
initialNote?: entities.Note;
|
initialNote?: NoteDraft;
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
@ -53,7 +54,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const form = shallowRef<InstanceType<typeof MkPostForm>>();
|
const form = shallowRef<InstanceType<typeof MkPostForm>>();
|
||||||
|
|
||||||
function onPosted() {
|
function onPosted() {
|
||||||
modal.value.close({
|
modal.value!.close({
|
||||||
useSendAnimation: true,
|
useSendAnimation: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
customEmojis?: any[]; // TODO
|
customEmojis?: entities.EmojiLite[];
|
||||||
noStyle?: boolean;
|
noStyle?: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
ref="tooltip"
|
ref="tooltip"
|
||||||
:target-element="targetElement"
|
:target-element="targetElement"
|
||||||
:max-width="340"
|
:max-width="340"
|
||||||
|
:showing="showing"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<div class="beeadbfb">
|
<div class="beeadbfb">
|
||||||
|
@ -18,12 +19,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from "vue";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
showing: Ref<boolean>;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
emojis: any[]; // TODO
|
emojis: entities.EmojiLite[];
|
||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
:target-element="targetElement"
|
:target-element="targetElement"
|
||||||
:max-width="340"
|
:max-width="340"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
|
:showing="showing"
|
||||||
>
|
>
|
||||||
<div class="bqxuuuey">
|
<div class="bqxuuuey">
|
||||||
<div class="reaction">
|
<div class="reaction">
|
||||||
|
@ -29,15 +30,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from "vue";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
showing: Ref<boolean>;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
users: any[]; // TODO
|
users: entities.User[]; // TODO
|
||||||
count: number;
|
count: number;
|
||||||
emojis: any[]; // TODO
|
emojis: entities.EmojiLite[]; // TODO
|
||||||
targetElement: HTMLElement;
|
targetElement?: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -89,7 +89,7 @@ useTooltip(
|
||||||
emojis: props.note.emojis,
|
emojis: props.note.emojis,
|
||||||
users,
|
users,
|
||||||
count: props.count,
|
count: props.count,
|
||||||
targetElement: buttonRef.value,
|
targetElement: buttonRef.value!,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed",
|
||||||
|
|
|
@ -46,7 +46,7 @@ const buttonRef = ref<HTMLElement>();
|
||||||
const canRenote = computed(
|
const canRenote = computed(
|
||||||
() =>
|
() =>
|
||||||
["public", "home"].includes(props.note.visibility) ||
|
["public", "home"].includes(props.note.visibility) ||
|
||||||
props.note.userId === me.id,
|
props.note.userId === me?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
useTooltip(buttonRef, async (showing) => {
|
useTooltip(buttonRef, async (showing) => {
|
||||||
|
@ -77,7 +77,7 @@ const hasRenotedBefore = ref(false);
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
os.api("notes/renotes", {
|
os.api("notes/renotes", {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
userId: me.id,
|
userId: me!.id,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
hasRenotedBefore.value = res.length > 0;
|
hasRenotedBefore.value = res.length > 0;
|
||||||
|
@ -251,6 +251,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
||||||
|
|
||||||
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
|
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
renote,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -145,9 +145,10 @@ import * as os from "@/os";
|
||||||
import { signIn } from "@/account";
|
import { signIn } from "@/account";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
const signing = ref(false);
|
const signing = ref(false);
|
||||||
const user = ref(null);
|
const user = ref<entities.UserDetailed | null>(null);
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
const token = ref("");
|
const token = ref("");
|
||||||
|
@ -249,7 +250,7 @@ function queryKey() {
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
signing.value = true;
|
signing.value = true;
|
||||||
console.log("submit");
|
console.log("submit");
|
||||||
if (window.PublicKeyCredential && user.value.securityKeys) {
|
if (window.PublicKeyCredential && user.value?.securityKeys) {
|
||||||
os.api("signin", {
|
os.api("signin", {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
|
@ -263,7 +264,7 @@ function onSubmit() {
|
||||||
return queryKey();
|
return queryKey();
|
||||||
})
|
})
|
||||||
.catch(loginFailed);
|
.catch(loginFailed);
|
||||||
} else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
|
} else if (!totpLogin.value && user.value?.twoFactorEnabled) {
|
||||||
totpLogin.value = true;
|
totpLogin.value = true;
|
||||||
signing.value = false;
|
signing.value = false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -272,8 +273,7 @@ function onSubmit() {
|
||||||
password: password.value,
|
password: password.value,
|
||||||
"hcaptcha-response": hCaptchaResponse.value,
|
"hcaptcha-response": hCaptchaResponse.value,
|
||||||
"g-recaptcha-response": reCaptchaResponse.value,
|
"g-recaptcha-response": reCaptchaResponse.value,
|
||||||
token:
|
token: user.value?.twoFactorEnabled ? token.value : undefined,
|
||||||
user.value && user.value.twoFactorEnabled ? token.value : undefined,
|
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
emit("login", res);
|
emit("login", res);
|
||||||
|
|
|
@ -305,12 +305,12 @@ const host = toUnicode(config.host);
|
||||||
const hcaptcha = ref();
|
const hcaptcha = ref();
|
||||||
const recaptcha = ref();
|
const recaptcha = ref();
|
||||||
|
|
||||||
const username: string = ref("");
|
const username = ref<string>("");
|
||||||
const password: string = ref("");
|
const password = ref<string>("");
|
||||||
const retypedPassword: string = ref("");
|
const retypedPassword = ref<string>("");
|
||||||
const invitationCode: string = ref("");
|
const invitationCode = ref<string>("");
|
||||||
const email = ref("");
|
const email = ref("");
|
||||||
const usernameState:
|
const usernameState = ref<
|
||||||
| null
|
| null
|
||||||
| "wait"
|
| "wait"
|
||||||
| "ok"
|
| "ok"
|
||||||
|
@ -318,9 +318,10 @@ const usernameState:
|
||||||
| "error"
|
| "error"
|
||||||
| "invalid-format"
|
| "invalid-format"
|
||||||
| "min-range"
|
| "min-range"
|
||||||
| "max-range" = ref(null);
|
| "max-range"
|
||||||
const invitationState: null | "entered" = ref(null);
|
>(null);
|
||||||
const emailState:
|
const invitationState = ref<null | "entered">(null);
|
||||||
|
const emailState = ref<
|
||||||
| null
|
| null
|
||||||
| "wait"
|
| "wait"
|
||||||
| "ok"
|
| "ok"
|
||||||
|
@ -330,11 +331,12 @@ const emailState:
|
||||||
| "unavailable:mx"
|
| "unavailable:mx"
|
||||||
| "unavailable:smtp"
|
| "unavailable:smtp"
|
||||||
| "unavailable"
|
| "unavailable"
|
||||||
| "error" = ref(null);
|
| "error"
|
||||||
const passwordStrength: "" | "low" | "medium" | "high" = ref("");
|
>(null);
|
||||||
const passwordRetypeState: null | "match" | "not-match" = ref(null);
|
const passwordStrength = ref<"" | "low" | "medium" | "high">("");
|
||||||
const submitting: boolean = ref(false);
|
const passwordRetypeState = ref<null | "match" | "not-match">(null);
|
||||||
const ToSAgreement: boolean = ref(false);
|
const submitting = ref(false);
|
||||||
|
const ToSAgreement = ref(false);
|
||||||
const hCaptchaResponse = ref(null);
|
const hCaptchaResponse = ref(null);
|
||||||
const reCaptchaResponse = ref(null);
|
const reCaptchaResponse = ref(null);
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
:text="note.cw"
|
:text="note.cw"
|
||||||
:author="note.user"
|
:author="note.user"
|
||||||
:lang="note.lang"
|
:lang="note.lang"
|
||||||
:i="me"
|
|
||||||
:custom-emojis="note.emojis"
|
:custom-emojis="note.emojis"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -63,8 +62,8 @@
|
||||||
<div
|
<div
|
||||||
class="body"
|
class="body"
|
||||||
v-bind="{
|
v-bind="{
|
||||||
'aria-hidden': note.cw && !showContent ? 'true' : null,
|
'aria-hidden': note.cw && !showContent ? 'true' : undefined,
|
||||||
tabindex: !showContent ? '-1' : null,
|
tabindex: !showContent ? '-1' : undefined,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span v-if="note.deletedAt" style="opacity: 0.5"
|
<span v-if="note.deletedAt" style="opacity: 0.5"
|
||||||
|
@ -103,7 +102,6 @@
|
||||||
v-if="note.text"
|
v-if="note.text"
|
||||||
:text="note.text"
|
:text="note.text"
|
||||||
:author="note.user"
|
:author="note.user"
|
||||||
:i="me"
|
|
||||||
:lang="note.lang"
|
:lang="note.lang"
|
||||||
:custom-emojis="note.emojis"
|
:custom-emojis="note.emojis"
|
||||||
/>
|
/>
|
||||||
|
@ -256,7 +254,7 @@ async function toggleMfm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusFooter(ev) {
|
function focusFooter(ev) {
|
||||||
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
|
if (ev.key === "Tab" && !ev.getModifierState("Shift")) {
|
||||||
emit("focusfooter");
|
emit("focusfooter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ onMounted(() => {
|
||||||
src: "/client-assets/tagcanvas.min.js",
|
src: "/client-assets/tagcanvas.min.js",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
|
||||||
.addEventListener("load", () => (available.value = true));
|
.addEventListener("load", () => (available.value = true));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
@after-leave="emit('closed')"
|
@after-leave="emit('closed')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-show="showing"
|
v-show="unref(showing)"
|
||||||
ref="el"
|
ref="el"
|
||||||
class="buebdbiu _acrylic _shadow"
|
class="buebdbiu _acrylic _shadow"
|
||||||
:style="{ zIndex, maxWidth: maxWidth + 'px' }"
|
:style="{ zIndex, maxWidth: maxWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<Mfm v-if="asMfm" :text="text" />
|
<Mfm v-if="asMfm" :text="text!" />
|
||||||
<span v-else>{{ text }}</span>
|
<span v-else>{{ text }}</span>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,15 +19,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
import {
|
||||||
|
type MaybeRef,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
} from "vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { calcPopupPosition } from "@/scripts/popup-position";
|
import { calcPopupPosition } from "@/scripts/popup-position";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: MaybeRef<boolean>;
|
||||||
targetElement?: HTMLElement;
|
targetElement?: HTMLElement | null;
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
@ -40,6 +47,7 @@ const props = withDefaults(
|
||||||
maxWidth: 250,
|
maxWidth: 250,
|
||||||
direction: "top",
|
direction: "top",
|
||||||
innerMargin: 0,
|
innerMargin: 0,
|
||||||
|
targetElement: null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -51,7 +59,7 @@ const el = ref<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex("high");
|
const zIndex = os.claimZIndex("high");
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
const data = calcPopupPosition(el.value, {
|
const data = calcPopupPosition(el.value!, {
|
||||||
anchorElement: props.targetElement,
|
anchorElement: props.targetElement,
|
||||||
direction: props.direction,
|
direction: props.direction,
|
||||||
align: "center",
|
align: "center",
|
||||||
|
@ -60,12 +68,12 @@ function setPosition() {
|
||||||
y: props.y,
|
y: props.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
el.value.style.transformOrigin = data.transformOrigin;
|
el.value!.style.transformOrigin = data.transformOrigin;
|
||||||
el.value.style.left = data.left + "px";
|
el.value!.style.left = `${data.left}px`;
|
||||||
el.value.style.top = data.top + "px";
|
el.value!.style.top = `${data.top}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let loopHandler;
|
let loopHandler: number;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
|
@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) {
|
||||||
if (height) tweetHeight.value = height;
|
if (height) tweetHeight.value = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as any).addEventListener("message", adjustTweetHeight);
|
window.addEventListener("message", adjustTweetHeight);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
(window as any).removeEventListener("message", adjustTweetHeight);
|
window.removeEventListener("message", adjustTweetHeight);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: users }">
|
<template #default="{ items }: { items: entities.UserDetailed[] }">
|
||||||
<div class="efvhhmdq">
|
<div class="efvhhmdq">
|
||||||
<MkUserInfo
|
<MkUserInfo
|
||||||
v-for="user in users"
|
v-for="user in items"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
class="user"
|
class="user"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
@ -27,16 +27,21 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import MkUserInfo from "@/components/MkUserInfo.vue";
|
import MkUserInfo from "@/components/MkUserInfo.vue";
|
||||||
import type { Paging } from "@/components/MkPagination.vue";
|
import type {
|
||||||
|
MkPaginationType,
|
||||||
|
PagingKeyOf,
|
||||||
|
PagingOf,
|
||||||
|
} from "@/components/MkPagination.vue";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pagination: Paging;
|
pagination: PagingOf<entities.UserDetailed>;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = ref<MkPaginationType<PagingKeyOf<entities.User>>>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -98,16 +98,16 @@ import { defaultStore } from "@/store";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "ok", selected: entities.UserDetailed): void;
|
ok: [selected: entities.UserDetailed];
|
||||||
(ev: "cancel"): void;
|
cancel: [];
|
||||||
(ev: "closed"): void;
|
closed: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const host = ref("");
|
const host = ref("");
|
||||||
const users: entities.UserDetailed[] = ref([]);
|
const users = ref<entities.UserDetailed[]>([]);
|
||||||
const recentUsers: entities.UserDetailed[] = ref([]);
|
const recentUsers = ref<entities.UserDetailed[]>([]);
|
||||||
const selected: entities.UserDetailed | null = ref(null);
|
const selected = ref<entities.UserDetailed | null>(null);
|
||||||
const dialogEl = ref();
|
const dialogEl = ref();
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
|
@ -132,7 +132,7 @@ const ok = () => {
|
||||||
|
|
||||||
// 最近使ったユーザー更新
|
// 最近使ったユーザー更新
|
||||||
let recents = defaultStore.state.recentlyUsedUsers;
|
let recents = defaultStore.state.recentlyUsedUsers;
|
||||||
recents = recents.filter((x) => x !== selected.value.id);
|
recents = recents.filter((x) => x !== selected.value!.id);
|
||||||
recents.unshift(selected.value.id);
|
recents.unshift(selected.value.id);
|
||||||
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
|
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
|
||||||
};
|
};
|
||||||
|
|
|
@ -94,9 +94,9 @@ import { defaultStore } from "@/store";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "ok", selected: entities.UserDetailed): void;
|
ok: [selected: entities.UserDetailed];
|
||||||
(ev: "cancel"): void;
|
cancel: [];
|
||||||
(ev: "closed"): void;
|
closed: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
|
@ -114,7 +114,7 @@ const search = () => {
|
||||||
query: username.value,
|
query: username.value,
|
||||||
origin: "local",
|
origin: "local",
|
||||||
limit: 10,
|
limit: 10,
|
||||||
detail: false,
|
detail: true,
|
||||||
}).then((_users) => {
|
}).then((_users) => {
|
||||||
users.value = _users;
|
users.value = _users;
|
||||||
});
|
});
|
||||||
|
@ -127,7 +127,7 @@ const ok = () => {
|
||||||
|
|
||||||
// 最近使ったユーザー更新
|
// 最近使ったユーザー更新
|
||||||
let recents = defaultStore.state.recentlyUsedUsers;
|
let recents = defaultStore.state.recentlyUsedUsers;
|
||||||
recents = recents.filter((x) => x !== selected.value.id);
|
recents = recents.filter((x) => x !== selected.value!.id);
|
||||||
recents.unshift(selected.value.id);
|
recents.unshift(selected.value.id);
|
||||||
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
|
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
:target-element="targetElement"
|
:target-element="targetElement"
|
||||||
:max-width="250"
|
:max-width="250"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
|
:showing="showing"
|
||||||
>
|
>
|
||||||
<div class="beaffaef">
|
<div class="beaffaef">
|
||||||
<div v-for="u in users" :key="u.id" class="user">
|
<div v-for="u in users" :key="u.id" class="user">
|
||||||
|
@ -18,12 +19,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from "vue";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
|
import type { entities } from "firefish-js";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
users: any[]; // TODO
|
showing: Ref<boolean>;
|
||||||
|
users: entities.User[];
|
||||||
count: number;
|
count: number;
|
||||||
targetElement: HTMLElement;
|
targetElement?: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:z-priority="'high'"
|
:z-priority="'high'"
|
||||||
:src="src"
|
:src="src"
|
||||||
@click="modal.close()"
|
@click="modal!.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<div class="_popup" :class="$style.root">
|
<div class="_popup" :class="$style.root">
|
||||||
|
@ -153,15 +153,15 @@ const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
currentVisibility: NoteVisibility;
|
currentVisibility: NoteVisibility;
|
||||||
currentLocalOnly: boolean;
|
currentLocalOnly: boolean;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement | null;
|
||||||
}>(),
|
}>(),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "changeVisibility", v: NoteVisibility): void;
|
changeVisibility: [v: NoteVisibility];
|
||||||
(ev: "changeLocalOnly", v: boolean): void;
|
changeLocalOnly: [v: boolean];
|
||||||
(ev: "closed"): void;
|
closed: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const v = ref(props.currentVisibility);
|
const v = ref(props.currentVisibility);
|
||||||
|
@ -175,7 +175,7 @@ function choose(visibility: NoteVisibility): void {
|
||||||
v.value = visibility;
|
v.value = visibility;
|
||||||
emit("changeVisibility", visibility);
|
emit("changeVisibility", visibility);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
modal.value.close();
|
modal.value!.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
v-if="success"
|
v-if="unref(success)"
|
||||||
:class="[$style.icon, $style.success, iconify('ph-check')]"
|
:class="[$style.icon, $style.success, iconify('ph-check')]"
|
||||||
></i>
|
></i>
|
||||||
<MkLoading
|
<MkLoading
|
||||||
|
@ -29,15 +29,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef, watch } from "vue";
|
import { MaybeRef, shallowRef, watch, unref } from "vue";
|
||||||
import MkModal from "@/components/MkModal.vue";
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import iconify from "@/scripts/icon";
|
import iconify from "@/scripts/icon";
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
success: boolean;
|
success: MaybeRef<boolean>;
|
||||||
showing: boolean;
|
showing: MaybeRef<boolean>;
|
||||||
text?: string;
|
text?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ import icon from "@/scripts/icon";
|
||||||
interface Widget {
|
interface Widget {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
data: Record<string, any>;
|
data: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target as HTMLElement)) return;
|
||||||
if (
|
if (
|
||||||
["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes(
|
["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes(
|
||||||
ev.target.tagName,
|
(ev.target as HTMLElement).tagName,
|
||||||
) ||
|
) ||
|
||||||
ev.target.attributes.contenteditable
|
(ev.target as HTMLElement).getAttribute("contentEditable")
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (window.getSelection()?.toString() !== "") return;
|
if (window.getSelection()?.toString() !== "") return;
|
||||||
|
|
|
@ -271,7 +271,7 @@ function onHeaderMousedown(evt: MouseEvent) {
|
||||||
? evt.touches[0].clientY
|
? evt.touches[0].clientY
|
||||||
: evt.clientY;
|
: evt.clientY;
|
||||||
const moveBaseX = beforeMaximized
|
const moveBaseX = beforeMaximized
|
||||||
? parseInt(unMaximizedWidth, 10) / 2
|
? Number.parseInt(unMaximizedWidth, 10) / 2
|
||||||
: clickX - position.left; // TODO: parseIntやめる
|
: clickX - position.left; // TODO: parseIntやめる
|
||||||
const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
|
const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
|
||||||
const browserWidth = window.innerWidth;
|
const browserWidth = window.innerWidth;
|
||||||
|
@ -321,8 +321,8 @@ function onTopHandleMousedown(evt) {
|
||||||
const main = rootEl.value;
|
const main = rootEl.value;
|
||||||
|
|
||||||
const base = evt.clientY;
|
const base = evt.clientY;
|
||||||
const height = parseInt(getComputedStyle(main, "").height, 10);
|
const height = Number.parseInt(getComputedStyle(main, "").height, 10);
|
||||||
const top = parseInt(getComputedStyle(main, "").top, 10);
|
const top = Number.parseInt(getComputedStyle(main, "").top, 10);
|
||||||
|
|
||||||
// 動かした時
|
// 動かした時
|
||||||
dragListen((me) => {
|
dragListen((me) => {
|
||||||
|
@ -349,8 +349,8 @@ function onRightHandleMousedown(evt) {
|
||||||
const main = rootEl.value;
|
const main = rootEl.value;
|
||||||
|
|
||||||
const base = evt.clientX;
|
const base = evt.clientX;
|
||||||
const width = parseInt(getComputedStyle(main, "").width, 10);
|
const width = Number.parseInt(getComputedStyle(main, "").width, 10);
|
||||||
const left = parseInt(getComputedStyle(main, "").left, 10);
|
const left = Number.parseInt(getComputedStyle(main, "").left, 10);
|
||||||
const browserWidth = window.innerWidth;
|
const browserWidth = window.innerWidth;
|
||||||
|
|
||||||
// 動かした時
|
// 動かした時
|
||||||
|
@ -375,8 +375,8 @@ function onBottomHandleMousedown(evt) {
|
||||||
const main = rootEl.value;
|
const main = rootEl.value;
|
||||||
|
|
||||||
const base = evt.clientY;
|
const base = evt.clientY;
|
||||||
const height = parseInt(getComputedStyle(main, "").height, 10);
|
const height = Number.parseInt(getComputedStyle(main, "").height, 10);
|
||||||
const top = parseInt(getComputedStyle(main, "").top, 10);
|
const top = Number.parseInt(getComputedStyle(main, "").top, 10);
|
||||||
const browserHeight = window.innerHeight;
|
const browserHeight = window.innerHeight;
|
||||||
|
|
||||||
// 動かした時
|
// 動かした時
|
||||||
|
@ -401,8 +401,8 @@ function onLeftHandleMousedown(evt) {
|
||||||
const main = rootEl.value;
|
const main = rootEl.value;
|
||||||
|
|
||||||
const base = evt.clientX;
|
const base = evt.clientX;
|
||||||
const width = parseInt(getComputedStyle(main, "").width, 10);
|
const width = Number.parseInt(getComputedStyle(main, "").width, 10);
|
||||||
const left = parseInt(getComputedStyle(main, "").left, 10);
|
const left = Number.parseInt(getComputedStyle(main, "").left, 10);
|
||||||
|
|
||||||
// 動かした時
|
// 動かした時
|
||||||
dragListen((me) => {
|
dragListen((me) => {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue