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",
"license": "MIT",
"dependencies": {
"@solid-primitives/storage": "^4.3.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-js": "^1.9.3"
},
"devDependencies": {
@ -1048,6 +1050,36 @@
"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": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
@ -1284,6 +1316,15 @@
"@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": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View file

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

29
src-tauri/Cargo.lock generated
View file

@ -3282,6 +3282,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-store",
]
[[package]]
@ -3665,6 +3666,22 @@ dependencies = [
"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]]
name = "tauri-runtime"
version = "2.5.1"
@ -3883,9 +3900,21 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"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]]
name = "tokio-util"
version = "0.7.14"

View file

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

View file

@ -2,9 +2,17 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"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)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())

View file

@ -2,5 +2,10 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
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()
}

View file

@ -20,6 +20,7 @@
--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-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);
background-color: var(--color-background);
@ -112,20 +113,21 @@
user-select: none;
flex-basis: 0;
flex-grow: 1;
padding: calc(1 * var(--unit));
padding: calc(0.5 * var(--unit));
border-radius: calc(1 * var(--unit));
background-color: var(--color-foreground-trace);
font-size: calc(5 * var(--unit));
font-weight: 600;
}
.badge {
font-size: calc(3 * var(--unit));
position: absolute;
top: 0;
left: 0;
filter: grayscale(100%);
}
.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);
}
.puzzle-group {
@ -157,18 +159,17 @@
font-weight: 700;
}
.puzzle-group-members {
font-weight: 500;
font-weight: 400;
font-size: 0.8em;
}
.puzzle-actions {
display: flex;
margin-top: calc(1 * var(--unit));
margin-top: calc(4 * var(--unit));
gap: calc(1 * var(--unit));
}
.puzzle-actions-secondary {
display: flex;
flex-direction: column;
gap: calc(1 * var(--unit));
min-width: calc(20 * var(--unit));
&>button {
flex-basis: 0;
flex-grow: 1;
}
}
select {
@ -212,6 +213,3 @@ button:focus-visible, button:hover {
button:active {
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 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() {
const {
connections,
@ -12,6 +19,7 @@ function App() {
handlePinUnpin,
handleSelectGame,
handleShuffle,
handleDeselect,
getFromPuzzle,
} = useAppModel();
@ -97,21 +105,24 @@ function App() {
)}
</For>
<div class="puzzle-actions">
<div class="puzzle-actions-secondary">
<button
type="button"
disabled={store.selected.length === 0}
on:click={handlePinUnpin}
>
{store.selected.length > 0 &&
store.selected.every((x) => x < store.pinnedCount)
? "Unpin"
: "Pin"}
</button>
<button type="button" on:click={handleShuffle}>
Shuffle
</button>
</div>
<button
type="button"
disabled={store.selected.length === 0}
on:click={handlePinUnpin}
>
{store.selected.length > 0 &&
store.selected.every((x) => x < store.pinnedCount)
? "Unpin"
: "Pin"}
</button>
<button type="button" on:click={handleShuffle}>
Shuffle
</button>
<button type="button" on:click={handleDeselect} disabled={
store.selected.length === 0
}>
Deselect all
</button>
<button
id="submitButton"
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> }) {
let textRef: SVGTextElement | undefined;
const [width, setWidth] = createSignal(100);
const [height, setHeight] = createSignal(100);
createEffect(
on(
props.body,
async () => {
setWidth(100)
setHeight(100)
await new Promise(resolve => setTimeout(resolve, 1))
const bounds = textRef?.getBBox();
if (bounds === undefined) {
return;
}
setWidth(bounds.width);
setHeight(bounds.height);
}
)
)
createEffect(() => {
props.body();
const bounds = textRef?.getBBox();
if (bounds === undefined) {
return;
}
setWidth(bounds.width);
setHeight(bounds.height);
});
return (
<svg
style={{
width: "100%",
"max-height": "50%",
"max-height": "40%",
}}
viewBox={`0 0 ${width()} ${height()}`}
overflow="visible"
@ -38,8 +31,9 @@ export default function FitText(props: { body: Accessor<string> }) {
y="50%"
dominant-baseline="central"
text-anchor="middle"
font-size="1rem"
font-size="30px"
font-family="inherit"
font-weight="inherit"
>
{props.body()}
</text>

View file

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