Compare commits

..

3 commits
master ... ci

Author SHA1 Message Date
e12e860ef9 CI try #3 2025-05-03 19:57:14 +02:00
feb5713dc1 CI try #2 2025-05-03 19:01:24 +02:00
77bdd61c91 Set up android CI 2025-05-03 18:27:44 +02:00
16 changed files with 1272 additions and 1268 deletions

View file

@ -18,6 +18,11 @@ jobs:
- name: Check out repo - name: Check out repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Update packages
run: |
sudo apt update
sudo apt upgrade
- name: Set up pnpm - name: Set up pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
@ -29,9 +34,7 @@ jobs:
# https://github.com/tauri-apps/tauri-action?tab=readme-ov-file#usage # https://github.com/tauri-apps/tauri-action?tab=readme-ov-file#usage
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/customizing-github-hosted-runners#installing-software-on-ubuntu-runners # https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/customizing-github-hosted-runners#installing-software-on-ubuntu-runners
- name: Install tauri dependencies - name: Install tauri dependencies
run: | run: sudo apt install libwebkit2gtk-4.1-dev
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev
- name: Build - name: Build
run: ./meta/build run: ./meta/build
@ -69,12 +72,58 @@ jobs:
target/release/gdn-cli.exe target/release/gdn-cli.exe
target/release/gdn-app.exe target/release/gdn-app.exe
build-android:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Set up env vars
run: echo "NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV
- name: Update packages
run: |
sudo apt update
sudo apt upgrade
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10
# https://v2.tauri.app/start/prerequisites/#android
- name: Set up rust
run: |
rustup update
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
# https://v2.tauri.app/distribute/sign/android/
- name: Set up Android signing
run: |
cd gdn-app/src-tauri/gen/android
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > key.properties
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> key.properties
base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks
echo "storeFile=$RUNNER_TEMP/keystore.jks" >> key.properties
- name: Build
run: |
export NDK_HOME="$ANDROID_NDK_HOME"
./meta/build-android
- name: Upload
uses: actions/upload-artifact@v4
with:
name: gdn-android
path: target/release/gdn-app.apk
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/v') }} if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs: needs:
- build-linux - build-linux
- build-windows - build-windows
- build-android
permissions: permissions:
contents: write contents: write
steps: steps:
@ -85,6 +134,7 @@ jobs:
run: | run: |
zip -r gdn-linux.zip gdn-linux zip -r gdn-linux.zip gdn-linux
zip -r gdn-windows.zip gdn-windows zip -r gdn-windows.zip gdn-windows
mv gdn-android/gdn-app.apk gdn-android.apk
- name: Create new release - name: Create new release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@ -93,3 +143,4 @@ jobs:
files: | files: |
gdn-linux.zip gdn-linux.zip
gdn-windows.zip gdn-windows.zip
gdn-android.apk

580
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,17 +9,17 @@ edition = "2024"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
clap = { version = "4.5.38", features = ["derive", "deprecated"] } clap = { version = "4.5.37", features = ["derive", "deprecated"] }
directories = "6.0.0" directories = "6.0.0"
gdn = { path = "gdn" } gdn = { path = "gdn" }
git2 = { version = "0.20.2", features = ["vendored-libgit2", "vendored-openssl"] } git2 = { version = "0.20.1", features = ["vendored-libgit2", "vendored-openssl"] }
jiff = "0.2.14" jiff = "0.2.11"
rand = "0.9.1" rand = "0.9.1"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
tauri = { version = "2.5.1", features = [] } tauri = { version = "2.5.1", features = [] }
tauri-build = { version = "2.2.0", features = [] } tauri-build = { version = "2.2.0", features = [] }
tauri-plugin-opener = "2.2.7" tauri-plugin-opener = "2.2.6"
[workspace.lints] [workspace.lints]
rust.unsafe_code = { level = "forbid", priority = 1 } rust.unsafe_code = { level = "forbid", priority = 1 }

View file

@ -13,27 +13,27 @@
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.1.6", "@floating-ui/vue": "^1.1.6",
"@remixicon/vue": "^4.6.0", "@remixicon/vue": "^4.6.0",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.4",
"@tauri-apps/api": "^2.5.0", "@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-opener": "^2.2.7", "@tauri-apps/plugin-opener": "^2.2.6",
"@vueuse/core": "^12.8.2", "@vueuse/core": "^12.8.2",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.4",
"vue": "^3.5.15", "vue": "^3.5.13",
"zod": "^3.25.29" "zod": "^3.24.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.27.0", "@eslint/js": "^9.25.1",
"@tauri-apps/cli": "^2.5.0", "@tauri-apps/cli": "^2.5.0",
"@types/node": "^22.15.21", "@types/node": "^22.15.2",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.3",
"eslint": "^9.27.0", "eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.2",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.31.0",
"vite": "^6.3.5", "vite": "^6.3.3",
"vite-plugin-vue-devtools": "^7.7.6", "vite-plugin-vue-devtools": "^7.7.5",
"vue-tsc": "^2.2.10" "vue-tsc": "^2.2.10"
} }
} }

View file

@ -4,22 +4,18 @@ use gdn::ids::NoteId;
use tauri::{AppHandle, Emitter, State}; use tauri::{AppHandle, Emitter, State};
use crate::{ use crate::{
state::AppState, store::Store,
types::{EventNotesStoreUpdate, Note}, types::{EventNotesStoreUpdate, Note},
}; };
// API methods are sorted alphabetically. // API methods are sorted alphabetically.
fn update_if_required(state: &mut AppState, app: &AppHandle) { fn update_if_required(store: &mut Store, app: &AppHandle) {
let store_id = state.store.id(); if let Some(store_id) = store.needs_update() {
if state.store_last_id == Some(store_id) {
// No update necessary if the id hasn't changed
return;
}
let payload = EventNotesStoreUpdate { store_id }; let payload = EventNotesStoreUpdate { store_id };
app.emit("notes_store_update", payload).unwrap(); app.emit("notes_store_update", payload).unwrap();
state.store_last_id = Some(store_id) store.update();
}
} }
#[tauri::command] #[tauri::command]
@ -28,12 +24,10 @@ pub fn note_child_add(
child_id: NoteId, child_id: NoteId,
child_position: isize, child_position: isize,
app: AppHandle, app: AppHandle,
state: State<'_, Arc<Mutex<AppState>>>, store: State<'_, Arc<Mutex<Store>>>,
) { ) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard guard.add_child_at_position(id, child_id, child_position);
.store
.add_child_at_position(id, child_id, child_position);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
@ -45,12 +39,10 @@ pub fn note_child_move(
to_id: NoteId, to_id: NoteId,
to_position: isize, to_position: isize,
app: AppHandle, app: AppHandle,
state: State<'_, Arc<Mutex<AppState>>>, store: State<'_, Arc<Mutex<Store>>>,
) { ) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard guard.move_child_by_id_to_position(child_id, from_id, from_iteration, to_id, to_position);
.store
.move_child_by_id_to_position(child_id, from_id, from_iteration, to_id, to_position);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
@ -60,12 +52,10 @@ pub fn note_child_remove(
child_id: NoteId, child_id: NoteId,
child_iteration: usize, child_iteration: usize,
app: AppHandle, app: AppHandle,
state: State<'_, Arc<Mutex<AppState>>>, store: State<'_, Arc<Mutex<Store>>>,
) { ) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard guard.remove_child_by_id(id, child_id, child_iteration);
.store
.remove_child_by_id(id, child_id, child_iteration);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
@ -74,33 +64,32 @@ pub fn note_children_set(
id: NoteId, id: NoteId,
children: Vec<NoteId>, children: Vec<NoteId>,
app: AppHandle, app: AppHandle,
state: State<'_, Arc<Mutex<AppState>>>, store: State<'_, Arc<Mutex<Store>>>,
) { ) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard.store.set_children(id, children); guard.set_children(id, children);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
#[tauri::command] #[tauri::command]
pub fn note_create(text: String, app: AppHandle, state: State<'_, Arc<Mutex<AppState>>>) -> Note { pub fn note_create(text: String, app: AppHandle, store: State<'_, Arc<Mutex<Store>>>) -> Note {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
let id = guard.store.create(text); let id = guard.create(text);
let note = guard.store.get(id).unwrap().into();
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
note guard.get(id).unwrap()
} }
#[tauri::command] #[tauri::command]
pub fn note_delete(id: NoteId, app: AppHandle, state: State<'_, Arc<Mutex<AppState>>>) { pub fn note_delete(id: NoteId, app: AppHandle, store: State<'_, Arc<Mutex<Store>>>) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard.store.delete(id); guard.delete(id);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
#[tauri::command] #[tauri::command]
pub fn note_get(id: NoteId, state: State<'_, Arc<Mutex<AppState>>>) -> Option<Note> { pub fn note_get(id: NoteId, store: State<'_, Arc<Mutex<Store>>>) -> Option<Note> {
let guard = state.lock().unwrap(); let guard = store.lock().unwrap();
guard.store.get(id).map(|it| it.into()) guard.get(id)
} }
#[tauri::command] #[tauri::command]
@ -108,16 +97,16 @@ pub fn note_text_set(
id: NoteId, id: NoteId,
text: String, text: String,
app: AppHandle, app: AppHandle,
state: State<'_, Arc<Mutex<AppState>>>, store: State<'_, Arc<Mutex<Store>>>,
) { ) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard.store.set_text(id, text); guard.set_text(id, text);
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }
#[tauri::command] #[tauri::command]
pub fn notes_clear(app: AppHandle, state: State<'_, Arc<Mutex<AppState>>>) { pub fn notes_clear(app: AppHandle, store: State<'_, Arc<Mutex<Store>>>) {
let mut guard = state.lock().unwrap(); let mut guard = store.lock().unwrap();
guard.store.clear(); guard.clear();
update_if_required(&mut guard, &app); update_if_required(&mut guard, &app);
} }

View file

@ -1,21 +1,16 @@
use serde_json as _; // Silence unused dependency warning
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::state::AppState; use store::Store;
mod api; mod api;
mod state; pub mod store;
mod types; mod types;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let state = Arc::new(Mutex::new(AppState::new()));
// TODO Launch store loading task
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.manage(state) .manage(Arc::new(Mutex::new(Store::new())))
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
api::note_child_add, api::note_child_add,
api::note_child_move, api::note_child_move,

View file

@ -1,15 +0,0 @@
use gdn::store::Store;
pub struct AppState {
pub store: Store,
pub store_last_id: Option<u64>,
}
impl AppState {
pub fn new() -> Self {
Self {
store: Store::new(),
store_last_id: None,
}
}
}

View file

@ -1,45 +1,21 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::{ use gdn::ids::NoteId;
ids::NoteId,
repo::{Note, Repo},
};
#[derive(Clone)] use crate::types::Note;
pub struct RawNote {
#[derive(Debug)]
pub struct NoteInfo {
pub text: String, pub text: String,
pub children: Vec<NoteId>, pub children: Vec<NoteId>,
} }
impl RawNote { /// A note store for testing.
pub fn load(note: Note) -> Self {
Self {
text: note.text,
children: note.children,
}
}
pub fn save(self, id: NoteId) -> Note {
Note {
id,
text: self.text,
children: self.children,
}
}
}
#[derive(Clone)]
pub struct RichNote {
pub id: NoteId,
pub text: String,
pub children: Vec<NoteId>,
pub parents: HashSet<NoteId>,
}
#[derive(Default)] #[derive(Default)]
pub struct Store { pub struct Store {
id: u64, last_id: u64,
notes: HashMap<NoteId, RawNote>, curr_id: u64,
notes: HashMap<NoteId, NoteInfo>,
parents: HashMap<NoteId, HashMap<NoteId, usize>>, parents: HashMap<NoteId, HashMap<NoteId, usize>>,
} }
@ -48,36 +24,19 @@ impl Store {
Self::default() Self::default()
} }
pub fn load(repo: Repo) -> Self { pub fn needs_update(&self) -> Option<u64> {
let notes = repo if self.last_id != self.curr_id {
.notes Some(self.curr_id)
.into_iter() } else {
.map(|note| (note.id, RawNote::load(note))) None
.collect::<HashMap<_, _>>(); }
let mut result = Self {
notes,
..Self::default()
};
result.make_consistent_and_tick();
result
} }
pub fn save(&self) -> Repo { pub fn update(&mut self) {
let notes = self self.last_id = self.curr_id;
.notes
.iter()
.map(|(id, note)| note.clone().save(*id))
.collect::<Vec<_>>();
Repo { notes }
} }
pub fn id(&self) -> u64 { pub fn get(&self, id: NoteId) -> Option<Note> {
self.id
}
pub fn get(&self, id: NoteId) -> Option<RichNote> {
let info = self.notes.get(&id)?; let info = self.notes.get(&id)?;
let parents = self let parents = self
@ -86,7 +45,7 @@ impl Store {
.map(|ps| ps.keys().copied().collect::<HashSet<_>>()) .map(|ps| ps.keys().copied().collect::<HashSet<_>>())
.unwrap_or_default(); .unwrap_or_default();
Some(RichNote { Some(Note {
id, id,
text: info.text.clone(), text: info.text.clone(),
children: info.children.clone(), children: info.children.clone(),
@ -95,7 +54,7 @@ impl Store {
} }
fn tick(&mut self) { fn tick(&mut self) {
self.id += 1; self.curr_id += 1;
} }
fn make_consistent_and_tick(&mut self) { fn make_consistent_and_tick(&mut self) {
@ -123,18 +82,18 @@ impl Store {
pub fn create(&mut self, text: String) -> NoteId { pub fn create(&mut self, text: String) -> NoteId {
let id = NoteId::new(); let id = NoteId::new();
let note = RawNote { let info = NoteInfo {
text, text,
children: vec![], children: vec![],
}; };
self.notes.insert(id, note); self.notes.insert(id, info);
self.make_consistent_and_tick(); self.make_consistent_and_tick();
id id
} }
pub fn delete(&mut self, id: NoteId) -> Option<RawNote> { pub fn delete(&mut self, id: NoteId) -> Option<NoteInfo> {
let info = self.notes.remove(&id)?; let info = self.notes.remove(&id)?;
self.make_consistent_and_tick(); self.make_consistent_and_tick();
Some(info) Some(info)

View file

@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use gdn::{ids::NoteId, store::RichNote}; use gdn::ids::NoteId;
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@ -12,17 +12,6 @@ pub struct Note {
pub parents: HashSet<NoteId>, pub parents: HashSet<NoteId>,
} }
impl From<RichNote> for Note {
fn from(value: RichNote) -> Self {
Self {
id: value.id,
text: value.text,
children: value.children,
parents: value.parents,
}
}
}
//////////// ////////////
// Events // // Events //
//////////// ////////////

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="flex h-7 w-7 items-center justify-center rounded-md bg-neutral-800 hover:bg-neutral-700 active:bg-neutral-500" class="flex items-center rounded-md bg-neutral-800 px-2 hover:bg-neutral-700 active:bg-neutral-500"
> >
<slot></slot> <slot></slot>
</div> </div>

View file

@ -1,4 +1,4 @@
import { z } from "zod/v4"; import { z } from "zod";
export type NodeId = z.infer<typeof NodeId>; export type NodeId = z.infer<typeof NodeId>;
export const NodeId = z.string().startsWith("n").length(17); export const NodeId = z.string().startsWith("n").length(17);

View file

@ -1,7 +1,6 @@
pub mod data; pub mod data;
pub mod ids; pub mod ids;
pub mod repo; pub mod repo;
pub mod store;
pub const PROPER_NAME: &str = "GedächtNAS"; pub const PROPER_NAME: &str = "GedächtNAS";
pub const TECHNICAL_NAME: &str = "gedaechtnas"; pub const TECHNICAL_NAME: &str = "gedaechtnas";

View file

@ -86,7 +86,6 @@ pub fn save(path: &Path, repo: Repo) -> anyhow::Result<Oid> {
let message = Zoned::now().to_string(); let message = Zoned::now().to_string();
// TODO Check that the repo is actually based on this commit. // TODO Check that the repo is actually based on this commit.
// TODO Check if there actually is a difference to the parent commit
let parent = match read_head(&repository)? { let parent = match read_head(&repository)? {
None => None, None => None,
Some(parent) => Some(parent.peel_to_commit()?), Some(parent) => Some(parent.peel_to_commit()?),

View file

@ -6,6 +6,11 @@ takes a short time to implement and represents an incremental improvement.
Once an idea has been implemented, it should be deleted from this file. Once an idea has been implemented, it should be deleted from this file.
- cli: `repo list` command
- cli: `repo add` command
- cli: `repo rename` command
- cli: `repo remove` command
- cli: Single-letter aliases for commands
- Logging (`log` or `tracing`?) - Logging (`log` or `tracing`?)
- app: Make log available in UI, for debugging and testing - app: Make log available in UI, for debugging and testing
- app: Create repo from dropdown - app: Create repo from dropdown

1650
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
# https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md#rust-tools
channel = "1.86.0"