persistence

This commit is contained in:
Joshua Seigler 2025-04-06 18:38:38 -07:00
parent 9649d407fd
commit 3d58a0663a
11 changed files with 176 additions and 60 deletions

41
package-lock.json generated
View file

@ -9,8 +9,10 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@solid-primitives/storage": "^4.3.1",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-js": "^1.9.3" "solid-js": "^1.9.3"
}, },
"devDependencies": { "devDependencies": {
@ -1048,6 +1050,36 @@
"win32" "win32"
] ]
}, },
"node_modules/@solid-primitives/storage": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-4.3.1.tgz",
"integrity": "sha512-xAJsY2pvXrAaCai4N2grmWY3xh5om9suTDVzGkRF5JBpDzs3Apk+xIovdTErbW0iCzXIEefENXb9xmSzdjuLYA==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.0"
},
"peerDependencies": {
"@tauri-apps/plugin-store": "*",
"solid-js": "^1.6.12"
},
"peerDependenciesMeta": {
"@tauri-apps/plugin-store": {
"optional": true
},
"solid-start": {
"optional": true
}
}
},
"node_modules/@solid-primitives/utils": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.0.tgz",
"integrity": "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A==",
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@tauri-apps/api": { "node_modules/@tauri-apps/api": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
@ -1284,6 +1316,15 @@
"@tauri-apps/api": "^2.0.0" "@tauri-apps/api": "^2.0.0"
} }
}, },
"node_modules/@tauri-apps/plugin-store": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.2.0.tgz",
"integrity": "sha512-hJTRtuJis4w5fW1dkcgftsYxKXK0+DbAqurZ3CURHG5WkAyyZgbxpeYctw12bbzF9ZbZREXZklPq8mocCC3Sgg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View file

@ -12,8 +12,10 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@solid-primitives/storage": "^4.3.1",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-js": "^1.9.3" "solid-js": "^1.9.3"
}, },
"devDependencies": { "devDependencies": {

29
src-tauri/Cargo.lock generated
View file

@ -3282,6 +3282,7 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-store",
] ]
[[package]] [[package]]
@ -3665,6 +3666,22 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-store"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c0c08fae6995909f5e9a0da6038273b750221319f2c0f3b526d6de1cde21505"
dependencies = [
"dunce",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.5.1" version = "2.5.1"
@ -3883,9 +3900,21 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.14" version = "0.7.14"

View file

@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tauri-plugin-store = "2"

View file

@ -2,9 +2,17 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default" "opener:default",
"store:allow-get",
"store:allow-set",
"store:allow-delete",
"store:allow-keys",
"store:allow-clear",
"store:default"
] ]
} }

View file

@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet]) .invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!()) .run(tauri::generate_context!())

View file

@ -2,5 +2,10 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
tauri::Builder::default()
// initialize store plugin:
.plugin(tauri_plugin_store::Builder::new().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
slick_connections_lib::run() slick_connections_lib::run()
} }

View file

@ -20,6 +20,7 @@
--color-background: lch(var(--background-lightness) var(--background-chroma) var(--background-hue)); --color-background: lch(var(--background-lightness) var(--background-chroma) var(--background-hue));
--color-foreground-trace: lch(var(--foreground-lightness) var(--foreground-chroma) var(--foreground-hue) / 0.1); --color-foreground-trace: lch(var(--foreground-lightness) var(--foreground-chroma) var(--foreground-hue) / 0.1);
--color-foreground-faint: lch(var(--foreground-lightness) var(--foreground-chroma) var(--foreground-hue) / 0.25); --color-foreground-faint: lch(var(--foreground-lightness) var(--foreground-chroma) var(--foreground-hue) / 0.25);
--color-foreground-medium: lch(var(--foreground-lightness) var(--foreground-chroma) var(--foreground-hue) / 0.5);
color: var(--color-foreground); color: var(--color-foreground);
background-color: var(--color-background); background-color: var(--color-background);
@ -112,20 +113,21 @@
user-select: none; user-select: none;
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
padding: calc(1 * var(--unit)); padding: calc(0.5 * var(--unit));
border-radius: calc(1 * var(--unit)); border-radius: calc(1 * var(--unit));
background-color: var(--color-foreground-trace); background-color: var(--color-foreground-trace);
font-size: calc(5 * var(--unit)); font-size: calc(5 * var(--unit));
font-weight: 600; font-weight: 600;
} }
.badge { .badge {
font-size: calc(3 * var(--unit));
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
filter: grayscale(100%); filter: grayscale(100%);
} }
.puzzle-item.is-selected { .puzzle-item.is-selected {
background-color: var(--color-foreground-faint); background-color: var(--color-foreground-medium);
outline: calc(0.33 * var(--unit)) solid var(--color-foreground); outline: calc(0.33 * var(--unit)) solid var(--color-foreground);
} }
.puzzle-group { .puzzle-group {
@ -157,18 +159,17 @@
font-weight: 700; font-weight: 700;
} }
.puzzle-group-members { .puzzle-group-members {
font-weight: 500; font-weight: 400;
font-size: 0.8em;
} }
.puzzle-actions { .puzzle-actions {
display: flex; display: flex;
margin-top: calc(1 * var(--unit)); margin-top: calc(4 * var(--unit));
gap: calc(1 * var(--unit)); gap: calc(1 * var(--unit));
} &>button {
.puzzle-actions-secondary { flex-basis: 0;
display: flex; flex-grow: 1;
flex-direction: column; }
gap: calc(1 * var(--unit));
min-width: calc(20 * var(--unit));
} }
select { select {
@ -212,6 +213,3 @@ button:focus-visible, button:hover {
button:active { button:active {
box-shadow: 0 0 3em -1.5em inset var(--color-foreground) box-shadow: 0 0 3em -1.5em inset var(--color-foreground)
} }
#submitButton {
flex-grow: 1;
}

View file

@ -3,6 +3,13 @@ import "./App.css";
import useAppModel from "./useAppModel"; import useAppModel from "./useAppModel";
import FitText from "./FitText"; import FitText from "./FitText";
// TODO
// add routing
// make overview page with calendar list of puzzles
// show solved / aced / busted
// make detail page with puzzle id in path
// add nav links
function App() { function App() {
const { const {
connections, connections,
@ -12,6 +19,7 @@ function App() {
handlePinUnpin, handlePinUnpin,
handleSelectGame, handleSelectGame,
handleShuffle, handleShuffle,
handleDeselect,
getFromPuzzle, getFromPuzzle,
} = useAppModel(); } = useAppModel();
@ -97,21 +105,24 @@ function App() {
)} )}
</For> </For>
<div class="puzzle-actions"> <div class="puzzle-actions">
<div class="puzzle-actions-secondary"> <button
<button type="button"
type="button" disabled={store.selected.length === 0}
disabled={store.selected.length === 0} on:click={handlePinUnpin}
on:click={handlePinUnpin} >
> {store.selected.length > 0 &&
{store.selected.length > 0 && store.selected.every((x) => x < store.pinnedCount)
store.selected.every((x) => x < store.pinnedCount) ? "Unpin"
? "Unpin" : "Pin"}
: "Pin"} </button>
</button> <button type="button" on:click={handleShuffle}>
<button type="button" on:click={handleShuffle}> Shuffle
Shuffle </button>
</button> <button type="button" on:click={handleDeselect} disabled={
</div> store.selected.length === 0
}>
Deselect all
</button>
<button <button
id="submitButton" id="submitButton"
type="button" type="button"

View file

@ -1,32 +1,25 @@
import { createEffect, createSignal, on, type Accessor } from "solid-js"; import { createEffect, createSignal, type Accessor } from "solid-js";
export default function FitText(props: { body: Accessor<string> }) { export default function FitText(props: { body: Accessor<string> }) {
let textRef: SVGTextElement | undefined; let textRef: SVGTextElement | undefined;
const [width, setWidth] = createSignal(100); const [width, setWidth] = createSignal(100);
const [height, setHeight] = createSignal(100); const [height, setHeight] = createSignal(100);
createEffect( createEffect(() => {
on( props.body();
props.body, const bounds = textRef?.getBBox();
async () => { if (bounds === undefined) {
setWidth(100) return;
setHeight(100) }
await new Promise(resolve => setTimeout(resolve, 1)) setWidth(bounds.width);
const bounds = textRef?.getBBox(); setHeight(bounds.height);
if (bounds === undefined) { });
return;
}
setWidth(bounds.width);
setHeight(bounds.height);
}
)
)
return ( return (
<svg <svg
style={{ style={{
width: "100%", width: "100%",
"max-height": "50%", "max-height": "40%",
}} }}
viewBox={`0 0 ${width()} ${height()}`} viewBox={`0 0 ${width()} ${height()}`}
overflow="visible" overflow="visible"
@ -38,8 +31,9 @@ export default function FitText(props: { body: Accessor<string> }) {
y="50%" y="50%"
dominant-baseline="central" dominant-baseline="central"
text-anchor="middle" text-anchor="middle"
font-size="1rem" font-size="30px"
font-family="inherit" font-family="inherit"
font-weight="inherit"
> >
{props.body()} {props.body()}
</text> </text>

View file

@ -1,6 +1,8 @@
import { makePersisted } from "@solid-primitives/storage";
import connections from "./assets/connections.json"; import connections from "./assets/connections.json";
import { shuffleArray } from "./utils"; import { shuffleArray } from "./utils";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { tauriStorage } from "@solid-primitives/storage/tauri";
type Connection = (typeof connections)[number]; type Connection = (typeof connections)[number];
type Answer = Connection["answers"][number]; type Answer = Connection["answers"][number];
@ -11,17 +13,34 @@ function fromIndex(index: number): [number, number] {
return [row, col]; return [row, col];
} }
type AppStore = {
puzzleId: number;
pinnedCount: number;
selected: number[];
solvedGroups: Answer[];
puzzle: number[];
get answers(): Answer[];
};
export default function useAppModel() { export default function useAppModel() {
const [store, setStore] = createStore({ const storage =
puzzleId: 1, "__TAURI_INTERNALS__" in window ? tauriStorage("AppStore") : localStorage;
pinnedCount: 0, const [store, setStore] = makePersisted(
selected: [] as number[], createStore<AppStore>({
solvedGroups: [] as Answer[], puzzleId: 1,
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)), pinnedCount: 0,
get answers() { selected: [],
return connections.find((x) => x.id === store.puzzleId)!.answers; solvedGroups: [],
}, puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
}); get answers(): Answer[] {
return connections.find((x) => x.id === store.puzzleId)!.answers;
},
}),
{
name: "slick-connections",
storage,
}
);
const handleSelectGame = (newId: number) => { const handleSelectGame = (newId: number) => {
setStore({ setStore({
@ -83,6 +102,12 @@ export default function useAppModel() {
}); });
}; };
const handleDeselect = () => {
setStore({
selected: [],
});
};
const handlePinUnpin = () => { const handlePinUnpin = () => {
if (store.selected.every((x) => x < store.pinnedCount)) { if (store.selected.every((x) => x < store.pinnedCount)) {
// we are unpinning // we are unpinning
@ -125,6 +150,7 @@ export default function useAppModel() {
handlePinUnpin, handlePinUnpin,
handleSelectGame, handleSelectGame,
handleShuffle, handleShuffle,
handleDeselect,
getFromPuzzle, getFromPuzzle,
}; };
} }