mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: ♻️ Finish rewrite and delete old settings backend
This commit is contained in:
parent
3ce71dd4df
commit
34ce25cc1d
10
app.vue
10
app.vue
|
|
@ -119,13 +119,13 @@ html.theme-changing * {
|
||||||
box-shadow 1s ease !important;
|
box-shadow 1s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-down-enter-active,
|
.slide-up-enter-active,
|
||||||
.slide-down-leave-active {
|
.slide-up-leave-active {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-down-enter-from,
|
.slide-up-enter-from,
|
||||||
.slide-down-leave-to {
|
.slide-up-leave-to {
|
||||||
transform: translateY(-100%);
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
5
bun.lock
5
bun.lock
|
|
@ -50,6 +50,7 @@
|
||||||
"vee-validate": "^4.15.0",
|
"vee-validate": "^4.15.0",
|
||||||
"virtua": "^0.40.4",
|
"virtua": "^0.40.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-sonner": "^1.3.2",
|
"vue-sonner": "^1.3.2",
|
||||||
"zod": "^3.24.3",
|
"zod": "^3.24.3",
|
||||||
|
|
@ -720,6 +721,8 @@
|
||||||
|
|
||||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||||
|
|
||||||
|
"@types/sortablejs": ["@types/sortablejs@1.15.8", "", {}, "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg=="],
|
||||||
|
|
||||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@types/video.js": ["@types/video.js@7.3.58", "", {}, "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ=="],
|
"@types/video.js": ["@types/video.js@7.3.58", "", {}, "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ=="],
|
||||||
|
|
@ -2098,6 +2101,8 @@
|
||||||
|
|
||||||
"vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="],
|
"vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="],
|
||||||
|
|
||||||
|
"vue-draggable-plus": ["vue-draggable-plus@0.6.0", "", { "dependencies": { "@types/sortablejs": "^1.15.8" } }, "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw=="],
|
||||||
|
|
||||||
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
|
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
|
||||||
|
|
||||||
"vue-sonner": ["vue-sonner@1.3.2", "", {}, "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A=="],
|
"vue-sonner": ["vue-sonner@1.3.2", "", {}, "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A=="],
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<FormLabel class="font-semibold tracking-tight" :as="CardTitle">
|
<FormLabel class="font-semibold tracking-tight" :as="CardTitle">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<CardDescription class="text-xs leading-none" v-if="description">
|
<FormDescription class="text-xs leading-none" v-if="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</CardDescription>
|
</FormDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
@ -19,7 +19,13 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "../ui/form";
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "../ui/form";
|
||||||
|
|
||||||
const { title, description } = defineProps<{
|
const { title, description } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
29
components/form/text.vue
Normal file
29
components/form/text.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{{ title }}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription v-if="description">
|
||||||
|
{{ description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "../ui/form";
|
||||||
|
|
||||||
|
const { title, description } = defineProps<{
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Card, CardTitle } from "../ui/card";
|
import { Card, CardTitle } from "../ui/card/index.ts";
|
||||||
// biome-ignore lint/style/useImportType: <explanation>
|
// biome-ignore lint/style/useImportType: <explanation>
|
||||||
import { preferences as prefs } from "./preferences.ts";
|
import { preferences as prefs } from "./preferences.ts";
|
||||||
import {
|
import {
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<template>
|
|
||||||
<Collapsible
|
|
||||||
:as="Card"
|
|
||||||
class="grid justify-normal items-center px-6 py-4 gap-4"
|
|
||||||
v-slot="{ open }"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-[1fr_auto] items-center gap-4">
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<CardTitle class="text-base">
|
|
||||||
{{ setting.title() }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{ setting.description() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CollapsibleTrigger :as-child="true">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
|
|
||||||
:title="open ? 'Collapse' : 'Expand'"
|
|
||||||
>
|
|
||||||
<ChevronDown class="duration-200" />
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</div>
|
|
||||||
<CollapsibleContent :as-child="true">
|
|
||||||
<CardFooter class="p-1">
|
|
||||||
<Textarea
|
|
||||||
:rows="10"
|
|
||||||
:model-value="setting.value"
|
|
||||||
@update:model-value="
|
|
||||||
(v) => {
|
|
||||||
setting.value = String(v);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</CardFooter>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ChevronDown } from "lucide-vue-next";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
|
||||||
import type { CodeSetting } from "~/settings.ts";
|
|
||||||
|
|
||||||
defineModel<CodeSetting>("setting", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -25,16 +25,17 @@ import Developer from "./developer.vue";
|
||||||
import Emojis from "./emojis/index.vue";
|
import Emojis from "./emojis/index.vue";
|
||||||
import Page from "./page.vue";
|
import Page from "./page.vue";
|
||||||
import { preferences } from "./preferences";
|
import { preferences } from "./preferences";
|
||||||
|
import Profile from "./profile.vue";
|
||||||
import Stats from "./stats.vue";
|
import Stats from "./stats.vue";
|
||||||
|
|
||||||
const pages = Object.values(preferences)
|
const pages = Object.values(preferences)
|
||||||
.map((p) => p.options.category)
|
.map((p) => p.options.category)
|
||||||
.filter((c) => c !== undefined)
|
.filter((c) => c !== undefined)
|
||||||
.map((c) => c.split("/")[0] as string)
|
.map((c) => c.split("/")[0] as string)
|
||||||
.concat(["Account", "Emojis", "Roles", "Developer", "About"])
|
.concat(["Account", "Emojis", "Developer", "About"])
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
.filter((c, i, a) => a.indexOf(c) === i);
|
.filter((c, i, a) => a.indexOf(c) === i);
|
||||||
const extraPages = ["Account", "Emojis", "Roles", "Developer", "About"];
|
const extraPages = ["Account", "Emojis", "Developer", "About"];
|
||||||
|
|
||||||
const icons: Record<string, Component> = {
|
const icons: Record<string, Component> = {
|
||||||
Account: UserIcon,
|
Account: UserIcon,
|
||||||
|
|
@ -75,11 +76,17 @@ const { account: author3 } = useAccountFromAcct(
|
||||||
client,
|
client,
|
||||||
"lina@social.lysand.org",
|
"lina@social.lysand.org",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
useListen("preferences:open", () => {
|
||||||
|
open.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog open v-if="identity">
|
<Dialog v-model:open="open" v-if="identity">
|
||||||
<DialogContent class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh]">
|
<DialogContent class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden">
|
||||||
<Tabs class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full" orientation="vertical"
|
<Tabs class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full" orientation="vertical"
|
||||||
:default-value="pages[0]">
|
:default-value="pages[0]">
|
||||||
<DialogHeader class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left">
|
<DialogHeader class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left">
|
||||||
|
|
@ -111,6 +118,11 @@ const { account: author3 } = useAccountFromAcct(
|
||||||
<Emojis />
|
<Emojis />
|
||||||
</Page>
|
</Page>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="Account" as-child>
|
||||||
|
<Page title="Account">
|
||||||
|
<Profile />
|
||||||
|
</Page>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="Developer" as-child>
|
<TabsContent value="Developer" as-child>
|
||||||
<Page title="Developer">
|
<Page title="Developer">
|
||||||
<Developer />
|
<Developer />
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<Collapsible :default-open="true">
|
|
||||||
<div class="grid grid-cols-[1fr_auto] gap-4 items-baseline">
|
|
||||||
<h2 class="text-2xl font-semibold tracking-tight">
|
|
||||||
{{ name }}
|
|
||||||
</h2>
|
|
||||||
<CollapsibleTrigger :as-child="true">
|
|
||||||
<Button size="icon" variant="outline" class="[&_svg]:data-[state=open]:-rotate-180">
|
|
||||||
<ChevronDown class="duration-200" />
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</div>
|
|
||||||
<CollapsibleContent class="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-3 mt-4">
|
|
||||||
<Emoji v-for="emoji in emojis" :key="emoji.id" :emoji="emoji" />
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Emoji as EmojiType } from "@versia/client/types";
|
|
||||||
import { ChevronDown } from "lucide-vue-next";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import Emoji from "./emoji.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
emojis: EmojiType[];
|
|
||||||
name: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
<template>
|
|
||||||
<DropdownMenu>
|
|
||||||
<Card
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'grid hover:cursor-pointer gap-4 items-center p-4',
|
|
||||||
canEdit
|
|
||||||
? 'grid-cols-[auto_1fr_auto]'
|
|
||||||
: 'grid-cols-[auto_1fr]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Avatar shape="square">
|
|
||||||
<AvatarImage :src="emoji.url" />
|
|
||||||
</Avatar>
|
|
||||||
<CardHeader class="p-0 gap-0 overflow-hidden">
|
|
||||||
<CardTitle as="span" class="text-sm font-mono truncate">
|
|
||||||
{{ emoji.shortcode }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{
|
|
||||||
emoji.global
|
|
||||||
? m.real_tame_moose_greet()
|
|
||||||
: m.witty_heroic_trout_cry()
|
|
||||||
}}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter class="p-0" v-if="canEdit">
|
|
||||||
<DropdownMenuTrigger :as-child="true">
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<Ellipsis />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<DropdownMenuContent class="min-w-48">
|
|
||||||
<DropdownMenuItem @click="editName">
|
|
||||||
<TextCursorInput />
|
|
||||||
{{ m.cuddly_such_swallow_hush() }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<!-- <DropdownMenuItem @click="editCaption">
|
|
||||||
<Captions />
|
|
||||||
<span>Add caption</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator /> -->
|
|
||||||
<DropdownMenuItem @click="_delete">
|
|
||||||
<Delete />
|
|
||||||
{{ m.tense_quick_cod_favor() }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { type Emoji, RolePermission } from "@versia/client/types";
|
|
||||||
import { Delete, Ellipsis, TextCursorInput } from "lucide-vue-next";
|
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
import { confirmModalService } from "~/components/modals/composable";
|
|
||||||
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
|
|
||||||
const { emoji } = defineProps<{
|
|
||||||
emoji: Emoji;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const permissions = usePermissions();
|
|
||||||
const canEdit =
|
|
||||||
(!emoji.global &&
|
|
||||||
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
|
|
||||||
permissions.value.includes(RolePermission.ManageEmojis);
|
|
||||||
|
|
||||||
const editName = async () => {
|
|
||||||
if (!identity.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await confirmModalService.confirm({
|
|
||||||
title: m.slimy_awful_florian_sail(),
|
|
||||||
defaultValue: emoji.shortcode,
|
|
||||||
confirmText: m.teary_antsy_panda_aid(),
|
|
||||||
inputType: "text",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.confirmed) {
|
|
||||||
const id = toast.loading(m.teary_tame_gull_bless());
|
|
||||||
try {
|
|
||||||
const { data } = await client.value.updateEmoji(emoji.id, {
|
|
||||||
shortcode: result.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.success(m.gaudy_lime_bison_adore());
|
|
||||||
|
|
||||||
identity.value.emojis = identity.value.emojis.map((e) =>
|
|
||||||
e.id === emoji.id ? data : e,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
toast.dismiss(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _delete = async () => {
|
|
||||||
if (!identity.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { confirmed } = await confirmModalService.confirm({
|
|
||||||
title: m.tense_quick_cod_favor(),
|
|
||||||
message: m.honest_factual_carp_aspire(),
|
|
||||||
confirmText: m.tense_quick_cod_favor(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
const id = toast.loading(m.weary_away_liger_zip());
|
|
||||||
try {
|
|
||||||
await client.value.deleteEmoji(emoji.id);
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.success(m.crisp_whole_canary_tear());
|
|
||||||
|
|
||||||
identity.value.emojis = identity.value.emojis.filter(
|
|
||||||
(e) => e.id !== emoji.id,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
toast.dismiss(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<DialogDescription class="sr-only">
|
<DialogDescription class="sr-only">
|
||||||
{{ m.frail_great_marten_pet() }}
|
{{ m.frail_great_marten_pet() }}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<form class="p-4 grid gap-6" @submit="submit">
|
<form class="grid gap-6" @submit="submit">
|
||||||
<div
|
<div
|
||||||
v-if="values.image"
|
v-if="values.image"
|
||||||
class="flex items-center justify-around *:size-20 *:p-2 *:rounded *:border *:shadow"
|
class="flex items-center justify-around *:size-20 *:p-2 *:rounded *:border *:shadow"
|
||||||
|
|
@ -125,32 +125,18 @@
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
v-slot="{ componentField, value, handleChange }"
|
v-slot="{ value, handleChange }"
|
||||||
v-if="hasEmojiAdmin"
|
v-if="hasEmojiAdmin"
|
||||||
name="global"
|
name="global"
|
||||||
:as="Card"
|
as-child
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormSwitch :title="m.pink_sharp_carp_work()" :description="m.dark_pretty_hyena_link()">
|
||||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
<Switch
|
||||||
>
|
:model-value="value"
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
@update:model-value="handleChange"
|
||||||
<FormLabel :as="CardTitle">
|
:disabled="isSubmitting"
|
||||||
{{ m.pink_sharp_carp_work() }}
|
/>
|
||||||
</FormLabel>
|
</FormSwitch>
|
||||||
<CardDescription>
|
|
||||||
{{ m.dark_pretty_hyena_link() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
:checked="value"
|
|
||||||
@update:checked="handleChange"
|
|
||||||
v-bind="componentField"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
@ -178,6 +164,7 @@ import { RolePermission } from "@versia/client/types";
|
||||||
import { useForm } from "vee-validate";
|
import { useForm } from "vee-validate";
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from "vue-sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import FormSwitch from "~/components/form/switch.vue";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
|
||||||
63
components/preferences/profile.ts
Normal file
63
components/preferences/profile.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
|
||||||
|
const characterRegex = new RegExp(/^[a-z0-9_-]+$/);
|
||||||
|
|
||||||
|
export const formSchema = (identity: Identity) =>
|
||||||
|
toTypedSchema(
|
||||||
|
z.strictObject({
|
||||||
|
banner: z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
(identity.instance.configuration.accounts
|
||||||
|
.header_size_limit ?? Number.POSITIVE_INFINITY),
|
||||||
|
m.civil_icy_ant_mend({
|
||||||
|
size: identity.instance.configuration.accounts
|
||||||
|
.header_size_limit,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
avatar: z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
(identity.instance.configuration.accounts
|
||||||
|
.avatar_size_limit ?? Number.POSITIVE_INFINITY),
|
||||||
|
m.zippy_caring_raven_edit({
|
||||||
|
size: identity.instance.configuration.accounts
|
||||||
|
.avatar_size_limit,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.or(z.string().url())
|
||||||
|
.optional(),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
identity.instance.configuration.accounts
|
||||||
|
.max_displayname_characters,
|
||||||
|
),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.regex(characterRegex, m.still_upper_otter_dine())
|
||||||
|
.max(
|
||||||
|
identity.instance.configuration.accounts
|
||||||
|
.max_username_characters,
|
||||||
|
),
|
||||||
|
bio: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
identity.instance.configuration.accounts
|
||||||
|
.max_note_characters,
|
||||||
|
),
|
||||||
|
bot: z.boolean().default(false),
|
||||||
|
locked: z.boolean().default(false),
|
||||||
|
discoverable: z.boolean().default(true),
|
||||||
|
fields: z.array(
|
||||||
|
z.strictObject({ name: z.string(), value: z.string() }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
185
components/preferences/profile.vue
Normal file
185
components/preferences/profile.vue
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<template>
|
||||||
|
<form v-if="identity" class="grid gap-6" @submit="save">
|
||||||
|
<Transition name="slide-up">
|
||||||
|
<Alert v-if="dirty" layout="button" class="absolute bottom-2 z-10 inset-x-2 w-[calc(100%-1rem)]">
|
||||||
|
<SaveOff class="size-4" />
|
||||||
|
<AlertTitle>Unsaved changes</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Click "apply" to save your changes.
|
||||||
|
</AlertDescription>
|
||||||
|
<Button variant="secondary" class="w-full" typ="submit" :disabled="submitting">Apply</Button>
|
||||||
|
</Alert>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
||||||
|
<TextInput :title="m.bright_late_osprey_renew()" :description="m.great_level_lamb_sway()">
|
||||||
|
<Input type="file" accept="image/*" @change="handleChange" @blur="handleBlur" />
|
||||||
|
</TextInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ setValue }" name="avatar">
|
||||||
|
<TextInput :title="m.safe_icy_bulldog_quell()">
|
||||||
|
<ImageUploader v-model:image="identity.account.avatar" @submit-file="(file) => setValue(file)"
|
||||||
|
@submit-url="(url) => setValue(url)" />
|
||||||
|
</TextInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
|
<TextInput :title="m.mild_known_mallard_jolt()" :description="m.lime_dry_skunk_loop()">
|
||||||
|
<Input v-bind="componentField" />
|
||||||
|
</TextInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="username">
|
||||||
|
<TextInput :title="m.neat_silly_dog_prosper()" :description="m.petty_plane_tadpole_earn()">
|
||||||
|
<Input v-bind="componentField" />
|
||||||
|
</TextInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="bio">
|
||||||
|
<TextInput :title="m.next_caring_ladybug_hack()" :description="m.stale_just_anaconda_earn()">
|
||||||
|
<Textarea rows="10" v-bind="componentField" />
|
||||||
|
</TextInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="fields">
|
||||||
|
<Fields :title="m.aqua_mealy_toucan_pride()" :value="value" @update:value="handleChange" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="bot" as-child>
|
||||||
|
<SwitchInput :title="m.gaudy_each_opossum_play()" :description="m.grassy_acidic_gadfly_cure()">
|
||||||
|
<Switch :model-value="value" @update:model-value="handleChange" />
|
||||||
|
</SwitchInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="locked" as-child>
|
||||||
|
<SwitchInput :title="m.dirty_moving_shark_emerge()" :description="m.bright_fun_mouse_boil()">
|
||||||
|
<Switch :model-value="value" @update:model-value="handleChange" />
|
||||||
|
</SwitchInput>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="discoverable" as-child>
|
||||||
|
<SwitchInput :title="m.red_vivid_cuckoo_spark()" :description="m.plain_zany_donkey_dart()">
|
||||||
|
<Switch :model-value="value" @update:model-value="handleChange" />
|
||||||
|
</SwitchInput>
|
||||||
|
</FormField>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { SaveOff } from "lucide-vue-next";
|
||||||
|
import { useForm } from "vee-validate";
|
||||||
|
import { toast } from "vue-sonner";
|
||||||
|
import SwitchInput from "~/components/form/switch.vue";
|
||||||
|
import TextInput from "~/components/form/text.vue";
|
||||||
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { FormField } from "../ui/form";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { formSchema } from "./profile";
|
||||||
|
import Fields from "./profile/fields.vue";
|
||||||
|
import ImageUploader from "./profile/image-uploader.vue";
|
||||||
|
|
||||||
|
const dirty = computed(() => form.meta.value.dirty);
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
if (!identity.value) {
|
||||||
|
throw new Error("Identity not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = computed(() => identity.value?.account as Identity["account"]);
|
||||||
|
const schema = formSchema(identity.value);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
validationSchema: schema,
|
||||||
|
initialValues: {
|
||||||
|
bio: account.value.source?.note ?? "",
|
||||||
|
bot: account.value.bot ?? false,
|
||||||
|
locked: account.value.locked ?? false,
|
||||||
|
discoverable: account.value.discoverable ?? true,
|
||||||
|
username: account.value.username,
|
||||||
|
name: account.value.display_name,
|
||||||
|
fields:
|
||||||
|
account.value.source?.fields.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
value: f.value,
|
||||||
|
})) ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const save = form.handleSubmit(async (values) => {
|
||||||
|
if (submitting.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
const id = toast.loading(m.jolly_noble_sloth_breathe());
|
||||||
|
|
||||||
|
const changedData = {
|
||||||
|
display_name:
|
||||||
|
values.name === account.value.display_name
|
||||||
|
? undefined
|
||||||
|
: values.name,
|
||||||
|
username:
|
||||||
|
values.username === account.value.username
|
||||||
|
? undefined
|
||||||
|
: values.username,
|
||||||
|
note:
|
||||||
|
values.bio === account.value.source?.note ? undefined : values.bio,
|
||||||
|
bot: values.bot === account.value.bot ? undefined : values.bot,
|
||||||
|
locked:
|
||||||
|
values.locked === account.value.locked ? undefined : values.locked,
|
||||||
|
discoverable:
|
||||||
|
values.discoverable === account.value.discoverable
|
||||||
|
? undefined
|
||||||
|
: values.discoverable,
|
||||||
|
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
|
||||||
|
fields_attributes: values.fields.every((field) =>
|
||||||
|
account.value.source?.fields?.some(
|
||||||
|
(f) => f.name === field.name && f.value === field.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? undefined
|
||||||
|
: values.fields,
|
||||||
|
header: values.banner ? values.banner : undefined,
|
||||||
|
avatar: values.avatar ? values.avatar : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.values(changedData).filter((v) => v !== undefined).length === 0
|
||||||
|
) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error(m.tough_alive_niklas_promise());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await client.value.updateCredentials(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.success(m.spry_honest_kestrel_arrive());
|
||||||
|
|
||||||
|
if (identity.value) {
|
||||||
|
identity.value.account = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.resetForm({
|
||||||
|
values: {
|
||||||
|
...form.values,
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -1,419 +0,0 @@
|
||||||
<template>
|
|
||||||
<Card v-if="identity" class="w-full max-h-full block overflow-y-auto">
|
|
||||||
<form class="p-4 grid gap-6" ref="formRef" @submit="handleSubmit">
|
|
||||||
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.bright_late_osprey_renew() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
@change="handleChange"
|
|
||||||
@blur="handleBlur"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.great_level_lamb_sway() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ setValue }" name="avatar">
|
|
||||||
<FormItem class="grid gap-1">
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.safe_icy_bulldog_quell() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<ImageUploader
|
|
||||||
v-model:image="identity.account.avatar"
|
|
||||||
@submit-file="(file) => setValue(file)"
|
|
||||||
@submit-url="(url) => setValue(url)"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="name">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.mild_known_mallard_jolt() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input v-bind="componentField" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.lime_dry_skunk_loop() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="username">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.neat_silly_dog_prosper() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input v-bind="componentField" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.petty_plane_tadpole_earn() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="bio">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.next_caring_ladybug_hack() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea rows="10" v-bind="componentField" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.stale_just_anaconda_earn() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="fields">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.aqua_mealy_toucan_pride() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div class="grid gap-4">
|
|
||||||
<div
|
|
||||||
v-for="(field, index) in value"
|
|
||||||
:key="index"
|
|
||||||
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))] gap-2"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="icon"
|
|
||||||
@click="
|
|
||||||
handleChange([
|
|
||||||
...value.slice(0, index),
|
|
||||||
...value.slice(index + 1),
|
|
||||||
])
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
</Button>
|
|
||||||
<Input
|
|
||||||
v-model="field.name"
|
|
||||||
placeholder="Name"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
handleChange([
|
|
||||||
...value.slice(0, index),
|
|
||||||
{ name: e, value: field.value },
|
|
||||||
...value.slice(index + 1),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="field.value"
|
|
||||||
placeholder="Value"
|
|
||||||
class="col-span-2"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
handleChange([
|
|
||||||
...value.slice(0, index),
|
|
||||||
{ name: field.name, value: e },
|
|
||||||
...value.slice(index + 1),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
@click="
|
|
||||||
handleChange([
|
|
||||||
...value,
|
|
||||||
{ name: '', value: '' },
|
|
||||||
])
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ m.front_north_eel_gulp() }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
v-slot="{ componentField, value, handleChange }"
|
|
||||||
name="bot"
|
|
||||||
:as="Card"
|
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
|
||||||
>
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<FormLabel :as="CardTitle">
|
|
||||||
{{ m.gaudy_each_opossum_play() }}
|
|
||||||
</FormLabel>
|
|
||||||
<CardDescription>
|
|
||||||
{{ m.grassy_acidic_gadfly_cure() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
:checked="value"
|
|
||||||
@update:checked="handleChange"
|
|
||||||
v-bind="componentField"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
v-slot="{ componentField, value, handleChange }"
|
|
||||||
name="locked"
|
|
||||||
:as="Card"
|
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
|
||||||
>
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<FormLabel :as="CardTitle">
|
|
||||||
{{ m.dirty_moving_shark_emerge() }}
|
|
||||||
</FormLabel>
|
|
||||||
<CardDescription>
|
|
||||||
{{ m.bright_fun_mouse_boil() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
:checked="value"
|
|
||||||
@update:checked="handleChange"
|
|
||||||
v-bind="componentField"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
v-slot="{ componentField, value, handleChange }"
|
|
||||||
name="discoverable"
|
|
||||||
:as="Card"
|
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
|
||||||
>
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<FormLabel :as="CardTitle">
|
|
||||||
{{ m.red_vivid_cuckoo_spark() }}
|
|
||||||
</FormLabel>
|
|
||||||
<CardDescription>
|
|
||||||
{{ m.plain_zany_donkey_dart() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
:checked="value"
|
|
||||||
@update:checked="handleChange"
|
|
||||||
v-bind="componentField"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
|
||||||
import type { ResponseError } from "@versia/client";
|
|
||||||
import { Trash } from "lucide-vue-next";
|
|
||||||
import { useForm } from "vee-validate";
|
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "~/components/ui/form";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { Switch } from "~/components/ui/switch";
|
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
import ImageUploader from "./image-uploader.vue";
|
|
||||||
|
|
||||||
if (!identity.value) {
|
|
||||||
throw new Error("Identity not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = ref(identity.value.account);
|
|
||||||
|
|
||||||
const formSchema = toTypedSchema(
|
|
||||||
z.object({
|
|
||||||
banner: z
|
|
||||||
.instanceof(File)
|
|
||||||
.refine(
|
|
||||||
(v) =>
|
|
||||||
v.size <=
|
|
||||||
(identity.value?.instance.configuration.accounts
|
|
||||||
.header_size_limit ?? Number.POSITIVE_INFINITY),
|
|
||||||
m.civil_icy_ant_mend({
|
|
||||||
size: identity.value?.instance.configuration.accounts
|
|
||||||
.header_size_limit,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
avatar: z
|
|
||||||
.instanceof(File)
|
|
||||||
.refine(
|
|
||||||
(v) =>
|
|
||||||
v.size <=
|
|
||||||
(identity.value?.instance.configuration.accounts
|
|
||||||
.avatar_size_limit ?? Number.POSITIVE_INFINITY),
|
|
||||||
m.zippy_caring_raven_edit({
|
|
||||||
size: identity.value?.instance.configuration.accounts
|
|
||||||
.avatar_size_limit,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.or(z.string().url())
|
|
||||||
.optional(),
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.max(
|
|
||||||
identity.value.instance.configuration.accounts
|
|
||||||
.max_displayname_characters,
|
|
||||||
),
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
|
||||||
.max(
|
|
||||||
identity.value.instance.configuration.accounts
|
|
||||||
.max_username_characters,
|
|
||||||
),
|
|
||||||
bio: z
|
|
||||||
.string()
|
|
||||||
.max(
|
|
||||||
identity.value.instance.configuration.accounts
|
|
||||||
.max_note_characters,
|
|
||||||
),
|
|
||||||
bot: z.boolean(),
|
|
||||||
locked: z.boolean(),
|
|
||||||
discoverable: z.boolean(),
|
|
||||||
fields: z.array(z.object({ name: z.string(), value: z.string() })),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
validationSchema: formSchema,
|
|
||||||
initialValues: {
|
|
||||||
bio: account.value.source?.note ?? "",
|
|
||||||
bot: account.value.bot ?? false,
|
|
||||||
locked: account.value.locked ?? false,
|
|
||||||
discoverable: account.value.discoverable ?? true,
|
|
||||||
username: account.value.username,
|
|
||||||
name: account.value.display_name,
|
|
||||||
fields: account.value.source?.fields ?? [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
|
||||||
const id = toast.loading(m.jolly_noble_sloth_breathe());
|
|
||||||
|
|
||||||
const changedData = {
|
|
||||||
display_name:
|
|
||||||
values.name === account.value.display_name
|
|
||||||
? undefined
|
|
||||||
: values.name,
|
|
||||||
username:
|
|
||||||
values.username === account.value.username
|
|
||||||
? undefined
|
|
||||||
: values.username,
|
|
||||||
note:
|
|
||||||
values.bio === account.value.source?.note ? undefined : values.bio,
|
|
||||||
bot: values.bot === account.value.bot ? undefined : values.bot,
|
|
||||||
locked:
|
|
||||||
values.locked === account.value.locked ? undefined : values.locked,
|
|
||||||
discoverable:
|
|
||||||
values.discoverable === account.value.discoverable
|
|
||||||
? undefined
|
|
||||||
: values.discoverable,
|
|
||||||
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
|
|
||||||
fields_attributes: values.fields.every((field) =>
|
|
||||||
account.value.source?.fields?.some(
|
|
||||||
(f) => f.name === field.name && f.value === field.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? undefined
|
|
||||||
: values.fields,
|
|
||||||
header: values.banner ? values.banner : undefined,
|
|
||||||
avatar: values.avatar ? values.avatar : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.values(changedData).filter((v) => v !== undefined).length === 0
|
|
||||||
) {
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.error(m.tough_alive_niklas_promise());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await client.value.updateCredentials(
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.success(m.spry_honest_kestrel_arrive());
|
|
||||||
|
|
||||||
if (identity.value) {
|
|
||||||
identity.value.account = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
account.value = data;
|
|
||||||
form.resetForm({
|
|
||||||
values: {
|
|
||||||
...form.values,
|
|
||||||
...values,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
const error = e as ResponseError<{ error: string }>;
|
|
||||||
|
|
||||||
toast.dismiss(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const formRef = ref<HTMLFormElement | null>(null);
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
submitForm: () => handleSubmit(),
|
|
||||||
dirty: computed(() => form.meta.value.dirty),
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
104
components/preferences/profile/fields.vue
Normal file
104
components/preferences/profile/fields.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{{ title }}
|
||||||
|
<Button type="button" variant="secondary" size="icon" class="ml-auto" @click="addField()" :title="m.front_north_eel_gulp()">
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<VueDraggable class="grid gap-4" v-model="list" :animation="200" handle=".drag-handle">
|
||||||
|
<div v-for="(field, index) in list" :key="field.id"
|
||||||
|
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))_auto] gap-2">
|
||||||
|
<Button as="span" variant="ghost" size="icon" class="drag-handle cursor-grab">
|
||||||
|
<GripVertical />
|
||||||
|
</Button>
|
||||||
|
<Input :model-value="field.name" placeholder="Name" @update:model-value="
|
||||||
|
(e) => updateKey(index, String(e))
|
||||||
|
" />
|
||||||
|
<Input :model-value="field.value" placeholder="Value" class="col-span-2" @update:model-value="
|
||||||
|
(e) => updateValue(index, String(e))
|
||||||
|
" />
|
||||||
|
<Button type="button" variant="secondary" size="icon" @click="removeField(index)">
|
||||||
|
<Trash />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</VueDraggable>
|
||||||
|
<FormMessage />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { GripVertical, Plus, Trash } from "lucide-vue-next";
|
||||||
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/components/ui/form";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
|
||||||
|
const { title } = defineProps<{
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const value = defineModel<{ name: string; value: string }[]>("value", {
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = ref<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
value.value.map((item, index) => ({
|
||||||
|
id: String(index),
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
list,
|
||||||
|
(newList) => {
|
||||||
|
value.value = newList.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateKey = (index: number, key: string) => {
|
||||||
|
if (!list.value[index]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.value[index].name = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateValue = (index: number, val: string) => {
|
||||||
|
if (!list.value[index]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.value[index].value = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeField = (index: number) => {
|
||||||
|
list.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addField = () => {
|
||||||
|
list.value.push({ name: "", value: "", id: String(list.value.length) });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -21,10 +21,10 @@
|
||||||
<DialogDescription class="sr-only">
|
<DialogDescription class="sr-only">
|
||||||
{{ m.suave_broad_albatross_drop() }}
|
{{ m.suave_broad_albatross_drop() }}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<form class="p-4 grid gap-6" @submit="submit">
|
<form class="grid gap-6" @submit="submit">
|
||||||
<Tabs
|
<Tabs
|
||||||
default-value="upload"
|
default-value="upload"
|
||||||
class="mt-2 data-[component=tabpanel]:*:mt-6"
|
class="mt-2 *:data-[slot=tabs-content]:mt-2"
|
||||||
>
|
>
|
||||||
<TabsList class="w-full *:w-full">
|
<TabsList class="w-full *:w-full">
|
||||||
<TabsTrigger value="upload">
|
<TabsTrigger value="upload">
|
||||||
|
|
@ -222,7 +222,7 @@ const emailToGravatar = async (email: string) => {
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const gravatarUrl = ref<string | undefined>(undefined);
|
const gravatarUrl = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
const { handleSubmit, isSubmitting, values } = useForm({
|
const { handleSubmit, isSubmitting } = useForm({
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<Card class="grid grid-cols-[1fr_auto] items-center px-6 py-4 gap-2">
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<CardTitle class="text-base">
|
|
||||||
{{ setting.title() }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{ setting.description() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter class="p-0">
|
|
||||||
<Select :model-value="setting.value" @update:model-value="v => { setting.value = v }">
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select an option" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem v-for="option of setting.options" :value="option.value">
|
|
||||||
{{ option.label() }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import type { EnumSetting } from "~/settings.ts";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
|
|
||||||
defineModel<EnumSetting>("setting", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<Card class="grid grid-rows-[1fr_auto] xl:grid-rows-none xl:grid-cols-[1fr_auto] items-center px-6 py-4 gap-4">
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<CardTitle class="text-base">
|
|
||||||
{{ setting.title() }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{ setting.description() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter class="p-0">
|
|
||||||
<Input :model-value="setting.value" @update:model-value="v => { setting.value = String(v) }" />
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import type { StringSetting } from "~/settings.ts";
|
|
||||||
|
|
||||||
defineModel<StringSetting>("setting", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<Card class="grid grid-cols-[1fr_auto] items-center px-6 py-4 gap-2">
|
|
||||||
<CardHeader class="space-y-0.5 p-0">
|
|
||||||
<CardTitle class="text-base">
|
|
||||||
{{ setting.title() }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{ setting.description() }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter class="p-0">
|
|
||||||
<Switch :disabled="setting.notImplemented" :checked="setting.value" @update:checked="v => { setting.value = v }" />
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import { Switch } from "~/components/ui/switch";
|
|
||||||
import type { BooleanSetting } from "~/settings.ts";
|
|
||||||
|
|
||||||
defineModel<BooleanSetting>("setting", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
<template>
|
|
||||||
<Dialog v-model:open="open">
|
|
||||||
<DialogTrigger>
|
|
||||||
<slot />
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>
|
|
||||||
{{ m.whole_icy_puffin_smile() }}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription class="sr-only">
|
|
||||||
{{ m.frail_great_marten_pet() }}
|
|
||||||
</DialogDescription>
|
|
||||||
<form class="grid gap-6" @submit="submit">
|
|
||||||
<div
|
|
||||||
v-if="values.image"
|
|
||||||
class="flex items-center justify-around *:size-20 *:p-2 *:rounded *:border *:shadow"
|
|
||||||
>
|
|
||||||
<div class="bg-background">
|
|
||||||
<img
|
|
||||||
class="h-full object-cover"
|
|
||||||
:src="createObjectURL(values.image as File)"
|
|
||||||
:alt="values.alt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="bg-zinc-700">
|
|
||||||
<img
|
|
||||||
class="h-full object-cover"
|
|
||||||
:src="createObjectURL(values.image as File)"
|
|
||||||
:alt="values.alt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="bg-zinc-400">
|
|
||||||
<img
|
|
||||||
class="h-full object-cover"
|
|
||||||
:src="createObjectURL(values.image as File)"
|
|
||||||
:alt="values.alt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="bg-foreground">
|
|
||||||
<img
|
|
||||||
class="h-full object-cover"
|
|
||||||
:src="createObjectURL(values.image as File)"
|
|
||||||
:alt="values.alt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField v-slot="{ handleChange, handleBlur }" name="image">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.active_direct_bear_compose() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
@change="(e: any) => {
|
|
||||||
handleChange(e);
|
|
||||||
|
|
||||||
if (!values.shortcode) {
|
|
||||||
setFieldValue('shortcode', e.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
@blur="handleBlur"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.lime_late_millipede_urge() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="shortcode">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.happy_mild_fox_gleam() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
v-bind="componentField"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.glad_day_kestrel_amaze() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="category">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.short_cute_jackdaw_comfort() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
v-bind="componentField"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="alt">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{{ m.watery_left_shrimp_bless() }}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
rows="2"
|
|
||||||
v-bind="componentField"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{{ m.weird_fun_jurgen_arise() }}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
v-slot="{ value, handleChange }"
|
|
||||||
v-if="hasEmojiAdmin"
|
|
||||||
name="global"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<FormSwitch :title="m.pink_sharp_carp_work()" :description="m.dark_pretty_hyena_link()">
|
|
||||||
<Switch
|
|
||||||
:model-value="value"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</FormSwitch>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose :as-child="true">
|
|
||||||
<Button variant="outline" :disabled="isSubmitting">
|
|
||||||
{{ m.soft_bold_ant_attend() }}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="default"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
{{ m.flat_safe_haddock_gaze() }}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
|
||||||
import { RolePermission } from "@versia/client/types";
|
|
||||||
import { useForm } from "vee-validate";
|
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import FormSwitch from "~/components/form/switch.vue";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "~/components/ui/form";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { Switch } from "~/components/ui/switch";
|
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
|
|
||||||
const open = ref(false);
|
|
||||||
const permissions = usePermissions();
|
|
||||||
const hasEmojiAdmin = permissions.value.includes(RolePermission.ManageEmojis);
|
|
||||||
const createObjectURL = URL.createObjectURL;
|
|
||||||
|
|
||||||
const formSchema = toTypedSchema(
|
|
||||||
z.object({
|
|
||||||
image: z
|
|
||||||
.instanceof(File, {
|
|
||||||
message: m.sound_topical_gopher_offer(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(v) =>
|
|
||||||
v.size <=
|
|
||||||
(identity.value?.instance.configuration.emojis
|
|
||||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY),
|
|
||||||
m.orange_weird_parakeet_hug({
|
|
||||||
count:
|
|
||||||
identity.value?.instance.configuration.emojis
|
|
||||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
shortcode: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(
|
|
||||||
identity.value?.instance.configuration.emojis
|
|
||||||
.max_emoji_shortcode_characters ?? Number.POSITIVE_INFINITY,
|
|
||||||
m.solid_inclusive_owl_hug({
|
|
||||||
count:
|
|
||||||
identity.value?.instance.configuration.emojis
|
|
||||||
.max_emoji_shortcode_characters ??
|
|
||||||
Number.POSITIVE_INFINITY,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.regex(emojiValidator),
|
|
||||||
global: z.boolean().default(false),
|
|
||||||
category: z
|
|
||||||
.string()
|
|
||||||
.max(
|
|
||||||
64,
|
|
||||||
m.home_cool_orangutan_hug({
|
|
||||||
count: 64,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
alt: z
|
|
||||||
.string()
|
|
||||||
.max(
|
|
||||||
identity.value?.instance.configuration.emojis
|
|
||||||
.max_emoji_description_characters ??
|
|
||||||
Number.POSITIVE_INFINITY,
|
|
||||||
m.key_ago_hound_emerge({
|
|
||||||
count:
|
|
||||||
identity.value?.instance.configuration.emojis
|
|
||||||
.max_emoji_description_characters ??
|
|
||||||
Number.POSITIVE_INFINITY,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
|
||||||
validationSchema: formSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submit = handleSubmit(async (values) => {
|
|
||||||
if (!identity.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = toast.loading(m.factual_gray_mouse_believe());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await client.value.uploadEmoji(
|
|
||||||
values.shortcode,
|
|
||||||
values.image,
|
|
||||||
{
|
|
||||||
alt: values.alt,
|
|
||||||
category: values.category,
|
|
||||||
global: values.global,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.success(m.cool_trite_gull_quiz());
|
|
||||||
|
|
||||||
identity.value.emojis = [...identity.value.emojis, data];
|
|
||||||
open.value = false;
|
|
||||||
} catch {
|
|
||||||
toast.dismiss(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<Card
|
<Card
|
||||||
class="flex-row gap-2 p-2 truncate items-center"
|
class="flex-row gap-2 p-2 truncate items-center"
|
||||||
:class="naked ? 'p-0 bg-transparent ring-0 border-none' : ''"
|
:class="naked ? 'p-0 bg-transparent ring-0 border-none shadow-none' : ''"
|
||||||
>
|
>
|
||||||
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
|
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
|
||||||
<CardContent class="leading-tight">
|
<CardContent class="leading-tight">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronsUpDown, DownloadCloud, Pen, UserPlus } from "lucide-vue-next";
|
import {
|
||||||
|
ChevronsUpDown,
|
||||||
|
Cog,
|
||||||
|
DownloadCloud,
|
||||||
|
Pen,
|
||||||
|
UserPlus,
|
||||||
|
} from "lucide-vue-next";
|
||||||
import TinyCard from "~/components/profiles/tiny-card.vue";
|
import TinyCard from "~/components/profiles/tiny-card.vue";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -37,6 +43,10 @@ const { $pwa } = useNuxtApp();
|
||||||
{{ m.salty_aloof_turkey_nudge() }}
|
{{ m.salty_aloof_turkey_nudge() }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-if="identity" size="lg" variant="secondary" @click="useEvent('preferences:open')">
|
||||||
|
<Cog />
|
||||||
|
Preferences
|
||||||
|
</Button>
|
||||||
<Button v-if="$pwa?.needRefresh" variant="destructive" size="lg"
|
<Button v-if="$pwa?.needRefresh" variant="destructive" size="lg"
|
||||||
class="w-full group-data-[collapsible=icon]:px-4" @click="$pwa?.updateServiceWorker(true)">
|
class="w-full group-data-[collapsible=icon]:px-4" @click="$pwa?.updateServiceWorker(true)">
|
||||||
<DownloadCloud />
|
<DownloadCloud />
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,6 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<SidebarGroup v-if="identity" class="mt-auto">
|
|
||||||
<SidebarGroupLabel>{{
|
|
||||||
m.close_short_kitten_coax()
|
|
||||||
}}</SidebarGroupLabel>
|
|
||||||
<NavGroup :items="sidebarConfig.navMain" />
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<FooterActions />
|
<FooterActions />
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|
@ -38,6 +32,5 @@ import {
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
import FooterActions from "./footer/footer-actions.vue";
|
import FooterActions from "./footer/footer-actions.vue";
|
||||||
import InstanceHeader from "./instance/instance-header.vue";
|
import InstanceHeader from "./instance/instance-header.vue";
|
||||||
import NavGroup from "./navigation/nav-group.vue";
|
|
||||||
import NavItems from "./navigation/nav-items.vue";
|
import NavItems from "./navigation/nav-items.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,9 @@
|
||||||
import {
|
import { BedSingle, Bell, Globe, House, MapIcon } from "lucide-vue-next";
|
||||||
BedSingle,
|
|
||||||
Bell,
|
|
||||||
Globe,
|
|
||||||
House,
|
|
||||||
MapIcon,
|
|
||||||
Settings2,
|
|
||||||
} from "lucide-vue-next";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
import type { SidebarConfig } from "~/types/sidebar";
|
import type { SidebarConfig } from "~/types/sidebar";
|
||||||
|
|
||||||
export const sidebarConfig: SidebarConfig = {
|
export const sidebarConfig: SidebarConfig = {
|
||||||
navMain: [
|
navMain: [],
|
||||||
{
|
|
||||||
title: m.patchy_seemly_hound_grace(),
|
|
||||||
url: "/preferences",
|
|
||||||
icon: Settings2,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: m.factual_arable_jurgen_endure(),
|
|
||||||
url: "/preferences/account",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: m.tough_clean_wolf_gleam(),
|
|
||||||
url: "/preferences/appearance",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: m.legal_best_tadpole_rise(),
|
|
||||||
url: "/preferences/behaviour",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: m.novel_trite_sloth_adapt(),
|
|
||||||
url: "/preferences/emojis",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: m.safe_green_mink_cook(),
|
|
||||||
url: "/preferences/roles",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
other: [
|
other: [
|
||||||
{
|
{
|
||||||
title: m.bland_chunky_sparrow_propel(),
|
title: m.bland_chunky_sparrow_propel(),
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export const useConfig = () => {
|
||||||
"jessew@social.lysand.org",
|
"jessew@social.lysand.org",
|
||||||
"jessew@beta.versia.social",
|
"jessew@beta.versia.social",
|
||||||
"jessew@versia.social",
|
"jessew@versia.social",
|
||||||
|
"jessew@vs.cpluspatch.com",
|
||||||
"aprl@social.lysand.org",
|
"aprl@social.lysand.org",
|
||||||
"aprl@beta.versia.social",
|
"aprl@beta.versia.social",
|
||||||
"aprl@versia.social",
|
"aprl@versia.social",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type ApplicationEvents = {
|
||||||
"account:update": Account;
|
"account:update": Account;
|
||||||
"attachment:view": Attachment;
|
"attachment:view": Attachment;
|
||||||
"identity:change": Identity;
|
"identity:change": Identity;
|
||||||
|
"preferences:open": undefined;
|
||||||
error: {
|
error: {
|
||||||
code: string;
|
code: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { StorageSerializers } from "@vueuse/core";
|
import { StorageSerializers } from "@vueuse/core";
|
||||||
import { preferences as prefs } from "~/components/preferences2/preferences.ts";
|
import { preferences as prefs } from "~/components/preferences/preferences";
|
||||||
|
|
||||||
type SerializedPreferences = {
|
type SerializedPreferences = {
|
||||||
[K in keyof typeof prefs]: (typeof prefs)[K]["options"]["defaultValue"];
|
[K in keyof typeof prefs]: (typeof prefs)[K]["options"]["defaultValue"];
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
import ComposerDialog from "~/components/composer/dialog.vue";
|
import ComposerDialog from "~/components/composer/dialog.vue";
|
||||||
import AuthRequired from "~/components/errors/AuthRequired.vue";
|
import AuthRequired from "~/components/errors/AuthRequired.vue";
|
||||||
import MobileNavbar from "~/components/navigation/mobile-navbar.vue";
|
import MobileNavbar from "~/components/navigation/mobile-navbar.vue";
|
||||||
import Preferences from "~/components/preferences2/index.vue";
|
import Preferences from "~/components/preferences/index.vue";
|
||||||
import AppSidebar from "~/components/sidebars/sidebar.vue";
|
import AppSidebar from "~/components/sidebars/sidebar.vue";
|
||||||
import { SidebarProvider } from "~/components/ui/sidebar";
|
import { SidebarProvider } from "~/components/ui/sidebar";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ in
|
||||||
|
|
||||||
pnpmDeps = pnpm.fetchDeps {
|
pnpmDeps = pnpm.fetchDeps {
|
||||||
inherit (finalAttrs) pname version src;
|
inherit (finalAttrs) pname version src;
|
||||||
hash = "sha256-JGZTMusNZf3PQqGcAhsO2J1q6Tj55BgNcgxAUqMN6S0=";
|
hash = "sha256-WYZDL8ankh/S2DrQMU9PRA0z8uWS7QO/nPp/i61mrVY=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@
|
||||||
"vee-validate": "^4.15.0",
|
"vee-validate": "^4.15.0",
|
||||||
"virtua": "^0.40.4",
|
"virtua": "^0.40.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-sonner": "^1.3.2",
|
"vue-sonner": "^1.3.2",
|
||||||
"zod": "^3.24.3"
|
"zod": "^3.24.3"
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="md:px-8 px-4 py-2 max-w-7xl mx-auto relative">
|
|
||||||
<ProfileEditor ref="profileEditor" />
|
|
||||||
|
|
||||||
<Transition name="slide-down">
|
|
||||||
<Alert
|
|
||||||
v-if="profileEditor?.dirty"
|
|
||||||
layout="button"
|
|
||||||
class="mb-4 absolute top-4 inset-x-4 w-[calc(100%-2rem)]"
|
|
||||||
>
|
|
||||||
<Check class="size-4" />
|
|
||||||
<AlertTitle>Unsaved changes</AlertTitle>
|
|
||||||
<AlertDescription >
|
|
||||||
Click "apply" to save your changes.
|
|
||||||
</AlertDescription>
|
|
||||||
<!-- Add pl-4 because Alert is adding additional padding, which we don't want -->
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
@click="profileEditor?.submitForm"
|
|
||||||
class="w-full !pl-4"
|
|
||||||
>Apply</Button
|
|
||||||
>
|
|
||||||
</Alert>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Check } from "lucide-vue-next";
|
|
||||||
// biome-ignore lint/style/useImportType: <explanation>
|
|
||||||
import ProfileEditor from "~/components/preferences/profile/editor.vue";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: m.actual_mean_cow_dare(),
|
|
||||||
});
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: "app",
|
|
||||||
breadcrumbs: () => [
|
|
||||||
{
|
|
||||||
text: m.broad_whole_herring_reside(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requiresAuth: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profileEditor = ref<InstanceType<typeof ProfileEditor> | null>(null);
|
|
||||||
</script>
|
|
||||||
|
|
@ -146,6 +146,9 @@ importers:
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.8.3)
|
version: 3.5.13(typescript@5.8.3)
|
||||||
|
vue-draggable-plus:
|
||||||
|
specifier: ^0.6.0
|
||||||
|
version: 0.6.0(@types/sortablejs@1.15.8)
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.5.1
|
specifier: ^4.5.1
|
||||||
version: 4.5.1(vue@3.5.13(typescript@5.8.3))
|
version: 4.5.1(vue@3.5.13(typescript@5.8.3))
|
||||||
|
|
@ -1873,6 +1876,9 @@ packages:
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
'@types/sortablejs@1.15.8':
|
||||||
|
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
|
|
@ -5021,6 +5027,15 @@ packages:
|
||||||
vue-devtools-stub@0.1.0:
|
vue-devtools-stub@0.1.0:
|
||||||
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
|
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
|
||||||
|
|
||||||
|
vue-draggable-plus@0.6.0:
|
||||||
|
resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/sortablejs': ^1.15.0
|
||||||
|
'@vue/composition-api': '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
vue-router@4.5.1:
|
vue-router@4.5.1:
|
||||||
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -7124,6 +7139,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
|
'@types/sortablejs@1.15.8': {}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
'@types/video.js@7.3.58': {}
|
'@types/video.js@7.3.58': {}
|
||||||
|
|
@ -10744,6 +10761,10 @@ snapshots:
|
||||||
|
|
||||||
vue-devtools-stub@0.1.0: {}
|
vue-devtools-stub@0.1.0: {}
|
||||||
|
|
||||||
|
vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8):
|
||||||
|
dependencies:
|
||||||
|
'@types/sortablejs': 1.15.8
|
||||||
|
|
||||||
vue-router@4.5.1(vue@3.5.13(typescript@5.8.3)):
|
vue-router@4.5.1(vue@3.5.13(typescript@5.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
|
|
|
||||||
339
settings.ts
339
settings.ts
|
|
@ -1,339 +0,0 @@
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
|
|
||||||
export enum SettingType {
|
|
||||||
String = "string",
|
|
||||||
Boolean = "boolean",
|
|
||||||
Enum = "enum",
|
|
||||||
Float = "float",
|
|
||||||
Integer = "integer",
|
|
||||||
Code = "code",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Setting = {
|
|
||||||
title: () => string;
|
|
||||||
description: () => string;
|
|
||||||
notImplemented?: boolean;
|
|
||||||
type: SettingType;
|
|
||||||
value: unknown;
|
|
||||||
page: SettingPages;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StringSetting = Setting & {
|
|
||||||
type: SettingType.String;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BooleanSetting = Setting & {
|
|
||||||
type: SettingType.Boolean;
|
|
||||||
value: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnumSetting = Setting & {
|
|
||||||
type: SettingType.Enum;
|
|
||||||
value: string;
|
|
||||||
options: {
|
|
||||||
value: string;
|
|
||||||
label: () => string;
|
|
||||||
icon?: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FloatSetting = Setting & {
|
|
||||||
type: SettingType.Float;
|
|
||||||
value: number;
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
step: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IntegerSetting = Setting & {
|
|
||||||
type: SettingType.Integer;
|
|
||||||
value: number;
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
step: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CodeSetting = Setting & {
|
|
||||||
type: SettingType.Code;
|
|
||||||
value: string;
|
|
||||||
language: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum SettingPages {
|
|
||||||
Account = "account",
|
|
||||||
Emojis = "emojis",
|
|
||||||
Behaviour = "behaviour",
|
|
||||||
Appearance = "appearance",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SettingIds {
|
|
||||||
Language = "language",
|
|
||||||
Mfm = "mfm",
|
|
||||||
CustomCSS = "custom-css",
|
|
||||||
Theme = "theme",
|
|
||||||
CustomEmojis = "custom-emojis",
|
|
||||||
ShowContentWarning = "show-content-warning",
|
|
||||||
PopupAvatarHover = "popup-avatar-hover",
|
|
||||||
InfiniteScroll = "infinite-scroll",
|
|
||||||
ConfirmDelete = "confirm-delete",
|
|
||||||
ConfirmFollow = "confirm-follow",
|
|
||||||
ConfirmReblog = "confirm-reblog",
|
|
||||||
ConfirmLike = "confirm-favourite",
|
|
||||||
CtrlEnterToSend = "ctrl-enter-to-send",
|
|
||||||
EmojiTheme = "emoji-theme",
|
|
||||||
BackgroundURL = "background-url",
|
|
||||||
NotificationsSidebar = "notifications-sidebar",
|
|
||||||
AvatarShape = "avatar-shape",
|
|
||||||
DefaultVisibility = "default-visibility",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const settings = (): Record<SettingIds, Setting> => {
|
|
||||||
return {
|
|
||||||
[SettingIds.Mfm]: {
|
|
||||||
title: m.quaint_clear_boar_attend,
|
|
||||||
description: m.aloof_helpful_larva_spur,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: false,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
notImplemented: true,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.DefaultVisibility]: {
|
|
||||||
title: m.loud_tense_kitten_exhale,
|
|
||||||
description: m.vivid_last_crocodile_offer,
|
|
||||||
type: SettingType.Enum,
|
|
||||||
value: "public",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "public",
|
|
||||||
label: m.lost_trick_dog_grace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "unlisted",
|
|
||||||
label: m.funny_slow_jannes_walk,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "private",
|
|
||||||
label: m.grassy_empty_raven_startle,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "direct",
|
|
||||||
label: m.pretty_bold_baboon_wave,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as EnumSetting,
|
|
||||||
[SettingIds.Language]: {
|
|
||||||
title: m.pretty_born_jackal_dial,
|
|
||||||
description: m.tired_happy_lobster_pet,
|
|
||||||
type: SettingType.Enum,
|
|
||||||
value: "en",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "en",
|
|
||||||
label: () =>
|
|
||||||
m.keen_aware_goldfish_thrive(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
locale: "en",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "fr",
|
|
||||||
label: () =>
|
|
||||||
m.vivid_mellow_sawfish_approve(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
locale: "fr",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "en-pt",
|
|
||||||
label: () => m.these_awful_ape_reside(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as EnumSetting,
|
|
||||||
[SettingIds.AvatarShape]: {
|
|
||||||
title: m.fit_cool_bulldog_dine,
|
|
||||||
description: m.agent_misty_firefox_arise,
|
|
||||||
type: SettingType.Enum,
|
|
||||||
value: "square",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "circle",
|
|
||||||
label: m.polite_awful_ladybug_greet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "square",
|
|
||||||
label: m.sad_each_cowfish_lock,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as EnumSetting,
|
|
||||||
[SettingIds.CustomCSS]: {
|
|
||||||
title: m.smart_awake_dachshund_view,
|
|
||||||
description: m.loved_topical_rat_coax,
|
|
||||||
type: SettingType.Code,
|
|
||||||
value: "",
|
|
||||||
language: "css",
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as CodeSetting,
|
|
||||||
[SettingIds.Theme]: {
|
|
||||||
title: m.hour_elegant_mink_grip,
|
|
||||||
description: m.male_stout_florian_feast,
|
|
||||||
type: SettingType.Enum,
|
|
||||||
value: "dark",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "dark",
|
|
||||||
label: m.wise_neat_ox_buzz,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "light",
|
|
||||||
label: m.each_strong_snail_aid,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "system",
|
|
||||||
label: m.helpful_raw_seal_nurture,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as EnumSetting,
|
|
||||||
[SettingIds.CustomEmojis]: {
|
|
||||||
title: m.loud_raw_sheep_imagine,
|
|
||||||
description: m.inclusive_pink_tuna_enjoy,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.ShowContentWarning]: {
|
|
||||||
title: m.fair_swift_elephant_hunt,
|
|
||||||
description: m.gray_minor_bee_endure,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.PopupAvatarHover]: {
|
|
||||||
title: m.north_nimble_turkey_transform,
|
|
||||||
description: m.bold_moving_fly_savor,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.InfiniteScroll]: {
|
|
||||||
title: m.sleek_this_earthworm_hug,
|
|
||||||
description: m.plane_dark_salmon_pout,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.ConfirmDelete]: {
|
|
||||||
title: m.trite_salty_eel_race,
|
|
||||||
description: m.helpful_early_worm_laugh,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.ConfirmFollow]: {
|
|
||||||
title: m.jolly_empty_bullock_mend,
|
|
||||||
description: m.calm_male_wombat_relish,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: false,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.ConfirmReblog]: {
|
|
||||||
title: m.honest_great_rooster_taste,
|
|
||||||
description: m.wacky_inner_osprey_intend,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: false,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.ConfirmLike]: {
|
|
||||||
title: m.patchy_basic_alligator_inspire,
|
|
||||||
description: m.antsy_weak_raven_treat,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: false,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.CtrlEnterToSend]: {
|
|
||||||
title: m.equal_blue_zebra_launch,
|
|
||||||
description: m.heavy_pink_meerkat_affirm,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Behaviour,
|
|
||||||
} as BooleanSetting,
|
|
||||||
[SettingIds.EmojiTheme]: {
|
|
||||||
title: m.weak_bad_martin_glow,
|
|
||||||
description: m.warm_round_dove_skip,
|
|
||||||
type: SettingType.Enum,
|
|
||||||
value: "native",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "native",
|
|
||||||
label: m.slimy_sound_termite_hug,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "twemoji",
|
|
||||||
label: m.new_brave_maggot_relish,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "noto",
|
|
||||||
label: m.shy_clear_spider_cook,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "fluent",
|
|
||||||
label: m.many_tasty_midge_zoom,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "fluent-flat",
|
|
||||||
label: m.less_early_lionfish_honor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as EnumSetting,
|
|
||||||
[SettingIds.BackgroundURL]: {
|
|
||||||
title: m.stock_large_marten_comfort,
|
|
||||||
description: m.mean_weird_donkey_stab,
|
|
||||||
type: SettingType.String,
|
|
||||||
value: "",
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as StringSetting,
|
|
||||||
[SettingIds.NotificationsSidebar]: {
|
|
||||||
title: m.tired_jumpy_rook_slurp,
|
|
||||||
description: m.wide_new_robin_empower,
|
|
||||||
type: SettingType.Boolean,
|
|
||||||
value: true,
|
|
||||||
page: SettingPages.Appearance,
|
|
||||||
} as BooleanSetting,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSettingsForPage = (page: SettingPages): Partial<Settings> => {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(settings()).filter(
|
|
||||||
([, setting]) => setting.page === page,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge a partly defined Settings object with the default settings
|
|
||||||
* Useful when there is an update to the settings in the backend
|
|
||||||
*/
|
|
||||||
export const mergeSettings = (
|
|
||||||
settingsToMerge: Record<SettingIds, Setting["value"]>,
|
|
||||||
): Settings => {
|
|
||||||
const finalSettings = settings();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(settingsToMerge)) {
|
|
||||||
if (key in settings()) {
|
|
||||||
finalSettings[key as SettingIds].value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSettings;
|
|
||||||
};
|
|
||||||
export type Settings = ReturnType<typeof settings>;
|
|
||||||
Loading…
Reference in a new issue