API: init icon resolve #1616
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
module.exports = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
@ -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!);
|
||||||
|
@ -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()}>
|
||||||
|
@ -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>
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user