API: init icon resolve #1616

Merged
clan-bot merged 1 commits from hsjobeki/clan-core:hsjobeki-main into main 2024-06-12 11:24:45 +00:00
8 changed files with 181 additions and 183 deletions

View File

@ -2,6 +2,7 @@ import argparse
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse
from clan_cli.api import API from clan_cli.api import API
from clan_cli.clan.create import ClanMetaInfo from clan_cli.clan.create import ClanMetaInfo
@ -22,6 +23,7 @@ def show_clan_meta(uri: str | Path) -> ClanMetaInfo:
] ]
) )
res = "{}" res = "{}"
try: try:
proc = run_no_stdout(cmd) proc = run_no_stdout(cmd)
res = proc.stdout.strip() res = proc.stdout.strip()
@ -33,10 +35,36 @@ def show_clan_meta(uri: str | Path) -> ClanMetaInfo:
) )
clan_meta = json.loads(res) clan_meta = json.loads(res)
# Check if icon is a URL such as http:// or https://
# Check if icon is an relative path
# All other schemas such as file://, ftp:// are not yet supported.
icon_path: str | None = None
if meta_icon := clan_meta.get("icon"):
scheme = urlparse(meta_icon).scheme
if scheme in ["http", "https"]:
icon_path = meta_icon
elif scheme in [""]:
if Path(meta_icon).is_absolute():
raise ClanError(
"Invalid absolute path",
location=f"show_clan {uri}",
description="Icon path must be a URL or a relative path.",
)
else:
icon_path = str((Path(uri) / meta_icon).resolve())
else:
raise ClanError(
"Invalid schema",
location=f"show_clan {uri}",
description="Icon path must be a URL or a relative path.",
)
return ClanMetaInfo( return ClanMetaInfo(
name=clan_meta.get("name"), name=clan_meta.get("name"),
description=clan_meta.get("description", None), description=clan_meta.get("description", None),
icon=clan_meta.get("icon", None), icon=icon_path,
) )

View File

@ -1,13 +1,12 @@
const fs = require("fs"); import fs from "node:fs";
const path = require("path"); import postcss from "postcss";
import path from "node:path";
import css_url from "postcss-url";
const distPath = path.resolve(__dirname, "dist"); const distPath = path.resolve("dist");
const manifestPath = path.join(distPath, ".vite/manifest.json"); const manifestPath = path.join(distPath, ".vite/manifest.json");
const outputPath = path.join(distPath, "index.html"); const outputPath = path.join(distPath, "index.html");
const postcss = require("postcss");
const css_url = require("postcss-url");
fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => { fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
if (err) { if (err) {
return console.error("Failed to read manifest:", err); return console.error("Failed to read manifest:", err);

View File

@ -2,6 +2,7 @@
"name": "@clan/webview-ui", "name": "@clan/webview-ui",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"dev": "vite", "dev": "vite",

View File

@ -1,6 +1,8 @@
module.exports = { const config = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; };
export default config;

View File

@ -17,6 +17,8 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
console.log(import.meta.env); console.log(import.meta.env);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log("Development mode"); console.log("Development mode");
// Load the debugger in development mode
await import("solid-devtools");
window.webkit = window.webkit || { window.webkit = window.webkit || {
messageHandlers: { messageHandlers: {
gtk: { gtk: {
@ -28,12 +30,11 @@ if (import.meta.env.DEV) {
console.log("mock", { mock }); console.log("mock", { mock });
window.clan[method](JSON.stringify(mock)); window.clan[method](JSON.stringify(mock));
}, 1000); }, 200);
}, },
}, },
}, },
}; };
} }
postMessage;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
render(() => <App />, root!); render(() => <App />, root!);

View File

@ -19,21 +19,17 @@ interface ClanDetailsProps {
directory: string; directory: string;
} }
interface MetaFieldsProps { interface ClanFormProps {
directory?: string;
meta: ClanMeta; meta: ClanMeta;
actions: JSX.Element; actions: JSX.Element;
editable?: boolean; editable?: boolean;
directory?: string;
} }
const fn = (e: SubmitEvent) => { export const ClanForm = (props: ClanFormProps) => {
e.preventDefault(); const { meta, actions, editable = true, directory } = props;
console.log("form submit", e.currentTarget); const [formStore, { Form, Field }] = createForm<ClanMeta>({
}; initialValues: meta,
export default function Login() {
const [, { Form, Field }] = createForm<ClanMeta>({
initialValues: { name: "MyClan" },
}); });
const handleSubmit: SubmitHandler<ClanMeta> = (values, event) => { const handleSubmit: SubmitHandler<ClanMeta> = (values, event) => {
@ -52,8 +48,48 @@ export default function Login() {
console.log("submit", values); console.log("submit", values);
}; };
return ( return (
<div class="card-body"> <div class="card card-compact w-96 bg-base-100 shadow-xl">
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Field name="icon">
{(field, props) => (
<>
<figure>
<Show
when={field.value}
fallback={
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
group
</span>
}
>
{(icon) => (
<img
class="aspect-square size-60 rounded-lg"
src={icon()}
alt="Clan Logo"
/>
)}
</Show>
</figure>
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">Select icon</span>
</div>
<input
type="file"
class="file-input file-input-bordered w-full max-w-xs"
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
</>
)}
</Field>
<div class="card-body">
<div class="card-body">
<Field <Field
name="name" name="name"
validate={[required("Please enter a unique name for the clan.")]} validate={[required("Please enter a unique name for the clan.")]}
@ -107,101 +143,25 @@ export default function Login() {
</label> </label>
)} )}
</Field> </Field>
<Field name="icon"> {actions}
{(field, props) => (
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">Select icon</span>
</div> </div>
<input
type="file"
class="file-input file-input-bordered w-full max-w-xs"
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
<div class="card-actions justify-end">
<button class="btn btn-primary" type="submit">
Create
</button>
</div> </div>
</Form> </Form>
</div> </div>
); );
}
export const EditMetaFields = (props: MetaFieldsProps) => {
const { meta, editable, actions, directory } = props;
const [editing, setEditing] = createSignal<
keyof MetaFieldsProps["meta"] | null
>(null);
return (
<div class="card card-compact w-96 bg-base-100 shadow-xl">
<figure>
<img
src="https://www.shutterstock.com/image-vector/modern-professional-ninja-mascot-logo-260nw-1729854862.jpg"
alt="Clan Logo"
/>
</figure>
<div class="card-body">
<Login />
{/* <form onSubmit={fn}> */}
{/* <h2 class="card-title justify-between">
<input
classList={{
[cx("text-slate-600")]: editing() !== "name",
}}
readOnly={editing() !== "name"}
class="w-full"
autofocus
onBlur={() => setEditing(null)}
type="text"
value={meta?.name}
onInput={(e) => {
console.log(e.currentTarget.value);
}}
/>
<Show when={editable}>
<button class="btn btn-square btn-ghost btn-sm">
<span
class="material-icons"
onClick={() => {
if (editing() !== "name") setEditing("name");
else {
setEditing(null);
}
}}
>
<Show when={editing() !== "name"} fallback="check">
edit
</Show>
</span>
</button>
</Show>
</h2>
<div class="flex gap-1 align-middle leading-8">
<i class="material-icons">description</i>
<span>{meta?.description || "No description"}</span>
</div>
<Show when={directory}>
<div class="flex gap-1 align-middle leading-8">
<i class="material-icons">folder</i>
<span>{directory}</span>
</div>
</Show>
{actions} */}
{/* </form> */}
</div>
</div>
);
}; };
// export const EditMetaFields = (props: MetaFieldsProps) => {
// const { meta, editable, actions, directory } = props;
// const [editing, setEditing] = createSignal<
// keyof MetaFieldsProps["meta"] | null
// >(null);
// return (
// );
// };
type ClanMeta = Extract< type ClanMeta = Extract<
OperationResponse<"show_clan_meta">, OperationResponse<"show_clan_meta">,
{ status: "success" } { status: "success" }
@ -209,7 +169,7 @@ type ClanMeta = Extract<
export const ClanDetails = (props: ClanDetailsProps) => { export const ClanDetails = (props: ClanDetailsProps) => {
const { directory } = props; const { directory } = props;
const [, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const [errors, setErrors] = createSignal< const [errors, setErrors] = createSignal<
| Extract< | Extract<
OperationResponse<"show_clan_meta">, OperationResponse<"show_clan_meta">,
@ -237,11 +197,16 @@ export const ClanDetails = (props: ClanDetailsProps) => {
}); });
return ( return (
<Switch fallback={"loading"}> <Switch fallback={"loading"}>
<Match when={loading()}>
<div>Loading</div>
</Match>
<Match when={data()}> <Match when={data()}>
<EditMetaFields {(data) => {
const meta = data();
return (
<ClanForm
directory={directory} directory={directory}
// @ts-expect-error: TODO: figure out how solid allows type narrowing this meta={meta}
meta={data()}
actions={ actions={
<div class="card-actions justify-between"> <div class="card-actions justify-between">
<button class="btn btn-link" onClick={() => loadMeta()}> <button class="btn btn-link" onClick={() => loadMeta()}>
@ -251,9 +216,11 @@ export const ClanDetails = (props: ClanDetailsProps) => {
</div> </div>
} }
/> />
);
}}
</Match> </Match>
<Match when={errors()}> <Match when={errors()}>
<button class="btn btn-link" onClick={() => loadMeta()}> <button class="btn btn-secondary" onClick={() => loadMeta()}>
Retry Retry
</button> </button>
<For each={errors()}> <For each={errors()}>

View File

@ -1,7 +1,7 @@
import { pyApi } from "@/src/api"; import { pyApi } from "@/src/api";
import { Match, Switch, createEffect, createSignal } from "solid-js"; import { Match, Switch, createEffect, createSignal } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
import { ClanDetails, EditMetaFields } from "./clanDetails"; import { ClanDetails, ClanForm } from "./clanDetails";
export const clan = () => { export const clan = () => {
const [mode, setMode] = createSignal<"init" | "open" | "create">("init"); const [mode, setMode] = createSignal<"init" | "open" | "create">("init");
@ -53,16 +53,16 @@ export const clan = () => {
<ClanDetails directory={clanDir() || ""} /> <ClanDetails directory={clanDir() || ""} />
</Match> </Match>
<Match when={mode() === "create"}> <Match when={mode() === "create"}>
<EditMetaFields <ClanForm
actions={ actions={
<div class="card-actions justify-end"> <div class="card-actions justify-end">
<button <button
class="btn btn-primary" class="btn btn-primary"
onClick={() => { // onClick={() => {
pyApi.open_file.dispatch({ // pyApi.open_file.dispatch({
file_request: { mode: "save" }, // file_request: { mode: "save" },
}); // });
}} // }}
> >
Save Save
</button> </button>

View File

@ -1,6 +1,6 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid"; import solidPlugin from "vite-plugin-solid";
// import devtools from "solid-devtools/vite"; import devtools from "solid-devtools/vite";
import path from "node:path"; import path from "node:path";
export default defineConfig({ export default defineConfig({
@ -14,7 +14,7 @@ export default defineConfig({
Uncomment the following line to enable solid-devtools. Uncomment the following line to enable solid-devtools.
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
*/ */
// devtools(), devtools(),
solidPlugin(), solidPlugin(),
], ],
server: { server: {