diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aaed5aa..95bba71 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -50,12 +50,37 @@ jobs:
- name: Generate icons
run: npm run gen-icons
+ - name: Import Apple Developer Certificate
+ env:
+ APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ run: |
+ echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
+ security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
+ security set-keychain-settings -t 3600 -u build.keychain
+ security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
+ security find-identity -v -p codesigning build.keychain
+
+ - name: Verify Certificate
+ run: |
+ CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
+ CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
+ echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
+ echo "Certificate imported."
+
- name: Build & publish (Tauri)
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+ APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
tagName: v__VERSION__
releaseName: DTM v__VERSION__
diff --git a/README.md b/README.md
index db6b016..f7b23df 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,19 @@
-# DTM
\ No newline at end of file
+# DTM
+
+## Building
+
+To build the app on Mac, you will need to have [Node/NPM](https://nodejs.org/en/download), and [Rust](https://www.rust-lang.org/tools/install) installed, as well as the Xcode command line tools (`xcode-select --install`)
+
+```bash
+npm install
+npm run gen:icons
+
+# Build the app for current architecture
+npm run build:mac
+
+# Build for Mac Universal
+npm run build:universal
+
+# Run in dev mode
+npm run dev
+```
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 195b204..2d1b128 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "dtm",
- "version": "0.2.1",
+ "version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dtm",
- "version": "0.2.1",
+ "version": "0.3.2",
"dependencies": {
"@chakra-ui/react": "^3.30.0",
"@emotion/react": "^11.14.0",
diff --git a/package.json b/package.json
index c52410b..0e63efb 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "dtm",
"author": "kcjerrell",
"private": true,
- "version": "0.3.2",
+ "version": "0.3.3",
"type": "module",
"scripts": {
"dev": "tauri dev",
diff --git a/public/img_not_available.svg b/public/img_not_available.svg
new file mode 100644
index 0000000..6e03d34
--- /dev/null
+++ b/public/img_not_available.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/src-tauri/.cargo/config.toml b/src-tauri/.cargo/config.toml
new file mode 100644
index 0000000..d500958
--- /dev/null
+++ b/src-tauri/.cargo/config.toml
@@ -0,0 +1,2 @@
+[env]
+RUST_TEST_THREADS = "1"
\ No newline at end of file
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index a78be52..00ea6e3 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1624,12 +1624,14 @@ name = "dtm"
version = "0.3.2"
dependencies = [
"anyhow",
+ "async-trait",
"base64 0.22.1",
"bytemuck",
"byteorder",
"bytes",
"cc",
"chrono",
+ "dashmap",
"dtm_macros",
"entity",
"flatbuffers",
@@ -1643,7 +1645,7 @@ dependencies = [
"log",
"migration",
"mime",
- "moka",
+ "notify-debouncer-mini",
"num_enum",
"objc2 0.6.3",
"objc2-app-kit 0.3.2",
@@ -1674,10 +1676,12 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-valtio",
"tauri-plugin-window-state",
+ "tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"unicode-normalization",
+ "walkdir",
"web-image-meta",
]
@@ -3582,26 +3586,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "moka"
-version = "0.12.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a"
-dependencies = [
- "async-lock 3.4.2",
- "crossbeam-channel",
- "crossbeam-epoch",
- "crossbeam-utils",
- "equivalent",
- "event-listener 5.4.1",
- "futures-util",
- "parking_lot",
- "portable-atomic",
- "smallvec",
- "tagptr",
- "uuid",
-]
-
[[package]]
name = "moxcms"
version = "0.7.11"
@@ -3767,6 +3751,18 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "notify-debouncer-mini"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17849edfaabd9a5fef1c606d99cfc615a8e99f7ac4366406d86c7942a3184cf2"
+dependencies = [
+ "log",
+ "notify",
+ "notify-types",
+ "tempfile",
+]
+
[[package]]
name = "notify-types"
version = "2.0.0"
@@ -4719,12 +4715,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "portable-atomic"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
-
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -6548,12 +6538,6 @@ dependencies = [
"version-compare",
]
-[[package]]
-name = "tagptr"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
-
[[package]]
name = "tao"
version = "0.34.5"
@@ -7144,9 +7128,9 @@ dependencies = [
[[package]]
name = "tempfile"
-version = "3.24.0"
+version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand 2.3.0",
"getrandom 0.3.4",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 7b55df1..dfcc2ec 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -3,7 +3,7 @@ members = ["migration", "entity", ".", "fpzip-sys", "macros"]
[package]
name = "dtm"
-version = "0.3.2"
+version = "0.3.3"
description = "A little app for reading Draw Things Metadata"
authors = ["kcjerrell"]
edition = "2021"
@@ -58,7 +58,6 @@ futures = "0.3.28"
tokio = { version = "1.48.0", features = ["full"] }
mime = "0.3.17"
once_cell = "1.21.3"
-moka = { version = "0.12.11", features = ["future"] }
bytes = "1.10.1"
byteorder = "1.5.0"
half = "2"
@@ -79,6 +78,10 @@ sevenz-rust = "0.6.1"
sha2 = "0.10.9"
futures-util = "0.3.31"
regex = "1.12.2"
+walkdir = "2.5.0"
+async-trait = "0.1.89"
+notify-debouncer-mini = { version = "0.7.0", features = ["macos_fsevent"] }
+dashmap = "6.1.0"
# macOS-only
[target."cfg(target_os = \"macos\")".dependencies]
@@ -91,3 +94,6 @@ tauri-plugin-nspopover = { git = "https://github.com/freethinkel/tauri-nspopover
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
+
+[dev-dependencies]
+tempfile = "3.25.0"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 6a2a45a..dc95e47 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -31,28 +31,46 @@
"fs:allow-home-read-recursive",
"fs:allow-watch",
"fs:allow-unwatch",
+ {
+ "identifier": "fs:allow-watch",
+ "allow": [
+ "$HOME/**",
+ "/Volumes/**"
+ ]
+ },
{
"identifier": "fs:allow-exists",
"allow": [
- "$HOME/Library/Containers/com.liuliu.draw-things/Data/*"
+ "$HOME/**",
+ "/Volumes/**"
]
},
{
"identifier": "fs:allow-read-dir",
"allow": [
- "$HOME/Library/Containers/com.liuliu.draw-things/Data/Documents/*"
+ "$HOME/**",
+ "/Volumes/**"
]
},
{
"identifier": "fs:allow-read-file",
"allow": [
- "$HOME/Library/Containers/com.liuliu.draw-things/Data/Documents/*"
+ "$HOME/**",
+ "/Volumes/**"
]
},
{
"identifier": "fs:allow-read",
"allow": [
- "$HOME/Library/Containers/com.liuliu.draw-things/Data/Documents/*"
+ "$HOME/**",
+ "/Volumes/**"
+ ]
+ },
+ {
+ "identifier": "fs:allow-stat",
+ "allow": [
+ "$HOME/**",
+ "/Volumes/**"
]
},
"http:default",
diff --git a/src-tauri/entity/Cargo.toml b/src-tauri/entity/Cargo.toml
index ec7c2b7..703224a 100644
--- a/src-tauri/entity/Cargo.toml
+++ b/src-tauri/entity/Cargo.toml
@@ -10,6 +10,6 @@ path = "src/mod.rs"
[dependencies]
sea-orm = { version = "2.0.0-rc" }
-serde = "1.0.228"
+serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
num_enum = "0.7.5"
diff --git a/src-tauri/entity/src/projects.rs b/src-tauri/entity/src/projects.rs
index 7b64f1b..f02a49d 100644
--- a/src-tauri/entity/src/projects.rs
+++ b/src-tauri/entity/src/projects.rs
@@ -4,18 +4,25 @@ use sea_orm::entity::prelude::*;
use serde::Serialize;
#[sea_orm::model]
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub fingerprint: String,
- #[sea_orm(unique)]
pub path: String,
+ pub watchfolder_id: i64,
pub filesize: Option,
pub modified: Option,
- pub missing_on: Option,
pub excluded: bool,
+ #[sea_orm(
+ belongs_to,
+ from = "watchfolder_id",
+ to = "id",
+ on_update = "NoAction",
+ on_delete = "Cascade"
+ )]
+ pub watchfolder: HasOne,
#[sea_orm(has_many)]
pub images: HasMany,
}
diff --git a/src-tauri/entity/src/watch_folders.rs b/src-tauri/entity/src/watch_folders.rs
index 8a1b456..1183759 100644
--- a/src-tauri/entity/src/watch_folders.rs
+++ b/src-tauri/entity/src/watch_folders.rs
@@ -4,14 +4,18 @@ use sea_orm::entity::prelude::*;
use serde::Serialize;
#[sea_orm::model]
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "watch_folders")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub path: String,
+ pub bookmark: String,
pub recursive: Option,
- pub last_updated: Option,
+ pub is_missing: bool,
+ pub is_locked: bool,
+ #[sea_orm(has_many)]
+ pub projects: HasMany,
}
impl ActiveModelBehavior for ActiveModel {}
diff --git a/src-tauri/macros/src/lib.rs b/src-tauri/macros/src/lib.rs
index 13237da..628f2f8 100644
--- a/src-tauri/macros/src/lib.rs
+++ b/src-tauri/macros/src/lib.rs
@@ -1,9 +1,7 @@
use proc_macro::TokenStream;
-use quote::quote;
+use quote::{format_ident, quote};
use syn::{
- parse::{Parse, ParseStream},
- parse_macro_input, Expr, FnArg, GenericArgument, ItemFn, Pat, PathArguments, ReturnType, Token,
- Type,
+ Expr, FnArg, GenericArgument, ItemFn, Pat, PathArguments, ReturnType, Token, Type, parse::{Parse, ParseStream}, parse_macro_input
};
struct DtmArgs {
@@ -180,3 +178,121 @@ pub fn dtm_command(args: TokenStream, input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
+
+#[proc_macro_attribute]
+pub fn dtp_command(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ // This is now a marker macro when used inside #[dtp_commands]
+ // If used alone, it will still try to generate, but will fail if inside an impl.
+ // We'll keep the logic but allow it to be stripped by dtp_commands.
+ item
+}
+
+#[proc_macro_attribute]
+pub fn dtp_commands(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input = parse_macro_input!(item as syn::ItemImpl);
+ let self_ty = &input.self_ty;
+
+ let mut generated_commands = Vec::new();
+
+ for item in &mut input.items {
+ if let syn::ImplItem::Fn(method) = item {
+ let mut has_dtp_command = false;
+ let mut dtp_command_idx = None;
+
+ for (i, attr) in method.attrs.iter().enumerate() {
+ if attr.path().is_ident("dtp_command") {
+ has_dtp_command = true;
+ dtp_command_idx = Some(i);
+ break;
+ }
+ }
+
+ if has_dtp_command {
+ // Remove the dtp_command attribute from the method
+ if let Some(idx) = dtp_command_idx {
+ method.attrs.remove(idx);
+ }
+
+ let vis = &method.vis;
+ let sig = &method.sig;
+ let fn_name = &sig.ident;
+
+ // Ensure async
+ if sig.asyncness.is_none() {
+ return syn::Error::new_spanned(
+ sig.fn_token,
+ "dtp_command functions must be async",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract args
+ let mut inputs = sig.inputs.iter();
+
+ // Ensure first arg is &self
+ let first = inputs.next();
+ match first {
+ Some(FnArg::Receiver(_)) => {}
+ _ => {
+ return syn::Error::new_spanned(
+ sig,
+ "dtp_command requires &self as first parameter",
+ )
+ .to_compile_error()
+ .into();
+ }
+ }
+
+ // Collect remaining args for wrapper
+ let mut wrapper_args = Vec::new();
+ let mut forward_args = Vec::new();
+
+ for arg in inputs {
+ if let FnArg::Typed(pat_type) = arg {
+ wrapper_args.push(pat_type.clone());
+
+ // extract argument name for forwarding
+ if let Pat::Ident(pat_ident) = &*pat_type.pat {
+ forward_args.push(pat_ident.ident.clone());
+ }
+ }
+ }
+
+ let output = &sig.output;
+ let wrapper_name = format_ident!("dtp_{}", fn_name);
+ let command_name_str = wrapper_name.to_string();
+
+ generated_commands.push(quote! {
+ #[tauri::command]
+ #vis async fn #wrapper_name(
+ state: tauri::State<'_, #self_ty>,
+ #(#wrapper_args),*
+ ) #output {
+ log::debug!("DTPService command: {}", #command_name_str);
+
+ let result = state.inner().#fn_name(#(#forward_args),*).await;
+
+ if let Err(ref e) = result {
+ log::error!(
+ "DTPService command failed: {} ({})",
+ #command_name_str,
+ e
+ );
+ }
+
+ result
+ }
+ });
+ }
+ }
+ }
+
+ let expanded = quote! {
+ #input
+
+ #(#generated_commands)*
+ };
+
+ expanded.into()
+}
\ No newline at end of file
diff --git a/src-tauri/migration/src/m20220101_000001_create_table.rs b/src-tauri/migration/src/m20220101_000001_create_table.rs
index a8a52fb..da23f1c 100644
--- a/src-tauri/migration/src/m20220101_000001_create_table.rs
+++ b/src-tauri/migration/src/m20220101_000001_create_table.rs
@@ -6,25 +6,60 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
- // projects
+ // watchfolders
manager
.create_table(
Table::create()
- .table(Projects::Table)
+ .table(WatchFolders::Table)
.if_not_exists()
.col(
- ColumnDef::new(Projects::Id)
+ ColumnDef::new(WatchFolders::Id)
.integer()
.not_null()
.primary_key()
.auto_increment(),
)
+ .col(ColumnDef::new(WatchFolders::Path).string().not_null())
.col(
- ColumnDef::new(Projects::Path)
+ ColumnDef::new(WatchFolders::Bookmark)
.string()
+ .unique_key()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(WatchFolders::Recursive)
+ .boolean()
+ .default(false),
+ )
+ .col(
+ ColumnDef::new(WatchFolders::IsMissing)
+ .boolean()
+ .default(true),
+ )
+ .col(
+ ColumnDef::new(WatchFolders::IsLocked)
+ .boolean()
+ .default(false),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // projects
+ manager
+ .create_table(
+ Table::create()
+ .table(Projects::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(Projects::Id)
+ .integer()
.not_null()
- .unique_key(),
+ .primary_key()
+ .auto_increment(),
)
+ .col(ColumnDef::new(Projects::Path).string().not_null())
+ .col(ColumnDef::new(Projects::WatchfolderId).integer().not_null())
.col(ColumnDef::new(Projects::Filesize).big_integer().null())
.col(ColumnDef::new(Projects::Modified).big_integer().null())
.col(
@@ -39,7 +74,20 @@ impl MigrationTrait for Migration {
.not_null()
.default(""),
)
- .col(ColumnDef::new(Projects::MissingOn).big_integer().null())
+ .foreign_key(
+ ForeignKey::create()
+ .name("fk_projects_watchfolder")
+ .from(Projects::Table, Projects::WatchfolderId)
+ .to(WatchFolders::Table, WatchFolders::Id)
+ .on_delete(ForeignKeyAction::Cascade),
+ )
+ .index(
+ Index::create()
+ .name("idx_projects_path_watchfolder_id")
+ .col(Projects::Path)
+ .col(Projects::WatchfolderId)
+ .unique(),
+ )
.to_owned(),
)
.await?;
@@ -387,35 +435,6 @@ impl MigrationTrait for Migration {
)
.await?;
- // watchfolders
- manager
- .create_table(
- Table::create()
- .table(WatchFolders::Table)
- .if_not_exists()
- .col(
- ColumnDef::new(WatchFolders::Id)
- .integer()
- .not_null()
- .primary_key()
- .auto_increment(),
- )
- .col(
- ColumnDef::new(WatchFolders::Path)
- .string()
- .not_null()
- .unique_key(),
- )
- .col(
- ColumnDef::new(WatchFolders::Recursive)
- .boolean()
- .default(false),
- )
- .col(ColumnDef::new(WatchFolders::LastUpdated).integer().null())
- .to_owned(),
- )
- .await?;
-
manager
.get_connection()
.execute_unprepared(
@@ -475,7 +494,7 @@ enum Projects {
Modified,
Excluded,
Fingerprint,
- MissingOn,
+ WatchfolderId,
}
#[derive(Iden)]
@@ -565,5 +584,7 @@ enum WatchFolders {
Id,
Path,
Recursive,
- LastUpdated,
+ Bookmark,
+ IsMissing,
+ IsLocked,
}
diff --git a/src-tauri/src/bookmarks.rs b/src-tauri/src/bookmarks.rs
index f408ba6..f121e6d 100644
--- a/src-tauri/src/bookmarks.rs
+++ b/src-tauri/src/bookmarks.rs
@@ -1,128 +1,33 @@
-use tauri::command;
-
#[cfg(target_os = "macos")]
-mod ffi {
- use std::os::raw::c_char;
+mod bookmarks_mac;
+#[cfg(target_os = "macos")]
+pub use bookmarks_mac::*;
- extern "C" {
- pub fn open_dt_folder_picker(default_path: *const c_char) -> *mut c_char;
- pub fn free_string_ptr(ptr: *mut c_char);
- pub fn start_accessing_security_scoped_resource(bookmark: *const c_char) -> *mut c_char;
- pub fn stop_all_security_scoped_resources();
- pub fn stop_accessing_security_scoped_resource(bookmark: *const c_char);
- }
-}
+#[cfg(target_os = "linux")]
+mod bookmarks_linux;
+#[cfg(target_os = "linux")]
+pub use bookmarks_linux::*;
+
+// Also support other non-macos platforms as linux-like (simple paths)
+#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
+mod bookmarks_linux;
+#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
+pub use bookmarks_linux::*;
-#[derive(serde::Serialize)]
+
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct PickFolderResult {
pub path: String,
pub bookmark: String,
}
-#[command]
-pub async fn pick_draw_things_folder(
- default_path: Option,
-) -> Result