mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
♻️ refactor: auto format
This commit is contained in:
@@ -27,7 +27,10 @@ function normalizePreferences(raw: Partial<AppPreferences> | undefined): AppPref
|
||||
const locale = raw?.locale;
|
||||
const accent = raw?.accent?.trim() || null;
|
||||
return {
|
||||
schemeMode: schemeMode === "light" || schemeMode === "dark" || schemeMode === "auto" ? schemeMode : "auto",
|
||||
schemeMode:
|
||||
schemeMode === "light" || schemeMode === "dark" || schemeMode === "auto"
|
||||
? schemeMode
|
||||
: "auto",
|
||||
accent: accent && /^#[\da-f]{6}$/i.test(accent) ? accent : null,
|
||||
locale: isAppLocale(locale) ? locale : DEFAULT_LOCALE,
|
||||
showDebugNav: raw?.showDebugNav === true,
|
||||
|
||||
@@ -81,7 +81,8 @@ const handlers = {
|
||||
"avatar:select": (avatarId) => guard(() => avatars.selectAvatar(avatarId)),
|
||||
"avatar:update": ({ avatarId, edit }) => guard(() => avatars.updateAvatar(avatarId, edit)),
|
||||
"avatar:delete": (avatarId) => guard(() => avatars.deleteAvatar(avatarId)),
|
||||
"avatar:favorite": ({ avatarId, folder }) => guard(() => avatars.favoriteAvatar(avatarId, folder)),
|
||||
"avatar:favorite": ({ avatarId, folder }) =>
|
||||
guard(() => avatars.favoriteAvatar(avatarId, folder)),
|
||||
"avatar:unfavorite": (avatarId) => guard(() => avatars.unfavoriteAvatar(avatarId)),
|
||||
"avatar:reloadFavorites": () => guard(() => avatars.reloadFavorites()),
|
||||
"avatar:moveFavorite": ({ avatarId, folder, reload }) =>
|
||||
|
||||
@@ -305,7 +305,11 @@ async function restoreAvatarFavorite(
|
||||
fav: { favoriteId: string; tags: string[] },
|
||||
): Promise<void> {
|
||||
await vrc.addFavorite({
|
||||
body: { type: "avatar", favoriteId: fav.favoriteId, tags: fav.tags.length ? fav.tags : ["avatars1"] },
|
||||
body: {
|
||||
type: "avatar",
|
||||
favoriteId: fav.favoriteId,
|
||||
tags: fav.tags.length ? fav.tags : ["avatars1"],
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@ export function toApiError(err: unknown): ApiError {
|
||||
else if (status === undefined && /network|fetch|ENOTFOUND|ECONN/i.test(message)) code = "network";
|
||||
|
||||
if (status === 429) {
|
||||
return { code, message: "VRChat API rate limit hit. Try again shortly.", retryAfter: retryAfterOf(e) };
|
||||
return {
|
||||
code,
|
||||
message: "VRChat API rate limit hit. Try again shortly.",
|
||||
retryAfter: retryAfterOf(e),
|
||||
};
|
||||
}
|
||||
if (status !== undefined && status >= 500) {
|
||||
return { code, message: "VRChat API is temporarily unavailable. Try again shortly." };
|
||||
|
||||
@@ -260,7 +260,13 @@ function fillSlots(existing: FavoriteGroupInput[], caps: WorldFavoriteCaps): Fav
|
||||
for (let i = 1; mine.length < max && i <= max; i++) {
|
||||
const name = `${prefix}${i}`;
|
||||
if (taken.has(name)) continue;
|
||||
mine.push({ name, displayName: prettyFolderName(name), visibility: "private", worlds: [], vrcPlus });
|
||||
mine.push({
|
||||
name,
|
||||
displayName: prettyFolderName(name),
|
||||
visibility: "private",
|
||||
worlds: [],
|
||||
vrcPlus,
|
||||
});
|
||||
}
|
||||
out.push(...mine);
|
||||
}
|
||||
@@ -354,10 +360,7 @@ export async function unfavoriteWorlds(worldIds: string[]): Promise<void> {
|
||||
await reloadMyFavorites();
|
||||
}
|
||||
|
||||
export async function moveWorldsToFolder(
|
||||
worldIds: string[],
|
||||
folder: string,
|
||||
): Promise<MoveResult> {
|
||||
export async function moveWorldsToFolder(worldIds: string[], folder: string): Promise<MoveResult> {
|
||||
const vrc = requireActiveClient();
|
||||
const me = await currentUser();
|
||||
const records = await favoriteRecords(vrc);
|
||||
@@ -458,10 +461,11 @@ async function favoriteTypeForExistingFolder(
|
||||
query: { ownerId: userId, n: 100 },
|
||||
throwOnError: true,
|
||||
});
|
||||
return (
|
||||
data.find((g) => g.name === folder && isWorldGroupType(g.type))?.type ??
|
||||
favoriteTypeForFolder(folder)
|
||||
const group = data.find(
|
||||
(g): g is (typeof data)[number] & { type: WorldFavoriteGroupType } =>
|
||||
g.name === folder && isWorldGroupType(g.type),
|
||||
);
|
||||
return group?.type ?? favoriteTypeForFolder(folder);
|
||||
}
|
||||
|
||||
type AddFavoriteBody = NonNullable<Parameters<VRChat["addFavorite"]>[0]>["body"];
|
||||
@@ -487,7 +491,9 @@ async function favoriteRecordEntries(vrc: VRChat): Promise<WorldFavoriteRecord[]
|
||||
query: { ownerId: me.id, n: 100 },
|
||||
throwOnError: true,
|
||||
});
|
||||
const groups = rawGroups.filter((g) => isWorldGroupType(g.type));
|
||||
const groups = rawGroups.filter((g): g is (typeof rawGroups)[number] & { type: WorldFavoriteGroupType } =>
|
||||
isWorldGroupType(g.type),
|
||||
);
|
||||
const pageSize = 100;
|
||||
const entries: WorldFavoriteRecord[] = [];
|
||||
for (const group of groups) {
|
||||
|
||||
@@ -23,7 +23,9 @@ export function CheckBox({
|
||||
onChange();
|
||||
}}
|
||||
className={`flex size-5 items-center justify-center rounded-md border transition-colors ${
|
||||
on ? "border-accent bg-accent text-on-accent" : "border-border bg-surface-2/80 hover:border-accent"
|
||||
on
|
||||
? "border-accent bg-accent text-on-accent"
|
||||
: "border-border bg-surface-2/80 hover:border-accent"
|
||||
}`}
|
||||
>
|
||||
{indeterminate ? <Minus size={13} /> : checked ? <Check size={13} /> : null}
|
||||
|
||||
@@ -15,15 +15,9 @@ export function useAccountSettings(): {
|
||||
const [nonce, setNonce] = useState(0);
|
||||
const [override, setOverride] = useState<AccountSettings | null>(null);
|
||||
|
||||
const fetched = useAsync(
|
||||
() => api.settings.get(),
|
||||
[activeId, nonce],
|
||||
"Failed to load settings.",
|
||||
);
|
||||
const fetched = useAsync(() => api.settings.get(), [activeId, nonce], "Failed to load settings.");
|
||||
|
||||
const state: Async<AccountSettings> = override
|
||||
? { status: "ready", data: override }
|
||||
: fetched;
|
||||
const state: Async<AccountSettings> = override ? { status: "ready", data: override } : fetched;
|
||||
|
||||
const reload = useCallback(() => {
|
||||
setOverride(null);
|
||||
|
||||
@@ -26,7 +26,9 @@ export function AccountSwitcher() {
|
||||
const status = self?.status ?? "offline";
|
||||
const activeName = active ? (demoMode ? demoName(active.id) : active.displayName) : "Account";
|
||||
const activeIcon = demoMode ? undefined : active?.userIcon;
|
||||
const activeSub = demoMode ? statusMeta[status].label : self?.statusDescription || statusMeta[status].label;
|
||||
const activeSub = demoMode
|
||||
? statusMeta[status].label
|
||||
: self?.statusDescription || statusMeta[status].label;
|
||||
|
||||
function close() {
|
||||
setOpen(false);
|
||||
@@ -57,9 +59,7 @@ export function AccountSwitcher() {
|
||||
<span className="truncate text-left text-[13.5px] font-semibold leading-[1.25]">
|
||||
{activeName}
|
||||
</span>
|
||||
<span className="truncate text-left text-[11px] font-medium text-faint">
|
||||
{activeSub}
|
||||
</span>
|
||||
<span className="truncate text-left text-[11px] font-medium text-faint">{activeSub}</span>
|
||||
</span>
|
||||
<span
|
||||
className={`acct__chevron shrink-0 text-faint transition-transform duration-[var(--dur)] ease-[var(--ease)] ${
|
||||
|
||||
@@ -58,7 +58,11 @@ export function AvatarActions({ avatar }: { avatar: Avatar }) {
|
||||
</Button>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setEditOpen(true)} title={t("avatar:actions.edit")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setEditOpen(true)}
|
||||
title={t("avatar:actions.edit")}
|
||||
>
|
||||
<Pencil size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
@@ -74,12 +78,14 @@ export function AvatarActions({ avatar }: { avatar: Avatar }) {
|
||||
{error ? <p className="text-[12px] text-danger">{error}</p> : null}
|
||||
|
||||
{favoriteOpen ? (
|
||||
<FavoriteModal avatar={avatar} currentFolder={folder} onClose={() => setFavoriteOpen(false)} />
|
||||
<FavoriteModal
|
||||
avatar={avatar}
|
||||
currentFolder={folder}
|
||||
onClose={() => setFavoriteOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{editOpen ? (
|
||||
<EditAvatarModal avatar={avatar} onClose={() => setEditOpen(false)} />
|
||||
) : null}
|
||||
{editOpen ? <EditAvatarModal avatar={avatar} onClose={() => setEditOpen(false)} /> : null}
|
||||
|
||||
<DeleteAvatarModal
|
||||
avatar={avatar}
|
||||
|
||||
@@ -41,9 +41,7 @@ export function AvatarCard({
|
||||
{selectable ? (
|
||||
<span
|
||||
className={`absolute left-1.5 top-1.5 flex size-5 items-center justify-center rounded-md border transition-colors ${
|
||||
selected
|
||||
? "border-accent bg-accent text-on-accent"
|
||||
: "border-border bg-surface-2/80"
|
||||
selected ? "border-accent bg-accent text-on-accent" : "border-border bg-surface-2/80"
|
||||
}`}
|
||||
>
|
||||
{selected ? <Check size={13} /> : null}
|
||||
|
||||
@@ -17,7 +17,8 @@ export function AvatarView({ avatarId }: { avatarId: string }) {
|
||||
const { avatar, failed } = useAvatar(avatarId);
|
||||
|
||||
if (avatar) return <AvatarDetail avatar={avatar} />;
|
||||
if (failed) return <Banner className="m-10 max-w-[420px]">{t("avatar:detail.unavailable")}</Banner>;
|
||||
if (failed)
|
||||
return <Banner className="m-10 max-w-[420px]">{t("avatar:detail.unavailable")}</Banner>;
|
||||
return <AvatarSkeleton />;
|
||||
}
|
||||
|
||||
@@ -55,7 +56,9 @@ function AvatarDetail({ avatar }: { avatar: Avatar }) {
|
||||
{avatar.releaseStatus !== "public" ? (
|
||||
<Tag color="var(--status-ask)">{avatar.releaseStatus}</Tag>
|
||||
) : null}
|
||||
{avatar.featured ? <Tag color="var(--accent)">{t("avatar:detail.featured")}</Tag> : null}
|
||||
{avatar.featured ? (
|
||||
<Tag color="var(--accent)">{t("avatar:detail.featured")}</Tag>
|
||||
) : null}
|
||||
{avatar.platforms?.pc ? <Tag>PC</Tag> : null}
|
||||
{avatar.platforms?.android ? <Tag color="var(--status-join)">Quest</Tag> : null}
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,13 @@ function BulkBar({ ids, onDone }: { ids: string[]; onDone: () => void }) {
|
||||
return (
|
||||
<SelectionBar label={t("avatar:bulk.selected", { count: ids.length })}>
|
||||
{busy ? (
|
||||
<div className="selection-bar__progress" aria-label={t("avatar:bulk.unfavoritingProgress", { done: deleteProgress, total: ids.length })}>
|
||||
<div
|
||||
className="selection-bar__progress"
|
||||
aria-label={t("avatar:bulk.unfavoritingProgress", {
|
||||
done: deleteProgress,
|
||||
total: ids.length,
|
||||
})}
|
||||
>
|
||||
<span className="selection-bar__progress-label">
|
||||
{t("avatar:bulk.unfavoritingProgress", { done: deleteProgress, total: ids.length })}
|
||||
</span>
|
||||
@@ -164,7 +170,10 @@ function BulkBar({ ids, onDone }: { ids: string[]; onDone: () => void }) {
|
||||
{busy ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-[12px] text-muted">
|
||||
{t("avatar:bulk.unfavoritingProgress", { done: deleteProgress, total: ids.length })}
|
||||
{t("avatar:bulk.unfavoritingProgress", {
|
||||
done: deleteProgress,
|
||||
total: ids.length,
|
||||
})}
|
||||
</span>
|
||||
<div className="h-1 overflow-hidden rounded-full bg-surface-hover">
|
||||
<div
|
||||
@@ -205,8 +214,17 @@ function CurrentAvatarSection() {
|
||||
|
||||
const isOwner = avatar.authorId === self?.id;
|
||||
const menuItems: ContextMenuEntry[] = [
|
||||
{ label: t("avatar:context.open"), icon: <Eye size={14} />, onClick: () => nav.openAvatar(avatar.id) },
|
||||
{ label: t("avatar:actions.wearing"), icon: <Shirt size={14} />, disabled: true, onClick: () => {} },
|
||||
{
|
||||
label: t("avatar:context.open"),
|
||||
icon: <Eye size={14} />,
|
||||
onClick: () => nav.openAvatar(avatar.id),
|
||||
},
|
||||
{
|
||||
label: t("avatar:actions.wearing"),
|
||||
icon: <Shirt size={14} />,
|
||||
disabled: true,
|
||||
onClick: () => {},
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: folder ? t("avatar:actions.manageFavorite") : t("avatar:actions.favorite"),
|
||||
@@ -214,15 +232,19 @@ function CurrentAvatarSection() {
|
||||
onClick: () => setFavoriteOpen(true),
|
||||
},
|
||||
...(isOwner
|
||||
? [
|
||||
{ label: t("avatar:actions.edit"), icon: <Pencil size={14} />, onClick: () => setEditOpen(true) },
|
||||
? ([
|
||||
{
|
||||
label: t("avatar:actions.edit"),
|
||||
icon: <Pencil size={14} />,
|
||||
onClick: () => setEditOpen(true),
|
||||
},
|
||||
{
|
||||
label: t("avatar:actions.delete"),
|
||||
icon: <Trash2 size={14} />,
|
||||
danger: true,
|
||||
onClick: () => setDeleteOpen(true),
|
||||
},
|
||||
] satisfies ContextMenuEntry[]
|
||||
] satisfies ContextMenuEntry[])
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -241,7 +263,11 @@ function CurrentAvatarSection() {
|
||||
/>
|
||||
</div>
|
||||
{favoriteOpen ? (
|
||||
<FavoriteModal avatar={avatar} currentFolder={folder} onClose={() => setFavoriteOpen(false)} />
|
||||
<FavoriteModal
|
||||
avatar={avatar}
|
||||
currentFolder={folder}
|
||||
onClose={() => setFavoriteOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{editOpen ? <EditAvatarModal avatar={avatar} onClose={() => setEditOpen(false)} /> : null}
|
||||
<DeleteAvatarModal
|
||||
@@ -266,7 +292,9 @@ function UploadedTab({ filter }: { filter: AvatarFilter }) {
|
||||
const t = useT();
|
||||
const nav = useNav();
|
||||
const mine = useMyAvatars();
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; avatar: Avatar } | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; avatar: Avatar } | null>(
|
||||
null,
|
||||
);
|
||||
const [editAvatar, setEditAvatar] = useState<Avatar | null>(null);
|
||||
const [deleteAvatar, setDeleteAvatar] = useState<Avatar | null>(null);
|
||||
|
||||
@@ -276,9 +304,21 @@ function UploadedTab({ filter }: { filter: AvatarFilter }) {
|
||||
if (!shown.length) return <p className="text-[13px] text-faint">{t("avatar:empty")}</p>;
|
||||
|
||||
const menuItems = (avatar: Avatar): ContextMenuEntry[] => [
|
||||
{ label: t("avatar:context.open"), icon: <Eye size={14} />, onClick: () => nav.openAvatar(avatar.id) },
|
||||
{ label: t("avatar:actions.wear"), icon: <Shirt size={14} />, onClick: () => void api.avatar.select(avatar.id) },
|
||||
{ label: t("avatar:actions.edit"), icon: <Pencil size={14} />, onClick: () => setEditAvatar(avatar) },
|
||||
{
|
||||
label: t("avatar:context.open"),
|
||||
icon: <Eye size={14} />,
|
||||
onClick: () => nav.openAvatar(avatar.id),
|
||||
},
|
||||
{
|
||||
label: t("avatar:actions.wear"),
|
||||
icon: <Shirt size={14} />,
|
||||
onClick: () => void api.avatar.select(avatar.id),
|
||||
},
|
||||
{
|
||||
label: t("avatar:actions.edit"),
|
||||
icon: <Pencil size={14} />,
|
||||
onClick: () => setEditAvatar(avatar),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: t("avatar:actions.delete"),
|
||||
@@ -363,7 +403,9 @@ function FavoritesTab({ filter, searching }: { filter: AvatarFilter; searching:
|
||||
setSelected(
|
||||
new Set(
|
||||
shown.flatMap((folder) =>
|
||||
folder.avatars.filter((avatar) => avatar.releaseStatus === status).map((avatar) => avatar.id),
|
||||
folder.avatars
|
||||
.filter((avatar) => avatar.releaseStatus === status)
|
||||
.map((avatar) => avatar.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -387,7 +429,7 @@ function FavoritesTab({ filter, searching }: { filter: AvatarFilter; searching:
|
||||
setSelected((s) => {
|
||||
const next = new Set(s);
|
||||
const all = ids.every((id) => next.has(id));
|
||||
for (const id of ids) (all ? next.delete(id) : next.add(id));
|
||||
for (const id of ids) all ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -398,9 +440,21 @@ function FavoritesTab({ filter, searching }: { filter: AvatarFilter; searching:
|
||||
};
|
||||
|
||||
const menuItems = (avatar: Avatar, folder: string): ContextMenuEntry[] => [
|
||||
{ label: t("avatar:context.open"), icon: <Eye size={14} />, onClick: () => nav.openAvatar(avatar.id) },
|
||||
{ label: t("avatar:context.select"), icon: <CheckSquare size={14} />, onClick: () => selectOne(avatar.id) },
|
||||
{ label: t("avatar:actions.wear"), icon: <Shirt size={14} />, onClick: () => void api.avatar.select(avatar.id) },
|
||||
{
|
||||
label: t("avatar:context.open"),
|
||||
icon: <Eye size={14} />,
|
||||
onClick: () => nav.openAvatar(avatar.id),
|
||||
},
|
||||
{
|
||||
label: t("avatar:context.select"),
|
||||
icon: <CheckSquare size={14} />,
|
||||
onClick: () => selectOne(avatar.id),
|
||||
},
|
||||
{
|
||||
label: t("avatar:actions.wear"),
|
||||
icon: <Shirt size={14} />,
|
||||
onClick: () => void api.avatar.select(avatar.id),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: t("avatar:actions.manageFavorite"),
|
||||
@@ -480,7 +534,12 @@ function FavoritesTab({ filter, searching }: { filter: AvatarFilter; searching:
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, avatar: a, folder: folder.name });
|
||||
setContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
avatar: a,
|
||||
folder: folder.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -529,9 +588,7 @@ function FavoritesTab({ filter, searching }: { filter: AvatarFilter; searching:
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selecting && selected.size > 0 ? (
|
||||
<BulkBar ids={[...selected]} onDone={exitSelect} />
|
||||
) : null}
|
||||
{selecting && selected.size > 0 ? <BulkBar ids={[...selected]} onDone={exitSelect} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ export function FolderEditModal({
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-[12px] font-medium text-muted">{t("avatar:folder.visibility")}</span>
|
||||
<span className="text-[12px] font-medium text-muted">
|
||||
{t("avatar:folder.visibility")}
|
||||
</span>
|
||||
<select
|
||||
className={INPUT_CLASS}
|
||||
value={visibility}
|
||||
|
||||
@@ -40,11 +40,18 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const { buildItems, modal } = useUserMenu();
|
||||
|
||||
const openContextMenu = useCallback((e: React.MouseEvent, friend: UserProfile) => {
|
||||
const openContextMenu = useCallback(
|
||||
(e: React.MouseEvent, friend: UserProfile) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, friend: demoMode ? anonymizeUserForDemo(friend) : friend });
|
||||
}, [demoMode]);
|
||||
setContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
friend: demoMode ? anonymizeUserForDemo(friend) : friend,
|
||||
});
|
||||
},
|
||||
[demoMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="friendsbar flex h-full flex-col overflow-hidden border-l border-border bg-surface">
|
||||
@@ -56,7 +63,9 @@ export function FriendsSidebar({ onOpen }: { onOpen: (id: string) => void }) {
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{selfAlone ? <FriendRow friend={self} onOpen={onOpen} isSelf selfChrome demoMode={demoMode} /> : null}
|
||||
{selfAlone ? (
|
||||
<FriendRow friend={self} onOpen={onOpen} isSelf selfChrome demoMode={demoMode} />
|
||||
) : null}
|
||||
|
||||
{friends.length === 0 ? (
|
||||
<p className="p-3 text-[13px] text-faint">{t("nav:friends.noFriendsOnline")}</p>
|
||||
|
||||
@@ -65,7 +65,9 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
: presence.effective.label;
|
||||
const avatar = avatarOf(displayProfile);
|
||||
const banner = bannerOf(displayProfile);
|
||||
const devLabel = displayProfile.developerType ? developerLabels[displayProfile.developerType] : undefined;
|
||||
const devLabel = displayProfile.developerType
|
||||
? developerLabels[displayProfile.developerType]
|
||||
: undefined;
|
||||
const bioLinks = (displayProfile.bioLinks ?? []).filter(Boolean);
|
||||
const showcasedBadges = (displayProfile.badges ?? []).filter((b) => b.showcased);
|
||||
|
||||
@@ -87,7 +89,9 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
body={
|
||||
<div className="min-w-0 flex-1 pb-2">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<h2 className="text-[32px] font-bold tracking-[-0.6px]">{displayProfile.displayName}</h2>
|
||||
<h2 className="text-[32px] font-bold tracking-[-0.6px]">
|
||||
{displayProfile.displayName}
|
||||
</h2>
|
||||
{profile.isSelf ? <Tag color="var(--accent)">{t("profile:badge.you")}</Tag> : null}
|
||||
{profile.isFriend && !profile.isSelf ? (
|
||||
<Tag color="var(--status-join)">{t("profile:badge.friend")}</Tag>
|
||||
@@ -212,9 +216,14 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
/>
|
||||
) : null}
|
||||
{displayProfile.state ? (
|
||||
<Fact label={t("profile:facts.state")} value={stateLabel(displayProfile.state, t)} />
|
||||
<Fact
|
||||
label={t("profile:facts.state")}
|
||||
value={stateLabel(displayProfile.state, t)}
|
||||
/>
|
||||
) : null}
|
||||
{demoMode ? null : <Fact label={t("profile:facts.userId")} value={profile.id} mono />}
|
||||
{demoMode ? null : (
|
||||
<Fact label={t("profile:facts.userId")} value={profile.id} mono />
|
||||
)}
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ export function useFavoriteWorlds(userId: string): {
|
||||
|
||||
const fallbackFolders = useMemo<FavoriteWorldFolder[]>(() => [], []);
|
||||
const streamedFolders = streamed?.userId === userId ? streamed.folders : fallbackFolders;
|
||||
const folders: FavoriteWorldFolder[] = fetched.status === "ready" ? fetched.data : streamedFolders;
|
||||
const folders: FavoriteWorldFolder[] =
|
||||
fetched.status === "ready" ? fetched.data : streamedFolders;
|
||||
|
||||
const ids = useMemo(() => folders.flatMap((f) => f.worldIds), [folders]);
|
||||
const worlds = useWorlds(useShallow((s) => ids.map((id) => s.worlds[id]).filter(Boolean)));
|
||||
|
||||
@@ -347,10 +347,7 @@ function GameSection() {
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => void run(pickGamePath(), t("settings:game.saved"))}
|
||||
>
|
||||
<Button variant="ghost" onClick={() => void run(pickGamePath(), t("settings:game.saved"))}>
|
||||
<FolderOpen size={14} />
|
||||
{t("settings:game.browse")}
|
||||
</Button>
|
||||
@@ -380,9 +377,7 @@ function GameSection() {
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2.5">
|
||||
<Button
|
||||
onClick={() =>
|
||||
void run(setGamePath(path.trim() || null), t("settings:game.saved"))
|
||||
}
|
||||
onClick={() => void run(setGamePath(path.trim() || null), t("settings:game.saved"))}
|
||||
loading={busy}
|
||||
disabled={!dirty}
|
||||
>
|
||||
|
||||
@@ -102,7 +102,7 @@ export function MyFavoriteWorldsSection({ filter }: { filter?: WorldFilter }) {
|
||||
setSelected((s) => {
|
||||
const next = new Set(s);
|
||||
const all = ids.every((id) => next.has(id));
|
||||
for (const id of ids) (all ? next.delete(id) : next.add(id));
|
||||
for (const id of ids) all ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -118,7 +118,11 @@ export function MyFavoriteWorldsSection({ filter }: { filter?: WorldFilter }) {
|
||||
};
|
||||
|
||||
const menuItems = (world: World, folder: string): ContextMenuEntry[] => [
|
||||
{ label: t("world:context.open"), icon: <Eye size={14} />, onClick: () => nav.openWorld(world.id) },
|
||||
{
|
||||
label: t("world:context.open"),
|
||||
icon: <Eye size={14} />,
|
||||
onClick: () => nav.openWorld(world.id),
|
||||
},
|
||||
{
|
||||
label: t("world:context.select"),
|
||||
icon: <CheckSquare size={14} />,
|
||||
@@ -144,7 +148,10 @@ export function MyFavoriteWorldsSection({ filter }: { filter?: WorldFilter }) {
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{selecting ? (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => selectWhere((w) => w.releaseStatus === "private")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => selectWhere((w) => w.releaseStatus === "private")}
|
||||
>
|
||||
{t("world:bulk.selectPrivate")}
|
||||
</Button>
|
||||
{hasDeleted ? (
|
||||
@@ -201,7 +208,12 @@ export function MyFavoriteWorldsSection({ filter }: { filter?: WorldFilter }) {
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, world: w, folder: folder.name });
|
||||
setContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
world: w,
|
||||
folder: folder.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,11 @@ export function WorldCard({
|
||||
return (
|
||||
<Card
|
||||
onClick={
|
||||
selectable ? onToggleSelect : world.deleted ? undefined : (onOpen ?? (() => openWorld(world.id)))
|
||||
selectable
|
||||
? onToggleSelect
|
||||
: world.deleted
|
||||
? undefined
|
||||
: (onOpen ?? (() => openWorld(world.id)))
|
||||
}
|
||||
onContextMenu={onContextMenu}
|
||||
className={`${selected ? "outline outline-[3px] -outline-offset-[3px] outline-accent" : ""} ${
|
||||
|
||||
@@ -83,9 +83,17 @@ function WorldCard({ world }: { world: World }) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CreateInstanceModal worldId={world.id} open={createOpen} onClose={() => setCreateOpen(false)} />
|
||||
<CreateInstanceModal
|
||||
worldId={world.id}
|
||||
open={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
/>
|
||||
{favoriteOpen ? (
|
||||
<WorldFavoriteModal world={world} currentFolder={folder} onClose={() => setFavoriteOpen(false)} />
|
||||
<WorldFavoriteModal
|
||||
world={world}
|
||||
currentFolder={folder}
|
||||
onClose={() => setFavoriteOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
@@ -153,7 +161,10 @@ function WorldCard({ world }: { world: World }) {
|
||||
<Fact label={t("world:detail.facts.version")} value={`${world.version}`} />
|
||||
) : null}
|
||||
{world.publishedAt ? (
|
||||
<Fact label={t("world:detail.facts.published")} value={formatDate(world.publishedAt)} />
|
||||
<Fact
|
||||
label={t("world:detail.facts.published")}
|
||||
value={formatDate(world.publishedAt)}
|
||||
/>
|
||||
) : null}
|
||||
{world.labsPublishedAt ? (
|
||||
<Fact
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { AppConfig, AppPreferences, PreferredRegion } from "../../../shared/types/appConfig";
|
||||
import { api } from "./api";
|
||||
|
||||
|
||||
@@ -45,11 +45,14 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [accent, setAccentState] = useState<string | null>(null);
|
||||
const lastApplied = useRef<string>("");
|
||||
|
||||
const setAccent = useCallback((hex: string | null) => {
|
||||
const setAccent = useCallback(
|
||||
(hex: string | null) => {
|
||||
applyAccent(hex);
|
||||
setAccentState(hex);
|
||||
void setPreferences({ accent: hex });
|
||||
}, [setPreferences]);
|
||||
},
|
||||
[setPreferences],
|
||||
);
|
||||
|
||||
const setTheme = useCallback(
|
||||
(id: string) => {
|
||||
|
||||
@@ -24,7 +24,11 @@ export function errorMessage(err: unknown, fallback: string): string {
|
||||
|
||||
export function isRetryableApiError(err: unknown): boolean {
|
||||
if (!(err instanceof ApiException)) return false;
|
||||
return err.error.code === "unknown" || err.error.code === "network" || err.error.code === "rate_limited";
|
||||
return (
|
||||
err.error.code === "unknown" ||
|
||||
err.error.code === "network" ||
|
||||
err.error.code === "rate_limited"
|
||||
);
|
||||
}
|
||||
|
||||
type DataOf<R> = R extends { ok: true; data: infer D } ? D : never;
|
||||
@@ -163,7 +167,8 @@ export const api = {
|
||||
game: {
|
||||
status: () => call("game:status"),
|
||||
launch: () => call("game:launch"),
|
||||
join: (location: string, shortName?: string | null) => call("game:join", { location, shortName }),
|
||||
join: (location: string, shortName?: string | null) =>
|
||||
call("game:join", { location, shortName }),
|
||||
openProtocol: (url: string) => call("game:openProtocol", url),
|
||||
},
|
||||
gallery: {
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export { I18nProvider, useI18n, useT } from "./I18nContext";
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
NAMESPACES,
|
||||
availableLocales,
|
||||
getLocale,
|
||||
i18n,
|
||||
} from "./registry";
|
||||
export { DEFAULT_LOCALE, NAMESPACES, availableLocales, getLocale, i18n } from "./registry";
|
||||
export type { Locale, LocaleCode, LocaleMeta, Messages } from "./types";
|
||||
|
||||
@@ -69,8 +69,7 @@ function toFolder(
|
||||
};
|
||||
}
|
||||
|
||||
export const useWorldFavoriteLimits = (): FavoriteLimits =>
|
||||
useWorldFavorites((s) => s.limits);
|
||||
export const useWorldFavoriteLimits = (): FavoriteLimits => useWorldFavorites((s) => s.limits);
|
||||
|
||||
export const useWorldFavoritesLoaded = (): boolean => useWorldFavorites((s) => s.loaded);
|
||||
|
||||
|
||||
+4
-1
@@ -109,7 +109,10 @@ export interface IpcRequests {
|
||||
"avatar:unfavoriteMany": (avatarIds: string[]) => IpcResult<void>;
|
||||
"avatar:moveFavoriteMany": (p: { avatarIds: string[]; folder: string }) => IpcResult<MoveResult>;
|
||||
"avatar:clearFavoriteFolder": (folder: string) => IpcResult<void>;
|
||||
"avatar:updateFavoriteFolder": (p: { folder: string; edit: FavoriteGroupEdit }) => IpcResult<void>;
|
||||
"avatar:updateFavoriteFolder": (p: {
|
||||
folder: string;
|
||||
edit: FavoriteGroupEdit;
|
||||
}) => IpcResult<void>;
|
||||
|
||||
"group:byUser": (userId: string) => IpcResult<Group[]>;
|
||||
"group:represented": (userId: string) => IpcResult<Group | null>;
|
||||
|
||||
Reference in New Issue
Block a user