mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
feat: templates page
This commit is contained in:
parent
5488f02f8c
commit
b91bdb92c1
6 changed files with 250 additions and 37 deletions
|
|
@ -1,7 +1,12 @@
|
|||
import { tc } from "@/lib/i18n";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
type PageType = "/packages/user-packages" | "/packages/repositories";
|
||||
type PageType =
|
||||
| "/packages/user-packages"
|
||||
| "/packages/repositories"
|
||||
| "/packages/templates";
|
||||
|
||||
// Note: For historical reasons, templates page are under packages in route.
|
||||
|
||||
export function HeadingPageName({
|
||||
pageType,
|
||||
|
|
@ -11,7 +16,7 @@ export function HeadingPageName({
|
|||
return (
|
||||
<div className={"-ml-1.5"}>
|
||||
<div
|
||||
className={"grid grid-cols-2 gap-1.5 bg-secondary p-1 -m-1 rounded-md"}
|
||||
className={"grid grid-cols-3 gap-1.5 bg-secondary p-1 -m-1 rounded-md"}
|
||||
>
|
||||
<HeadingButton
|
||||
currentPage={pageType}
|
||||
|
|
@ -25,6 +30,12 @@ export function HeadingPageName({
|
|||
>
|
||||
{tc("packages:user packages")}
|
||||
</HeadingButton>
|
||||
<HeadingButton
|
||||
currentPage={pageType}
|
||||
targetPage={"/packages/templates"}
|
||||
>
|
||||
{tc("packages:templates")}
|
||||
</HeadingButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -40,7 +51,7 @@ function HeadingButton({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const button =
|
||||
"cursor-pointer py-1.5 font-bold grow-0 hover:bg-background rounded-sm text-center p-2";
|
||||
"cursor-pointer px-3 py-2 font-bold grow-0 hover:bg-background rounded-sm text-center p-2";
|
||||
|
||||
if (currentPage === targetPage) {
|
||||
return <div className={`${button} bg-background`}>{children}</div>;
|
||||
|
|
|
|||
145
vrc-get-gui/app/_main/packages/templates/index.tsx
Normal file
145
vrc-get-gui/app/_main/packages/templates/index.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import Loading from "@/app/-loading";
|
||||
import { HeadingPageName } from "@/app/_main/packages/-tab-selector";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { type TauriProjectTemplateInfo, commands } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { usePrevPathName } from "@/lib/prev-page";
|
||||
import {
|
||||
projectTemplateCategory,
|
||||
projectTemplateName,
|
||||
} from "@/lib/project-template";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CircleX } from "lucide-react";
|
||||
import { Suspense, useId } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_main/packages/templates/")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-left"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
leading={<HeadingPageName pageType={"/packages/templates"} />}
|
||||
trailing={<Button>{tc("templates:create template")}</Button>}
|
||||
/>
|
||||
<main
|
||||
className={`shrink overflow-hidden flex w-full h-full ${bodyAnimation}`}
|
||||
>
|
||||
<ScrollableCardTable className={"h-full w-full"}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<TemplatesTableBody />
|
||||
</Suspense>
|
||||
</ScrollableCardTable>
|
||||
</main>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplatesTableBody() {
|
||||
const information = useSuspenseQuery({
|
||||
queryKey: ["environmentProjectCreationInformation"],
|
||||
queryFn: async () => await commands.environmentProjectCreationInformation(),
|
||||
});
|
||||
|
||||
const TABLE_HEAD = [
|
||||
"general:name",
|
||||
"templates:category",
|
||||
"", // actions
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{information.data.templates.map((template) => (
|
||||
<TemplateRow key={template.id} template={template} />
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplateRow({
|
||||
template,
|
||||
remove,
|
||||
}: {
|
||||
template: TauriProjectTemplateInfo;
|
||||
remove?: () => void;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const id = useId();
|
||||
|
||||
const category = projectTemplateCategory(template.id);
|
||||
|
||||
return (
|
||||
<tr className="even:bg-secondary/30">
|
||||
<td className={`${cellClass} w-full`}>
|
||||
<label htmlFor={id}>
|
||||
<p className="font-normal">{projectTemplateName(template)}</p>
|
||||
</label>
|
||||
</td>
|
||||
<td className={cellClass}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p className="font-normal">
|
||||
{tc(`templates:category:${category}`)}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{tc(`templates:tooltip:category:${category}`)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={`${cellClass} w-min`}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
className={category !== "alcom" ? "opacity-50" : ""}
|
||||
>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{category === "alcom"
|
||||
? tc("templates:tooltip:remove template")
|
||||
: category === "builtin"
|
||||
? tc("templates:tooltip:remove builtin template")
|
||||
: category === "vcc"
|
||||
? tc("templates:tooltip:remove vcc template")
|
||||
: ""}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
@ -31,6 +31,11 @@ import { type DialogContext, showDialog } from "@/lib/dialog";
|
|||
import { tc, tt } from "@/lib/i18n";
|
||||
import { router } from "@/lib/main";
|
||||
import { pathSeparator } from "@/lib/os";
|
||||
import {
|
||||
type ProjectTemplateCategory,
|
||||
projectTemplateCategory,
|
||||
projectTemplateName,
|
||||
} from "@/lib/project-template";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -115,11 +120,6 @@ interface ProjectCreationInformation {
|
|||
projectLocation: string;
|
||||
projectName: string;
|
||||
}
|
||||
const AVATARS_TEMPLATE_ID = "com.anatawa12.vrc-get.vrchat.avatars";
|
||||
const WORLDS_TEMPLATE_ID = "com.anatawa12.vrc-get.vrchat.worlds";
|
||||
const BLANK_TEMPLATE_ID = "com.anatawa12.vrc-get.blank";
|
||||
const VCC_TEMPLATE_PREFIX = "com.anatawa12.vrc-get.vcc.";
|
||||
const UNNAMED_TEMPLATE_PREFIX = "com.anatawa12.vrc-get.user.";
|
||||
|
||||
function EnteringInformation({
|
||||
templates,
|
||||
|
|
@ -183,24 +183,17 @@ function EnteringInformation({
|
|||
const templateInputId = useId();
|
||||
const unityInputId = useId();
|
||||
|
||||
type TemplateCategory = "builtin" | "alcom" | "vcc";
|
||||
const templatesByCategory = useMemo(() => {
|
||||
const byCategory: { [k in TemplateCategory]: TauriProjectTemplateInfo[] } =
|
||||
{
|
||||
builtin: [],
|
||||
alcom: [],
|
||||
vcc: [],
|
||||
};
|
||||
|
||||
const templateCategory = (id: string): TemplateCategory => {
|
||||
if (id.startsWith(VCC_TEMPLATE_PREFIX)) return "vcc";
|
||||
if (id.startsWith(UNNAMED_TEMPLATE_PREFIX)) return "alcom";
|
||||
if (id.startsWith("com.anatawa12.vrc-get.")) return "builtin";
|
||||
return "alcom";
|
||||
const byCategory: {
|
||||
[k in ProjectTemplateCategory]: TauriProjectTemplateInfo[];
|
||||
} = {
|
||||
builtin: [],
|
||||
alcom: [],
|
||||
vcc: [],
|
||||
};
|
||||
|
||||
for (const template of templates) {
|
||||
byCategory[templateCategory(template.id)].push(template);
|
||||
byCategory[projectTemplateCategory(template.id)].push(template);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -208,7 +201,7 @@ function EnteringInformation({
|
|||
["builtin", byCategory.builtin],
|
||||
["alcom", byCategory.alcom],
|
||||
["vcc", byCategory.vcc],
|
||||
] as const
|
||||
] satisfies [ProjectTemplateCategory, TauriProjectTemplateInfo[]][]
|
||||
).filter((x) => x[1].length > 0);
|
||||
}, [templates]);
|
||||
|
||||
|
|
@ -259,7 +252,7 @@ function EnteringInformation({
|
|||
disabled={disabled}
|
||||
key={template.id}
|
||||
>
|
||||
{templateName(template)}
|
||||
{projectTemplateName(template)}
|
||||
</SelectItem>
|
||||
);
|
||||
if (!template.available) {
|
||||
|
|
@ -351,19 +344,6 @@ function EnteringInformation({
|
|||
);
|
||||
}
|
||||
|
||||
function templateName(template: TauriProjectTemplateInfo): React.ReactNode {
|
||||
switch (template.id) {
|
||||
case AVATARS_TEMPLATE_ID:
|
||||
return tc("projects:template-name:avatars");
|
||||
case WORLDS_TEMPLATE_ID:
|
||||
return tc("projects:template-name:worlds");
|
||||
case BLANK_TEMPLATE_ID:
|
||||
return tc("projects:template-name:blank");
|
||||
default:
|
||||
return template.display_name;
|
||||
}
|
||||
}
|
||||
|
||||
function useProjectNameCheck(
|
||||
projectLocation: string,
|
||||
projectName: string,
|
||||
|
|
|
|||
35
vrc-get-gui/lib/project-template.ts
Normal file
35
vrc-get-gui/lib/project-template.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { TauriProjectTemplateInfo } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import type React from "react";
|
||||
|
||||
const AVATARS_TEMPLATE_ID = "com.anatawa12.vrc-get.vrchat.avatars";
|
||||
const WORLDS_TEMPLATE_ID = "com.anatawa12.vrc-get.vrchat.worlds";
|
||||
const BLANK_TEMPLATE_ID = "com.anatawa12.vrc-get.blank";
|
||||
const VCC_TEMPLATE_PREFIX = "com.anatawa12.vrc-get.vcc.";
|
||||
const UNNAMED_TEMPLATE_PREFIX = "com.anatawa12.vrc-get.user.";
|
||||
|
||||
export function projectTemplateName(
|
||||
template: TauriProjectTemplateInfo,
|
||||
): React.ReactNode {
|
||||
switch (template.id) {
|
||||
case AVATARS_TEMPLATE_ID:
|
||||
return tc("projects:template-name:avatars");
|
||||
case WORLDS_TEMPLATE_ID:
|
||||
return tc("projects:template-name:worlds");
|
||||
case BLANK_TEMPLATE_ID:
|
||||
return tc("projects:template-name:blank");
|
||||
default:
|
||||
return template.display_name;
|
||||
}
|
||||
}
|
||||
|
||||
export type ProjectTemplateCategory = "builtin" | "alcom" | "vcc";
|
||||
|
||||
export const projectTemplateCategory = (
|
||||
id: string,
|
||||
): ProjectTemplateCategory => {
|
||||
if (id.startsWith(VCC_TEMPLATE_PREFIX)) return "vcc";
|
||||
if (id.startsWith(UNNAMED_TEMPLATE_PREFIX)) return "alcom";
|
||||
if (id.startsWith("com.anatawa12.vrc-get.")) return "builtin";
|
||||
return "alcom";
|
||||
};
|
||||
|
|
@ -27,6 +27,7 @@ import { Route as SetupSetupAppearanceIndexImport } from './../app/_setup/setup/
|
|||
import { Route as MainSettingsLicensesIndexImport } from './../app/_main/settings/licenses/index'
|
||||
import { Route as MainProjectsManageIndexImport } from './../app/_main/projects/manage/index'
|
||||
import { Route as MainPackagesUserPackagesIndexImport } from './../app/_main/packages/user-packages/index'
|
||||
import { Route as MainPackagesTemplatesIndexImport } from './../app/_main/packages/templates/index'
|
||||
import { Route as MainPackagesRepositoriesIndexImport } from './../app/_main/packages/repositories/index'
|
||||
|
||||
// Create/Update Routes
|
||||
|
|
@ -129,6 +130,14 @@ const MainPackagesUserPackagesIndexRoute =
|
|||
getParentRoute: () => MainRouteRoute,
|
||||
} as any)
|
||||
|
||||
const MainPackagesTemplatesIndexRoute = MainPackagesTemplatesIndexImport.update(
|
||||
{
|
||||
id: '/packages/templates/',
|
||||
path: '/packages/templates/',
|
||||
getParentRoute: () => MainRouteRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const MainPackagesRepositoriesIndexRoute =
|
||||
MainPackagesRepositoriesIndexImport.update({
|
||||
id: '/packages/repositories/',
|
||||
|
|
@ -196,6 +205,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof MainPackagesRepositoriesIndexImport
|
||||
parentRoute: typeof MainRouteImport
|
||||
}
|
||||
'/_main/packages/templates/': {
|
||||
id: '/_main/packages/templates/'
|
||||
path: '/packages/templates'
|
||||
fullPath: '/packages/templates'
|
||||
preLoaderRoute: typeof MainPackagesTemplatesIndexImport
|
||||
parentRoute: typeof MainRouteImport
|
||||
}
|
||||
'/_main/packages/user-packages/': {
|
||||
id: '/_main/packages/user-packages/'
|
||||
path: '/packages/user-packages'
|
||||
|
|
@ -270,6 +286,7 @@ interface MainRouteRouteChildren {
|
|||
MainProjectsIndexRoute: typeof MainProjectsIndexRoute
|
||||
MainSettingsIndexRoute: typeof MainSettingsIndexRoute
|
||||
MainPackagesRepositoriesIndexRoute: typeof MainPackagesRepositoriesIndexRoute
|
||||
MainPackagesTemplatesIndexRoute: typeof MainPackagesTemplatesIndexRoute
|
||||
MainPackagesUserPackagesIndexRoute: typeof MainPackagesUserPackagesIndexRoute
|
||||
MainProjectsManageIndexRoute: typeof MainProjectsManageIndexRoute
|
||||
MainSettingsLicensesIndexRoute: typeof MainSettingsLicensesIndexRoute
|
||||
|
|
@ -281,6 +298,7 @@ const MainRouteRouteChildren: MainRouteRouteChildren = {
|
|||
MainProjectsIndexRoute: MainProjectsIndexRoute,
|
||||
MainSettingsIndexRoute: MainSettingsIndexRoute,
|
||||
MainPackagesRepositoriesIndexRoute: MainPackagesRepositoriesIndexRoute,
|
||||
MainPackagesTemplatesIndexRoute: MainPackagesTemplatesIndexRoute,
|
||||
MainPackagesUserPackagesIndexRoute: MainPackagesUserPackagesIndexRoute,
|
||||
MainProjectsManageIndexRoute: MainProjectsManageIndexRoute,
|
||||
MainSettingsLicensesIndexRoute: MainSettingsLicensesIndexRoute,
|
||||
|
|
@ -320,6 +338,7 @@ export interface FileRoutesByFullPath {
|
|||
'/projects': typeof MainProjectsIndexRoute
|
||||
'/settings': typeof MainSettingsIndexRoute
|
||||
'/packages/repositories': typeof MainPackagesRepositoriesIndexRoute
|
||||
'/packages/templates': typeof MainPackagesTemplatesIndexRoute
|
||||
'/packages/user-packages': typeof MainPackagesUserPackagesIndexRoute
|
||||
'/projects/manage': typeof MainProjectsManageIndexRoute
|
||||
'/settings/licenses': typeof MainSettingsLicensesIndexRoute
|
||||
|
|
@ -339,6 +358,7 @@ export interface FileRoutesByTo {
|
|||
'/projects': typeof MainProjectsIndexRoute
|
||||
'/settings': typeof MainSettingsIndexRoute
|
||||
'/packages/repositories': typeof MainPackagesRepositoriesIndexRoute
|
||||
'/packages/templates': typeof MainPackagesTemplatesIndexRoute
|
||||
'/packages/user-packages': typeof MainPackagesUserPackagesIndexRoute
|
||||
'/projects/manage': typeof MainProjectsManageIndexRoute
|
||||
'/settings/licenses': typeof MainSettingsLicensesIndexRoute
|
||||
|
|
@ -360,6 +380,7 @@ export interface FileRoutesById {
|
|||
'/_main/projects/': typeof MainProjectsIndexRoute
|
||||
'/_main/settings/': typeof MainSettingsIndexRoute
|
||||
'/_main/packages/repositories/': typeof MainPackagesRepositoriesIndexRoute
|
||||
'/_main/packages/templates/': typeof MainPackagesTemplatesIndexRoute
|
||||
'/_main/packages/user-packages/': typeof MainPackagesUserPackagesIndexRoute
|
||||
'/_main/projects/manage/': typeof MainProjectsManageIndexRoute
|
||||
'/_main/settings/licenses/': typeof MainSettingsLicensesIndexRoute
|
||||
|
|
@ -381,6 +402,7 @@ export interface FileRouteTypes {
|
|||
| '/projects'
|
||||
| '/settings'
|
||||
| '/packages/repositories'
|
||||
| '/packages/templates'
|
||||
| '/packages/user-packages'
|
||||
| '/projects/manage'
|
||||
| '/settings/licenses'
|
||||
|
|
@ -399,6 +421,7 @@ export interface FileRouteTypes {
|
|||
| '/projects'
|
||||
| '/settings'
|
||||
| '/packages/repositories'
|
||||
| '/packages/templates'
|
||||
| '/packages/user-packages'
|
||||
| '/projects/manage'
|
||||
| '/settings/licenses'
|
||||
|
|
@ -418,6 +441,7 @@ export interface FileRouteTypes {
|
|||
| '/_main/projects/'
|
||||
| '/_main/settings/'
|
||||
| '/_main/packages/repositories/'
|
||||
| '/_main/packages/templates/'
|
||||
| '/_main/packages/user-packages/'
|
||||
| '/_main/projects/manage/'
|
||||
| '/_main/settings/licenses/'
|
||||
|
|
@ -468,6 +492,7 @@ export const routeTree = rootRoute
|
|||
"/_main/projects/",
|
||||
"/_main/settings/",
|
||||
"/_main/packages/repositories/",
|
||||
"/_main/packages/templates/",
|
||||
"/_main/packages/user-packages/",
|
||||
"/_main/projects/manage/",
|
||||
"/_main/settings/licenses/"
|
||||
|
|
@ -504,6 +529,10 @@ export const routeTree = rootRoute
|
|||
"filePath": "_main/packages/repositories/index.tsx",
|
||||
"parent": "/_main"
|
||||
},
|
||||
"/_main/packages/templates/": {
|
||||
"filePath": "_main/packages/templates/index.tsx",
|
||||
"parent": "/_main"
|
||||
},
|
||||
"/_main/packages/user-packages/": {
|
||||
"filePath": "_main/packages/user-packages/index.tsx",
|
||||
"parent": "/_main"
|
||||
|
|
|
|||
|
|
@ -364,6 +364,19 @@
|
|||
"user packages:toast:package added": "The package was added successfully.",
|
||||
"user packages:toast:package removed": "The package was removed successfully.",
|
||||
|
||||
"packages:templates": "Templates",
|
||||
"templates:create template": "Create Template",
|
||||
"templates:category": "Category",
|
||||
"templates:category:builtin": "Builtin",
|
||||
"templates:tooltip:category:builtin": "Builtin Templates are templates built by ALCOM developers and shipped with ALCOM",
|
||||
"templates:category:alcom": "Custom",
|
||||
"templates:tooltip:category:alcom": "Custom Templates are templates you created and you can edit with ALCOM.",
|
||||
"templates:category:vcc": "VCC",
|
||||
"templates:tooltip:category:vcc": "VCC Templates are templates you created for VCC manually.",
|
||||
"templates:tooltip:remove template": "Remove Template",
|
||||
"templates:tooltip:remove builtin template": "You cannot remove Builtin Templates.",
|
||||
"templates:tooltip:remove vcc template": "You cannot remove VCC templates here. You should remove from Templates directory instead.",
|
||||
|
||||
|
||||
// Settings Page
|
||||
"settings": "Settings",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue