This commit is contained in:
Michael Zhang 2024-05-25 05:04:05 -05:00
parent b7cad16044
commit b56e5a24f1
71 changed files with 3089 additions and 2980 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*]
indent_size = 2
indent_style = space

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
node_modules
dist
dist
target

File diff suppressed because it is too large Load diff

2
Cargo.toml Normal file
View file

@ -0,0 +1,2 @@
workspace.resolver = "2"
workspace.members = ["crates/*", "app/src-tauri"]

24
app/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
app/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View file

@ -1,6 +1,6 @@
# Tauri + Solid + Typescript
# Tauri + React + Typescript
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
This template should help get you started developing with Tauri, React and Typescript in Vite.
## Recommended IDE Setup

14
app/index.html Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

27
app/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "panorama-app",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@fontsource/inter": "^5.0.18",
"@tanstack/react-query": "^5.37.1",
"@tauri-apps/api": "^1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.2.1",
"sass": "^1.77.2",
"typescript": "^5.0.2",
"vite": "^5.0.0"
}
}

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

7
app/src-tauri/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

20
app/src-tauri/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "panorama"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "http-request", "shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

15
app/src-tauri/src/main.rs Normal file
View file

@ -0,0 +1,15 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -0,0 +1,48 @@
{
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "panorama",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"http": {
"all": false,
"request": true,
"scope": ["http://localhost:5195/*"]
}
},
"windows": [
{
"title": "panorama",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

18
app/src/App.module.scss Normal file
View file

@ -0,0 +1,18 @@
.container {
display: flex;
flex-direction: column;
min-height: 0;
width: 100%;
height: 100%;
}
.nodeContainer {
flex-grow: 1;
display: flex;
justify-items: stretch;
padding: 12px;
}

45
app/src/App.tsx Normal file
View file

@ -0,0 +1,45 @@
import Header from "./components/Header";
import styles from "./App.module.scss";
import "@fontsource/inter";
import "./global.scss";
import { useEffect, useState } from "react";
import NodeDisplay from "./components/NodeDisplay";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
const [nodesOpened, setNodesOpened] = useState<string[]>(() => []);
useEffect(() => {
(async () => {
console.log("ndoes", nodesOpened);
if (nodesOpened.length === 0) {
console.log("Opening today's entry.");
const resp = await fetch(
"http://localhost:5195/journal/get_todays_journal_id",
);
const data = await resp.json();
console.log("resp", data);
setNodesOpened([data.node_id]);
}
})();
}, [nodesOpened]);
const nodes = nodesOpened.map((nodeId) => (
<NodeDisplay key={nodeId} id={nodeId} />
));
return (
<QueryClientProvider client={queryClient}>
<div className={styles.container}>
<Header />
<div className={styles.nodeContainer}>{nodes}</div>
</div>
</QueryClientProvider>
);
}
export default App;

1
app/src/assets/react.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,10 @@
.Header {
display: flex;
align-items: center;
padding: 12px;
gap: 12px;
background: rgb(204, 201, 255);
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
}

View file

@ -0,0 +1,10 @@
import styles from "./Header.module.scss";
export default function Header() {
return (
<div className={styles.Header}>
<span>Panorama</span>
<input type="text" placeholder="Search..." />
</div>
);
}

View file

@ -0,0 +1,9 @@
.container {
max-width: 400px;
overflow-wrap: break-word;
overflow-y: auto;
border: 1px solid lightgray;
padding: 12px;
}

View file

@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import styles from "./NodeDisplay.module.scss";
export interface NodeDisplayProps {
id: string;
}
export default function NodeDisplay({ id }: NodeDisplayProps) {
const query = useQuery({
queryKey: ["fetchNode", id],
queryFn: async () => {
const resp = await fetch(`http://localhost:5195/node/${id}`);
console.log("id", resp);
return "helloge";
},
});
return (
<div className={styles.container}>
Node {id}
<p>{JSON.stringify(query)}</p>
</div>
);
}

15
app/src/global.scss Normal file
View file

@ -0,0 +1,15 @@
body,
html {
padding: 0;
margin: 0;
overscroll-behavior: none;
font-family: "Inter";
}
body,
html,
#root {
width: 100%;
height: 100%;
}

9
app/src/main.tsx Normal file
View file

@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

25
app/tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -1,13 +1,9 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
import { internalIpV4 } from "internal-ip";
// @ts-expect-error process is a nodejs global
const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM);
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [solid()],
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
@ -17,13 +13,9 @@ export default defineConfig(async () => ({
server: {
port: 1420,
strictPort: true,
host: mobile ? "0.0.0.0" : false,
hmr: mobile
? {
protocol: "ws",
host: await internalIpV4(),
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

View file

@ -1,13 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": { "indentWidth": 2, "indentStyle": "space" },
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "panorama-daemon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.86"
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
dirs = "5.0.1"
futures = "0.3.30"
miette = "5.5.0"
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
sugars = "3.0.1"
tokio = { version = "1.37.0", features = ["full"] }
tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["cors"] }
uuid = { version = "1.8.0", features = ["v7"] }

View file

@ -0,0 +1,32 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use miette::{IntoDiagnostic, Report};
pub type AppResult<T, E = AppError> = std::result::Result<T, E>;
// Make our own error that wraps `anyhow::Error`.
pub struct AppError(miette::Report);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<miette::Report>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View file

@ -0,0 +1,65 @@
use axum::{extract::State, Json};
use chrono::Local;
use cozo::{DataValue, ScriptMutability};
use serde_json::Value;
use uuid::Uuid;
use crate::{ensure_ok, error::AppResult, AppState};
pub async fn get_todays_journal_id(
State(state): State<AppState>,
) -> AppResult<Json<Value>> {
let today = todays_date();
println!("Getting journal id for {today}!");
let result = state.db.run_script(
"
?[node_id] := *journal_days[day, node_id], day = $day
",
btmap! {
"day".to_owned() => today.clone().into(),
},
ScriptMutability::Immutable,
)?;
println!("Result: {:?}", result);
if result.rows.len() == 0 {
// Insert a new one
let uuid = Uuid::now_v7();
let node_id = uuid.to_string();
let result = state.db.run_script_fold_err(
"
{
?[id] <- [[$node_id]]
:put node { id }
}
{
?[node_id, plaintext] <- [[$node_id, '']]
:put journal { node_id => plaintext }
}
{
?[day, node_id] <- [[$day, $node_id]]
:put journal_days { day => node_id }
}
",
btmap! {
"node_id".to_owned() => node_id.into(),
"day".to_owned() => today.clone().into(),
},
ScriptMutability::Mutable,
);
}
let node_id = result.rows[0][0].get_str().unwrap();
Ok(Json(json!({
"node_id": node_id
})))
}
fn todays_date() -> String {
let now = Local::now();
let date = now.date_naive();
date.format("%Y-%m-%d").to_string()
}

View file

@ -0,0 +1,79 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate sugars;
mod error;
mod journal;
mod migrations;
mod node;
use std::fs;
use anyhow::Result;
use axum::{http::Method, routing::get, Router};
use cozo::DbInstance;
use serde_json::Value;
use tokio::net::TcpListener;
use tower::ServiceBuilder;
use tower_http::cors::{self, CorsLayer};
use crate::{
journal::get_todays_journal_id, migrations::run_migrations, node::get_node,
};
#[derive(Clone)]
pub struct AppState {
db: DbInstance,
}
#[tokio::main]
async fn main() -> Result<()> {
let data_dir = dirs::data_dir().unwrap();
let panorama_dir = data_dir.join("panorama");
let db_path = panorama_dir.join("db.sqlite");
fs::create_dir_all(panorama_dir)?;
let db = DbInstance::new(
"sqlite",
db_path.display().to_string(),
Default::default(),
)
.unwrap();
run_migrations(&db).await?;
let state = AppState { db };
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(cors::Any);
// build our application with a single route
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/node/{id}", get(get_node))
.route("/journal/get_todays_journal_id", get(get_todays_journal_id))
.layer(ServiceBuilder::new().layer(cors))
.with_state(state);
let listener = TcpListener::bind("0.0.0.0:5195").await?;
println!("Listening...");
axum::serve(listener, app).await?;
Ok(())
}
pub fn ensure_ok(s: &str) -> Result<()> {
let status: Value = serde_json::from_str(&s)?;
let status = status.as_object().unwrap();
let ok = status.get("ok").unwrap().as_bool().unwrap_or(false);
if !ok {
bail!("shit (error: {s})")
}
Ok(())
}

View file

@ -0,0 +1,150 @@
use anyhow::{bail, Result};
use cozo::{DbInstance, ScriptMutability};
use futures::Future;
use serde_json::Value;
use crate::ensure_ok;
pub async fn run_migrations(db: &DbInstance) -> Result<()> {
let migration_status = check_migration_status(db).await?;
println!("migration status: {:?}", migration_status);
let migrations: Vec<Box<dyn for<'a> Fn(&'a DbInstance) -> Result<()>>> = vec![
Box::new(no_op),
Box::new(migration_01),
Box::new(migration_02),
];
if let MigrationStatus::NoMigrations = migration_status {
let result = db.run_script_str(
"
{ :create migrations { yeah: Int default 0 => version: Int default 0 } }
{
?[yeah, version] <- [[0, 0]]
:put migrations { yeah, version }
}
",
"",
false,
);
ensure_ok(&result)?;
}
let start_at_migration = match migration_status {
MigrationStatus::NoMigrations => 0,
MigrationStatus::HasVersion(n) => n,
};
let migrations_to_run = migrations
.iter()
.enumerate()
.skip(start_at_migration as usize + 1);
// println!("running {} migrations...", migrations_to_run.len());
//TODO: This should all be done in a transaction
for (idx, migration) in migrations_to_run {
println!("running migration {idx}...");
migration(db)?;
let result = db.run_script_str(
"
?[yeah, version] <- [[0, $version]]
:put migrations { yeah => version }
",
&format!("{{\"version\":{}}}", idx),
false,
);
ensure_ok(&result)?;
println!("succeeded migration {idx}!");
}
Ok(())
}
#[derive(Debug)]
enum MigrationStatus {
NoMigrations,
HasVersion(u64),
}
async fn check_migration_status(db: &DbInstance) -> Result<MigrationStatus> {
let status = db.run_script_str(
"
?[yeah, version] := *migrations[yeah, version]
",
"",
true,
);
println!("Status: {}", status);
let status: Value = serde_json::from_str(&status)?;
let status = status.as_object().unwrap();
let ok = status.get("ok").unwrap().as_bool().unwrap_or(false);
if !ok {
let status_code = status.get("code").unwrap().as_str().unwrap();
if status_code == "query::relation_not_found" {
return Ok(MigrationStatus::NoMigrations);
}
}
let rows = status.get("rows").unwrap().as_array().unwrap();
let row = rows[0].as_array().unwrap();
let version = row[1].as_number().unwrap().as_u64().unwrap();
println!("row: {row:?}");
Ok(MigrationStatus::HasVersion(version))
}
fn no_op(_: &DbInstance) -> Result<()> {
Ok(())
}
fn migration_01(db: &DbInstance) -> Result<()> {
let result = db.run_script_str(
"
# Primary node type
{
:create node {
id: String
=>
created_at: Float default now(),
updated_at: Float default now(),
extra_data: Json default {},
}
}
# Inverse mapping from keys to nodes
{ :create has_key { key: String => id: String } }
{ :create node_managed_by_app { node_id: String => app: String } }
",
"",
false,
);
ensure_ok(&result)?;
Ok(())
}
fn migration_02(db: &DbInstance) -> Result<()> {
let result = db.run_script_str(
"
# Create journal type
{ :create journal { node_id: String => plaintext: String } }
{ :create journal_days { day: String => node_id: String } }
{
::fts create journal:text_index {
extractor: plaintext,
extract_filter: !is_null(plaintext),
tokenizer: Simple,
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
}
}
",
"",
false,
);
ensure_ok(&result)?;
Ok(())
}

View file

@ -0,0 +1,27 @@
use axum::{
extract::{Path, State},
Json,
};
use cozo::ScriptMutability;
use serde_json::Value;
use crate::{error::AppResult, AppState};
pub async fn get_node(
State(state): State<AppState>,
Path(node_id): Path<String>,
) -> AppResult<Json<Value>> {
let result = state.db.run_script(
"
?[extra_data] := *node{ id, extra_data }, id = $node_id
",
btmap! {"node_id".to_owned() => node_id.clone().into()},
ScriptMutability::Immutable,
)?;
println!("REUSLT {:?}", result);
Ok(Json(json!({
"node": node_id,
})))
}

View file

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
<title>Tauri + Solid + Typescript App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View file

@ -1,27 +0,0 @@
{
"name": "panorama",
"version": "0.0.0",
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"tauri": "tauri"
},
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2.0.0-alpha.11",
"@tauri-apps/plugin-shell": "^2.0.0-alpha.3",
"solid-js": "^1.7.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0-alpha.17",
"internal-ip": "^7.0.0",
"sass": "^1.70.0",
"typescript": "^5.0.2",
"vite": "^4.4.4",
"vite-plugin-solid": "^2.7.0"
}
}

File diff suppressed because it is too large Load diff

3
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,3 @@
packages:
# - 'react'
- 'app'

View file

@ -1,4 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View file

@ -1,33 +0,0 @@
[package]
name = "panorama"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "panorama_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-alpha", features = [] }
[dependencies]
tauri = { version = "2.0.0-alpha", features = [] }
tauri-plugin-window = "2.0.0-alpha"
tauri-plugin-shell = "2.0.0-alpha"
serde_json = "1.0"
cozo = { version = "0.7.5", features = ["storage-rocksdb"] }
dirs = "5.0.1"
eyre = "0.6.12"
miette = { version = "5.10.0", features = ["backtrace", "fancy", "is-terminal", "serde"] }
autosurgeon = "0.8.3"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View file

@ -1,28 +0,0 @@
mod schema;
use cozo::{DbInstance, ScriptMutability};
use schema::ensure_schema;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let dir = dirs::data_dir().unwrap().join("panorama");
let db_path = dir.join("cozo-db");
let db = DbInstance::new("rocksdb", db_path, Default::default()).unwrap();
if let Err(err) = ensure_schema(&db) {
println!("WTF? {:?}", err);
}
tauri::Builder::default()
.plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
panorama_lib::run()
}

View file

@ -1,35 +0,0 @@
use std::collections::BTreeMap;
use cozo::{DataValue, DbInstance, ScriptMutability};
use miette::Result;
pub fn ensure_schema(db: &DbInstance) -> Result<()> {
let existing_relations = db.run_script(
"::relations",
Default::default(),
ScriptMutability::Immutable,
)?;
println!("Result: {:?}", existing_relations);
db.run_script(
"%ignore_error {:create node {id: String, content: String}}",
Default::default(),
ScriptMutability::Mutable,
)?;
let mut create_rule_params = BTreeMap::new();
create_rule_params
.insert(format!("input_data"), DataValue::from(format!("root")));
db.run_script(
"
%if { len[count(x)] := *node[x, y]; ?[x] := len[z], x = z == 0 }
{ ?[node, content] := $input_data; :put node {id, content} }
%end
",
create_rule_params,
ScriptMutability::Mutable,
)?;
Ok(())
}

View file

@ -1,43 +0,0 @@
{
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "panorama",
"version": "0.0.0"
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"security": {
"csp": null
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "panorama",
"width": 800,
"height": 600
}
]
},
"plugins": {
"shell": {
"open": true
}
}
}

View file

@ -1,6 +0,0 @@
.container {
display: flex;
flex-direction: column;
align-items: stretch;
}

View file

@ -1,21 +0,0 @@
import NodeContainer from "./components/NodeContainer";
import Sidebar from "./components/Sidebar";
import styles from "./App.module.scss";
import { createSignal } from "solid-js";
function App() {
const [getFocusedNodeId, setFocusedNodeId] = createSignal<string | null>(
null,
);
const focusedNodeId = getFocusedNodeId();
return (
<div class={styles.container}>
<Sidebar />
{focusedNodeId !== null && <NodeContainer id={focusedNodeId} />}
</div>
);
}
export default App;

View file

@ -1,7 +0,0 @@
export interface NodeContainerProps {
id: string;
}
export default function NodeContainer({ id }: NodeContainerProps) {
return <>{id}</>;
}

View file

@ -1,8 +0,0 @@
.sidebar {
background-color: skyblue;
display: flex;
flex-direction: column;
flex-grow: 1;
width: 18rem;
}

View file

@ -1,13 +0,0 @@
import styles from "./Sidebar.module.scss";
export default function Sidebar() {
return (
<div class={styles.sidebar}>
<h1>Panorama</h1>
<h3>Bookmarked Nodes</h3>
<h3>Settings</h3>
</div>
);
}

View file

@ -1 +0,0 @@
declare module "*.scss";

View file

@ -1,7 +0,0 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./styles.scss";
import App from "./App";
render(() => <App />, document.getElementById("root") as HTMLElement);

View file

@ -1,23 +0,0 @@
html,
body,
#root {
margin: 0;
padding: 0;
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}

View file

@ -1,26 +1,3 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"compilerOptions": { "jsx": "react-jsx" }
}