This commit is contained in:
Brian McGee
2025-07-09 10:03:00 +01:00
parent 8c46b01130
commit ec6c4ee68e
6 changed files with 275 additions and 42 deletions

View File

@@ -0,0 +1,92 @@
import { API } from "@/api/API";
import { Schema as Inventory } from "@/api/Inventory";
type OperationNames = keyof API;
type Services = NonNullable<Inventory["services"]>;
type ServiceNames = keyof Services;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
Services[T]
>[string];
export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "success" }
>;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
interface SendHeaderType {
logging?: { group_path: string[] };
}
interface BackendSendType<K extends OperationNames> {
body: OperationArgs<K>;
header?: SendHeaderType;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ReceiveHeaderType {}
interface BackendReturnType<K extends OperationNames> {
body: OperationResponse<K>;
header: ReceiveHeaderType;
}
/**
* Interface representing an API call with a unique identifier, result promise, and cancellation capability.
*
* @template K - A generic type parameter extending the set of operation names.
*
* @property {string} uuid - A unique identifier for the API call.
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
*/
interface ApiCall<K extends OperationNames> {
uuid: string;
result: Promise<OperationResponse<K>>;
cancel: () => Promise<void>;
}
export const callApi = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
backendOpts?: SendHeaderType,
): ApiCall<K> => {
// if window[method] does not exist, throw an error
if (!(method in window)) {
console.error(`Method ${method} not found on window object`);
return {
uuid: "",
result: Promise.reject(`Method ${method} not found on window object`),
cancel: () => Promise.resolve()
}
}
const req: BackendSendType<OperationNames> = {
body: args,
header: backendOpts,
};
const result = (
window as unknown as Record<
OperationNames,
(
args: BackendSendType<OperationNames>,
) => Promise<BackendReturnType<OperationNames>>
>
)[method](req) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (result as any)._webviewMessageId as string;
return {
uuid: op_key,
result: result.then(({ body }) => body),
cancel: async () => {
console.log("Cancelling api call: ", op_key);
await callApi("delete_task", { task_id: op_key }).result;
}
};
};

View File

@@ -0,0 +1,29 @@
import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator } from "@solidjs/router";
export const selectClanFolder = async () => {
const req = callApi("open_file", { file_request: { mode: "select_folder" } });
const res = await req.result;
if (res.status === "error") {
throw new Error(res.errors[0].message);
}
if (res.status === "success" && res.data) {
const [uri] = res.data;
addClanURI(uri);
setActiveClanURI(uri);
return uri;
}
throw new Error("Illegal state exception");
};
export const navigateToClan = (navigate: Navigator, uri: string) => {
navigate("/clan/" + encodeURIComponent(uri));
};
export const clanURIParam = (params: Params) => {
return decodeURIComponent(params.clanURI);
};

View File

@@ -0,0 +1,9 @@
import { RouteSectionProps, useParams } from "@solidjs/router";
import { Component } from "solid-js";
import { clanURIParam } from "@/src/hooks/clan";
export const Clan: Component<RouteSectionProps> = (props) => {
const params = useParams();
const clanURI = clanURIParam(params);
return <h1>{clanURI}</h1>;
};

View File

@@ -1,51 +1,61 @@
import { Component } from "solid-js";
import { RouteSectionProps } from "@solidjs/router";
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import "./Onboarding.css";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
import { Divider } from "@/src/components/Divider/Divider";
import { Logo } from "@/src/components/Logo/Logo";
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
export const Onboarding: Component<RouteSectionProps> = (props) => (
<main id="welcome">
<div class="background">
<div class="layer-1" />
<div class="layer-2" />
<div class="layer-3" />
</div>
<div class="choose">
<Logo variant="Darknet" inverted={true}/>
export const Onboarding: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate()
<div class="controls">
<Typography
hierarchy="headline"
size="xxl"
weight="bold"
align="center"
inverted={true}
>
Build your <br />
own darknet
</Typography>
<Button hierarchy="secondary">Start building</Button>
<div class="separator">
<Divider orientation="horizontal" inverted={true} />
<Typography
hierarchy="body"
size="s"
weight="medium"
inverted={true}
align="center"
>
or
</Typography>
<Divider orientation="horizontal" inverted={true} />
</div>
<Button hierarchy="primary" ghost={true}>
Select folder
</Button>
const selectFolder = async () => {
const uri = await selectClanFolder();
navigateToClan(navigate, uri);
};
return (
<main id="welcome">
<div class="background">
<div class="layer-1" />
<div class="layer-2" />
<div class="layer-3" />
</div>
<Logo variant="Clan" inverted={true}/>
</div>
</main>
);
<div class="choose">
<Logo variant="Darknet" inverted={true} />
<div class="controls">
<Typography
hierarchy="headline"
size="xxl"
weight="bold"
align="center"
inverted={true}
>
Build your <br />
own darknet
</Typography>
<Button hierarchy="secondary">Start building</Button>
<div class="separator">
<Divider orientation="horizontal" inverted={true} />
<Typography
hierarchy="body"
size="s"
weight="medium"
inverted={true}
align="center"
>
or
</Typography>
<Divider orientation="horizontal" inverted={true} />
</div>
<Button hierarchy="primary" ghost={true} onAction={selectFolder}>
Select folder
</Button>
</div>
<Logo variant="Clan" inverted={true} />
</div>
</main>
);
};

View File

@@ -1,9 +1,14 @@
import type { RouteDefinition } from "@solidjs/router/dist/types";
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
import { Clan } from "@/src/routes/Clan/Clan";
export const Routes: RouteDefinition[] = [
{
path: "/",
component: Onboarding,
},
{
path: "/clan/:clanURI",
component: Clan
}
];

View File

@@ -0,0 +1,88 @@
import { createStore, produce } from "solid-js/store";
import { makePersisted } from "@solid-primitives/storage";
interface ClanStoreType {
clanURIs: string[];
activeClanURI?: string;
}
const [store, setStore] = makePersisted(
createStore<ClanStoreType>({
clanURIs: [],
}),
{
name: "clanStore",
storage: localStorage,
},
);
/**
* Retrieves the active clan URI from the store.
*
* @function
* @returns {string} The URI of the active clan.
*/
const activeClanURI = (): string | undefined => store.activeClanURI;
/**
* Updates the active Clan URI in the store.
*
* @param {string} uri - The URI to be set as the active Clan URI.
*/
const setActiveClanURI = (uri: string) => setStore("activeClanURI", uri);
/**
* Retrieves the current list of clan URIs from the store.
*
* @function clanURIs
* @returns {*} The clan URIs from the store.
*/
const clanURIs = (): string[] => store.clanURIs;
/**
* Adds a new clan URI to the list of clan URIs in the store.
*
* @param {string} uri - The URI of the clan to be added.
*
*/
const addClanURI = (uri: string) =>
setStore("clanURIs", store.clanURIs.length, uri);
/**
* Removes a specified URI from the clan URI list and updates the active clan URI.
*
* This function modifies the store in the following ways:
* - Removes the specified URI from the `clanURIs` array.
* - Clears the `activeClanURI` if the removed URI matches the currently active URI.
* - Sets a new active clan URI to the last URI in the `clanURIs` array if the active clan URI is undefined
* and there are remaining clan URIs in the list.
*
* @param {string} uri - The URI to be removed from the clan list.
*/
const removeClanURI = (uri: string) => {
setStore(
produce((state) => {
// remove from the clan list
state.clanURIs = state.clanURIs.filter((el) => el !== uri);
// clear active clan uri if it's the one being removed
if (state.activeClanURI === uri) {
state.activeClanURI = undefined;
}
// select a new active URI if at least one remains
if (!state.activeClanURI && state.clanURIs.length > 0) {
state.activeClanURI = state.clanURIs[state.clanURIs.length - 1];
}
}),
);
};
export {
store,
activeClanURI,
setActiveClanURI,
clanURIs,
addClanURI,
removeClanURI,
};