1
0
forked from clan/clan-core

init webview: add webview ui and list machine as api example

This commit is contained in:
Johannes Kirschbauer 2024-05-14 18:55:44 +02:00 committed by hsjobeki
parent 97a1d8b52a
commit fef16a84a9
20 changed files with 3658 additions and 1 deletions

View File

@ -106,7 +106,7 @@ class MainApplication(Adw.Application):
def on_activate(self, source: "MainApplication") -> None:
if not self.window:
self.init_style()
self.window = MainWindow(config=ClanConfig(initial_view="list"))
self.window = MainWindow(config=ClanConfig(initial_view="webview"))
self.window.set_application(self)
self.window.show()

View File

@ -0,0 +1,51 @@
import sys
from pathlib import Path
from typing import Any, List
from clan_cli import machines
import time
import gi
import json
gi.require_version("WebKit", "6.0")
from gi.repository import WebKit
site_index: Path = (Path(sys.argv[0]).absolute() / Path("../..") / Path("web/app/dist/index.html") ).resolve()
class WebView():
def __init__(self) -> None:
self.webview = WebKit.WebView()
self.manager = self.webview.get_user_content_manager()
# Can be called with: window.webkit.messageHandlers.gtk.postMessage("...")
# Important: it seems postMessage must be given some payload, otherwise it won't trigger the event
self.manager.register_script_message_handler("gtk")
self.manager.connect("script-message-received", self.on_message_received)
self.webview.load_uri(f"file://{site_index}")
def on_message_received(
self, user_content_manager: WebKit.UserContentManager, message: Any
) -> None:
# payload = json.loads(message.to_json(0))
# TODO:
# Dynamically call functions in the js context
# I.e. the result function should have the same name as the target method in the gtk context
# Example:
# request -> { method: "list_machines", data: None }
# internally call list_machines and serialize the result
# result -> window.clan.list_machines(`{serialized}`)
list_of_machines = machines.list.list_machines(".")
serialized = json.dumps(list_of_machines)
# Important: use ` backticks to avoid escaping issues with conflicting quotes in js and json
self.webview.evaluate_javascript(f"""
setTimeout(() => {{
window.clan.setMachines(`{serialized}`);
}},2000);
""", -1, None, None, None)
def get_webview(self) -> WebKit.WebView:
return self.webview

View File

@ -11,6 +11,7 @@ from clan_vm_manager.singletons.use_vms import ClanStore
from clan_vm_manager.views.details import Details
from clan_vm_manager.views.list import ClanList
from clan_vm_manager.views.logs import Logs
from clan_vm_manager.views.webview import WebView
gi.require_version("Adw", "1")
@ -58,6 +59,7 @@ class MainWindow(Adw.ApplicationWindow):
stack_view.add_named(ClanList(config), "list")
stack_view.add_named(Details(), "details")
stack_view.add_named(Logs(), "logs")
stack_view.add_named(WebView().get_webview(), "webview")
stack_view.set_visible_child_name(config.initial_view)

View File

@ -12,6 +12,7 @@
clan-cli,
makeDesktopItem,
libadwaita,
webkitgtk_6_0,
pytest, # Testing framework
pytest-cov, # Generate coverage reports
pytest-subprocess, # fake the real subprocess behavior to make your tests more independent.
@ -35,6 +36,7 @@ let
pygobject-stubs
gtk4
libadwaita
webkitgtk_6_0
gnome.adwaita-icon-theme
];

View File

@ -0,0 +1,7 @@
# Webkit GTK doesn't interop flawless with Solid.js build result
1. Webkit expects script tag to be in `body` only solid.js puts the in the head.
2. script and css files are loaded with type="module" and crossorigin tags beeing set. WebKit silently fails to load then.
3. Paths to resiources are not allowed to start with "/" because webkit interprets them relative to the system and not the base url.
4. webkit doesn't support native features such as directly handling external urls (i.e opening them in the default browser)
6. Other problems to be found?

View File

@ -10,6 +10,7 @@
python3,
gtk4,
libadwaita,
nodejs_latest
}:
let
@ -28,6 +29,7 @@ mkShell {
inherit (clan-vm-manager) nativeBuildInputs;
buildInputs =
[
nodejs_latest
ruff
gtk4.dev # has the demo called 'gtk4-widget-factory'
libadwaita.devdoc # has the demo called 'adwaita-1-demo'

View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -0,0 +1,34 @@
## Usage
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
```bash
$ npm install # or pnpm install or yarn install
```
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
## Available Scripts
In the project directory, you can run:
### `npm run dev` or `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
### `npm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Solid App</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<div id="app"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"name": "vite-template-solid",
"version": "0.0.0",
"description": "",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"license": "MIT",
"devDependencies": {
"solid-devtools": "^0.29.2",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2",
"autoprefixer": "^10.4.19",
"daisyui": "^4.11.1",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3"
},
"dependencies": {
"solid-js": "^1.8.11"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,38 @@
import { Match, Switch, createSignal, type Component } from "solid-js";
import { CountProvider } from "./Config";
import { Nested } from "./nested";
type Route = "home" | "graph";
const App: Component = () => {
const [route, setRoute] = createSignal<Route>("home");
return (
<CountProvider>
<div class="w-full flex items-center flex-col gap-2 my-2">
<div>Clan</div>
<div class="flex items-center">
<button
onClick={() => setRoute((o) => (o === "graph" ? "home" : "graph"))}
class="btn btn-link"
>
Navigate to {route() === "home" ? "graph" : "home"}
</button>
<Switch fallback={<p>{route()} not found</p>}>
<Match when={route() == "home"}>
<p>Current route: {route()}</p>
</Match>
<Match when={route() == "graph"}>
<p>Current route: {route()}</p>
</Match>
</Switch>
</div>
<div class="flex items-center">
<Nested />
</div>
</div>
</CountProvider>
);
};
export default App;

View File

@ -0,0 +1,58 @@
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
const initialValue = 0 as const;
export const makeCountContext = () => {
const [count, setCount] = createSignal(0);
const [machines, setMachines] = createSignal<string[]>([]);
const [loading, setLoading] = createSignal(false);
// Add this callback to global window so we can test it from gtk
// @ts-ignore
window.clan.setMachines = (data: str) => {
try {
setMachines(JSON.parse(data));
} catch (e) {
alert(`Error parsing JSON: ${e}`);
} finally {
setLoading(false);
}
};
return [
{ count, loading, machines },
{
setCount,
setLoading,
setMachines,
getMachines: () => {
// When the gtk function sends its data the loading state will be set to false
setLoading(true);
// Example of how to dispatch a gtk function
// @ts-ignore
window.webkit.messageHandlers.gtk.postMessage(1);
},
},
] as const;
// `as const` forces tuple type inference
};
type CountContextType = ReturnType<typeof makeCountContext>;
export const CountContext = createContext<CountContextType>([
{ count: () => initialValue, loading: () => false, machines: () => [] },
{
setCount: () => {},
setLoading: () => {},
setMachines: () => {},
getMachines: () => {},
},
]);
export const useCountContext = () => useContext(CountContext);
export function CountProvider(props: { children: JSXElement }) {
return (
<CountContext.Provider value={makeCountContext()}>
{props.children}
</CountContext.Provider>
);
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,18 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./index.css";
import App from "./App";
const root = document.getElementById("app");
// @ts-ignore: add the clan scope to the window object so we can register callbacks for gtk
window.clan = window.clan || {};
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
);
}
render(() => <App />, root!);

View File

@ -0,0 +1,29 @@
import { For, Match, Switch, type Component } from "solid-js";
import { useCountContext } from "./Config";
export const Nested: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
return (
<div>
<button onClick={() => getMachines()} class="btn btn-primary">
Get machines
</button>
<hr />
<Switch>
<Match when={loading()}>Loading...</Match>
<Match when={!loading() && machines().length === 0}>
No machines found
</Match>
<Match when={!loading() && machines().length}>
<For each={machines()}>
{(machine, i) => (
<li>
{i() + 1}: {machine}
</li>
)}
</For>
</Match>
</Switch>
</div>
);
};

View File

@ -0,0 +1,9 @@
const daisyui = require("daisyui");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [daisyui],
};

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true,
"isolatedModules": true,
},
}

View File

@ -0,0 +1,22 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
// import devtools from "solid-devtools/vite";
export default defineConfig({
plugins: [
/*
Uncomment the following line to enable solid-devtools.
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
*/
// devtools(),
solidPlugin(),
],
server: {
port: 3000,
},
build: {
target: "safari11",
// assetsDi
manifest: true,
},
});