Skip to content

Commit 24b5754

Browse files
committed
First basic image and caption viewing
1 parent c991f47 commit 24b5754

File tree

12 files changed

+427
-22
lines changed

12 files changed

+427
-22
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@sveltejs/adapter-node": "^1.3.1",
1919
"@sveltejs/adapter-static": "^2.0.3",
2020
"@sveltejs/kit": "^1.5.0",
21+
"@tailwindcss/forms": "^0.5.4",
2122
"@tailwindcss/typography": "^0.5.9",
2223
"@tauri-apps/cli": "^1.4.0",
2324
"@typescript-eslint/eslint-plugin": "^5.45.0",
@@ -39,6 +40,7 @@
3940
},
4041
"type": "module",
4142
"dependencies": {
43+
"@svelte-put/dragscroll": "^3.0.0",
4244
"@tauri-apps/api": "^1.4.0"
4345
}
4446
}

pnpm-lock.yaml

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tokio = { version = "1", features = ["full"] }
2424
actix-web = "4.3.1"
2525
actix-files = "0.6.2"
2626
once_cell = "1.18.0"
27+
actix-cors = "0.6.4"
2728

2829
[features]
2930
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

src-tauri/src/main.rs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static PORT: Lazy<Arc<RwLock<Option<u16>>>> = Lazy::new(|| Arc::new(RwLock::new(
2424

2525
fn main() {
2626
#[cfg(debug_assertions)]
27-
ts::export(collect_types![select_folder], "../src/lib/bindings.ts").unwrap();
27+
ts::export(collect_types![select_folder, get_served_dir, get_available_files], "../src/lib/bindings.ts").unwrap();
2828

2929
// Run the server as long as tauri runs
3030

@@ -38,7 +38,7 @@ fn main() {
3838
});
3939
Ok(())
4040
})
41-
.invoke_handler(tauri::generate_handler![select_folder])
41+
.invoke_handler(tauri::generate_handler![select_folder, get_served_dir, get_available_files])
4242
.run(tauri::generate_context!())
4343
.expect("error while running tauri application");
4444
}
@@ -54,11 +54,88 @@ async fn select_folder() -> Result<String, String> {
5454
let dialog_result: Option<PathBuf> = FileDialogBuilder::new().pick_folder();
5555

5656
// Return the path as a string if the user selected a folder
57-
match dialog_result {
57+
let path = match dialog_result {
5858
Some(path) => match path.to_str() {
5959
Some(path_str) => Ok(path_str.to_string()),
6060
None => Err("Error converting path to string".to_string()),
6161
},
6262
None => Err("No folder selected".to_string()),
63+
};
64+
65+
// Save the path to the global variable
66+
if let Ok(path) = &path {
67+
68+
let mut served_dir_write = SERVED_DIR.write().unwrap();
69+
*served_dir_write = path.clone();
70+
71+
// Now remove the lock
72+
drop(served_dir_write);
73+
74+
// Print out the port number and the currently served absolute path to the console
75+
println!(
76+
"Serving files from {} on port {}",
77+
SERVED_DIR.read().unwrap(),
78+
PORT.read().unwrap().unwrap()
79+
);
80+
} else {
81+
println!("No folder selected");
6382
}
83+
84+
path
85+
}
86+
87+
88+
// Expose a command to return the currently served directory, and the port number
89+
#[tauri::command]
90+
#[specta::specta]
91+
async fn get_served_dir() -> Result<(String, u16), String> {
92+
let port = PORT.read().unwrap().unwrap();
93+
let served_dir = SERVED_DIR.read().unwrap().clone();
94+
95+
Ok((served_dir, port))
96+
}
97+
98+
// Expose a command to obtain all files available in the currently served directory
99+
// Directory traversal is recursive, with a maximum file count of 10000
100+
#[tauri::command]
101+
#[specta::specta]
102+
async fn get_available_files() -> Result<Vec<String>, String> {
103+
use std::fs;
104+
use std::path::PathBuf;
105+
106+
let served_dir = SERVED_DIR.read().unwrap().clone();
107+
108+
let mut files: Vec<String> = Vec::new();
109+
110+
// Recursively traverse the directory, and add all files to the vector
111+
let mut dir_queue: Vec<PathBuf> = Vec::new();
112+
dir_queue.push(PathBuf::from(served_dir.clone()));
113+
114+
while let Some(dir) = dir_queue.pop() {
115+
if let Ok(entries) = fs::read_dir(dir) {
116+
for entry in entries {
117+
if let Ok(entry) = entry {
118+
let path = entry.path();
119+
if path.is_dir() {
120+
dir_queue.push(path);
121+
} else if let Some(path_str) = path.to_str() {
122+
123+
// Only store the part of the path that is relative to the served directory
124+
let path_str = path_str.replace(&served_dir, "");
125+
// Also remove the leading slash (can also be a backslash on Windows)
126+
let path_str = path_str.trim_start_matches('/').trim_start_matches('\\');
127+
128+
files.push(path_str.to_string());
129+
130+
// Limit the file count to 10000
131+
if files.len() > 10000 {
132+
return Ok(files);
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
Ok(files)
64141
}

src-tauri/src/server/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use tauri::AppHandle;
55
use actix_files::NamedFile;
66
use actix_web::{web, App, HttpServer, Result, middleware};
77
use std::path::PathBuf;
8+
use actix_cors::Cors;
89
use super::PORT;
910
use super::SERVED_DIR;
1011

@@ -20,9 +21,16 @@ pub async fn init(app: AppHandle) -> std::io::Result<()> {
2021

2122
// Create server, bind it to any available port
2223
let server = HttpServer::new(move || {
24+
25+
let cors = Cors::default().allow_any_origin().send_wildcard();
26+
27+
// Additionally just return 200 if checkAlive as route is called
28+
2329
App::new()
2430
.app_data(tauri_app.clone())
31+
.wrap(cors)
2532
.wrap(middleware::Logger::default())
33+
.route("/checkAlive", web::get().to(|| async { "OK" }))
2634
.service(web::resource("/{filename:.*}").route(web::get().to(serve_file)))
2735
})
2836
.bind("127.0.0.1:0")?;
@@ -56,3 +64,4 @@ async fn serve_file(path: web::Path<(String,)>) -> Result<NamedFile> {
5664

5765
Ok(NamedFile::open(file_path)?)
5866
}
67+

src-tauri/tauri.conf.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@
6464
"windows": [
6565
{
6666
"fullscreen": false,
67-
"height": 600,
67+
"height": 900,
6868
"resizable": true,
6969
"title": "image-set-tag-editor",
70-
"width": 800
70+
"width": 1600
7171
}
7272
]
7373
}

src/lib/bindings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ export function selectFolder() {
1414
return invoke()<string>("select_folder")
1515
}
1616

17+
export function getServedDir() {
18+
return invoke()<[string, number]>("get_served_dir")
19+
}
20+
21+
export function getAvailableFiles() {
22+
return invoke()<string[]>("get_available_files")
23+
}
24+
1725

src/lib/stores.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { writable, derived, get } from 'svelte/store';
2+
3+
export const working_folder = writable('');
4+
export const file_server_port = writable(0);
5+
export const available_files = writable<string[]>([]);
6+
7+
const VALID_IMAGE_FORMATS = ['.jpg', '.png', '.gif', '.bmp', '.webp', '.jpeg', '.avif']
8+
9+
// Have a derived store, which combines all available files into an image file, and a corresponding caption file (the first non image file with the same name)
10+
export const available_images = writable<{image: string, caption: string, caption_file: string, viewed: boolean}[]>([]);
11+
export const is_loading_image_data = writable(false);
12+
13+
const update_available_files = async () => {
14+
const image_map = new Map<string, {image: string, caption: string, caption_file: string, viewed: boolean}>();
15+
16+
17+
const unprocessed_files = [];
18+
19+
for (const file of get(available_files)) {
20+
// Get the file without the extension
21+
const [file_name, file_extension] = file.split('.');
22+
if (VALID_IMAGE_FORMATS.includes(`.${file_extension.toLocaleLowerCase()}`)) {
23+
image_map.set(file_name, {
24+
image: file,
25+
caption: '',
26+
caption_file: '',
27+
viewed: false,
28+
});
29+
} else {
30+
unprocessed_files.push(file);
31+
}
32+
};
33+
34+
for (const file of unprocessed_files) {
35+
const [file_name, file_extension] = file.split('.');
36+
const image = image_map.get(file_name);
37+
if (image && image.caption === '') {
38+
image.caption_file = file;
39+
40+
// Fetch the text caption from our file server
41+
image.caption = await fetch(`http://localhost:${get(file_server_port)}/${file}`).then((response) => {
42+
return response.text();
43+
}).catch((error) => {
44+
console.error(error);
45+
}) || '';
46+
47+
}
48+
}
49+
50+
return Array.from(image_map.values());
51+
}
52+
53+
54+
// But we need to manually, asynchronously update the available images by subscribing to the available files store
55+
available_files.subscribe(async (files) => {
56+
is_loading_image_data.set(true);
57+
available_images.set(await update_available_files());
58+
is_loading_image_data.set(false);
59+
});
60+
61+

0 commit comments

Comments
 (0)