Skip to content

Commit daf3fdb

Browse files
committed
Add save button for all captions
1 parent 24b5754 commit daf3fdb

File tree

7 files changed

+345
-154
lines changed

7 files changed

+345
-154
lines changed

src-tauri/src/main.rs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
mod server;
55

6-
use std::thread;
76
use once_cell::sync::Lazy;
87
use specta::collect_types;
98
use std::env;
109
use std::sync::{Arc, RwLock};
10+
use std::thread;
1111
use tauri_specta::ts;
1212

1313
// Store where our files are served from
@@ -24,7 +24,16 @@ 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, get_served_dir, get_available_files], "../src/lib/bindings.ts").unwrap();
27+
ts::export(
28+
collect_types![
29+
select_folder,
30+
get_served_dir,
31+
get_available_files,
32+
save_captions
33+
],
34+
"../src/lib/bindings.ts",
35+
)
36+
.unwrap();
2837

2938
// Run the server as long as tauri runs
3039

@@ -38,7 +47,12 @@ fn main() {
3847
});
3948
Ok(())
4049
})
41-
.invoke_handler(tauri::generate_handler![select_folder, get_served_dir, get_available_files])
50+
.invoke_handler(tauri::generate_handler![
51+
select_folder,
52+
get_served_dir,
53+
get_available_files,
54+
save_captions
55+
])
4256
.run(tauri::generate_context!())
4357
.expect("error while running tauri application");
4458
}
@@ -64,7 +78,6 @@ async fn select_folder() -> Result<String, String> {
6478

6579
// Save the path to the global variable
6680
if let Ok(path) = &path {
67-
6881
let mut served_dir_write = SERVED_DIR.write().unwrap();
6982
*served_dir_write = path.clone();
7083

@@ -84,7 +97,6 @@ async fn select_folder() -> Result<String, String> {
8497
path
8598
}
8699

87-
88100
// Expose a command to return the currently served directory, and the port number
89101
#[tauri::command]
90102
#[specta::specta]
@@ -119,7 +131,6 @@ async fn get_available_files() -> Result<Vec<String>, String> {
119131
if path.is_dir() {
120132
dir_queue.push(path);
121133
} else if let Some(path_str) = path.to_str() {
122-
123134
// Only store the part of the path that is relative to the served directory
124135
let path_str = path_str.replace(&served_dir, "");
125136
// Also remove the leading slash (can also be a backslash on Windows)
@@ -139,3 +150,48 @@ async fn get_available_files() -> Result<Vec<String>, String> {
139150

140151
Ok(files)
141152
}
153+
154+
// Expose a command to save captions, it takes an array of a path to the caption file, and the new caption content
155+
// We first check if it is a valid path which is inside the served directory, and then write the new content to the file replacing the old content
156+
#[tauri::command]
157+
#[specta::specta]
158+
async fn save_captions(captions: Vec<(String, String)>) -> Result<(), String> {
159+
use std::fs;
160+
use std::io::Write;
161+
use std::path::PathBuf;
162+
163+
let served_dir = SERVED_DIR.read().unwrap().clone();
164+
165+
for caption in captions {
166+
let path = PathBuf::from(&served_dir).join(&caption.0);
167+
168+
// Check if the path is inside the served directory, if not return an error
169+
if !path.starts_with(&served_dir) {
170+
return Err("Invalid path".to_string());
171+
}
172+
173+
// Create the file if it doesn't exist yet
174+
if !path.exists() {
175+
match path.parent() {
176+
Some(parent) => {
177+
fs::create_dir_all(parent).unwrap();
178+
}
179+
None => {
180+
return Err("Invalid path".to_string());
181+
}
182+
}
183+
}
184+
185+
// Write the new caption content to the file
186+
match fs::File::create(&path) {
187+
Ok(mut file) => {
188+
file.write_all(caption.1.as_bytes()).unwrap();
189+
}
190+
Err(_) => {
191+
return Err("Error writing to file".to_string());
192+
}
193+
}
194+
}
195+
196+
Ok(())
197+
}

src-tauri/src/server/mod.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
21
use std::sync::Mutex;
32

4-
use tauri::AppHandle;
5-
use actix_files::NamedFile;
6-
use actix_web::{web, App, HttpServer, Result, middleware};
7-
use std::path::PathBuf;
8-
use actix_cors::Cors;
93
use super::PORT;
104
use super::SERVED_DIR;
5+
use actix_cors::Cors;
6+
use actix_files::NamedFile;
7+
use actix_web::{middleware, web, App, HttpServer, Result};
8+
use std::path::PathBuf;
9+
use tauri::AppHandle;
1110

1211
struct TauriAppState {
1312
app: Mutex<AppHandle>,
@@ -21,7 +20,6 @@ pub async fn init(app: AppHandle) -> std::io::Result<()> {
2120

2221
// Create server, bind it to any available port
2322
let server = HttpServer::new(move || {
24-
2523
let cors = Cors::default().allow_any_origin().send_wildcard();
2624

2725
// Additionally just return 200 if checkAlive as route is called
@@ -52,7 +50,6 @@ pub async fn init(app: AppHandle) -> std::io::Result<()> {
5250
server.run().await
5351
}
5452

55-
5653
async fn serve_file(path: web::Path<(String,)>) -> Result<NamedFile> {
5754
let directory = SERVED_DIR.read().unwrap();
5855
let file_path = PathBuf::from(&*directory).join(&path.0);
@@ -64,4 +61,3 @@ async fn serve_file(path: web::Path<(String,)>) -> Result<NamedFile> {
6461

6562
Ok(NamedFile::open(file_path)?)
6663
}
67-

src/app.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ declare namespace App {
77
// interface Error {}
88
// interface Platform {}
99
}
10+
11+
import 'unplugin-icons/types/svelte';

src/lib/bindings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ export function getAvailableFiles() {
2222
return invoke()<string[]>("get_available_files")
2323
}
2424

25+
export function saveCaptions(captions: ([string, string])[]) {
26+
return invoke()<null>("save_captions", { captions })
27+
}
28+
2529

src/lib/stores.ts

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,96 @@ export const working_folder = writable('');
44
export const file_server_port = writable(0);
55
export const available_files = writable<string[]>([]);
66

7-
const VALID_IMAGE_FORMATS = ['.jpg', '.png', '.gif', '.bmp', '.webp', '.jpeg', '.avif']
7+
const VALID_IMAGE_FORMATS = ['.jpg', '.png', '.gif', '.bmp', '.webp', '.jpeg', '.avif'];
88

99
// 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}[]>([]);
10+
export const available_images = writable<
11+
{ image: string; caption: string; caption_file: string; viewed: boolean }[]
12+
>([]);
1113
export const is_loading_image_data = writable(false);
1214

1315
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-
}
16+
const image_map = new Map<
17+
string,
18+
{ image: string; caption: string; caption_file: string; viewed: boolean }
19+
>();
5220

21+
const unprocessed_files = [];
22+
23+
for (const file of get(available_files)) {
24+
// Get the file without the extension
25+
const [file_name, file_extension] = file.split('.');
26+
if (file_extension && VALID_IMAGE_FORMATS.includes(`.${file_extension.toLowerCase()}`)) {
27+
image_map.set(file_name, {
28+
image: file,
29+
caption: '',
30+
caption_file: '',
31+
viewed: false
32+
});
33+
} else {
34+
unprocessed_files.push(file);
35+
}
36+
}
37+
38+
const caption_counts: Map<string, number> = new Map();
39+
40+
for (const file of unprocessed_files) {
41+
const [file_name, file_extension] = file.split('.');
42+
43+
// If a file extension exists
44+
if (file_extension !== undefined) {
45+
// Count how often we see the different caption extensions
46+
if (caption_counts.has(file_extension.toLowerCase())) {
47+
caption_counts.set(
48+
file_extension.toLocaleLowerCase(),
49+
caption_counts.get(file_extension.toLowerCase())! + 1
50+
);
51+
} else {
52+
caption_counts.set(file_extension.toLowerCase(), 1);
53+
}
54+
}
55+
56+
const image = image_map.get(file_name);
57+
if (image && image.caption === '') {
58+
image.caption_file = file;
59+
60+
// Fetch the text caption from our file server, disable caching
61+
image.caption =
62+
(await fetch(`http://localhost:${get(file_server_port)}/${file}`, { cache: 'no-store' })
63+
.then((response) => {
64+
return response.text();
65+
})
66+
.catch((error) => {
67+
console.error(error);
68+
})) || '';
69+
}
70+
}
71+
72+
let final_caption_extension = 'txt';
73+
74+
if (caption_counts.size > 0) {
75+
// Find the most common caption extension
76+
const most_common_caption_extensions = [...caption_counts.entries()].reduce((a, e) =>
77+
e[1] > a[1] ? e : a
78+
);
79+
if (most_common_caption_extensions.length > 0) {
80+
[final_caption_extension] = most_common_caption_extensions;
81+
}
82+
}
83+
84+
// Iterate all images, and set the caption file if it is not already set (using the same name as the image, but with the most common caption extension)
85+
for (const image of image_map.values()) {
86+
if (image.caption_file === '') {
87+
image.caption_file = `${image.image.split('.')[0]}.${final_caption_extension}`;
88+
}
89+
}
90+
91+
return Array.from(image_map.values());
92+
};
5393

5494
// But we need to manually, asynchronously update the available images by subscribing to the available files store
5595
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);
96+
is_loading_image_data.set(true);
97+
available_images.set(await update_available_files());
98+
is_loading_image_data.set(false);
5999
});
60-
61-

0 commit comments

Comments
 (0)