[0.1.23] Export a capture's original to a folder: [export] folder lines, a dropdown or Custom...

This commit is contained in:
Musa Mahmood 2026-10-15 17:49:03 -04:00
parent c5ef13f146
commit c351ca92c0
6 changed files with 228 additions and 19 deletions

View File

@ -272,7 +272,10 @@ What becomes of each file:
The date is the phone's (Apple's creation date), else one in its name, else the video's own The date is the phone's (Apple's creation date), else one in its name, else the video's own
creation time, else the file's time; the phone and its location are kept too. The thumbnail shows a creation time, else the file's time; the phone and its location are kept too. The thumbnail shows a
play mark and how long it runs; **Play in the default player** in the Capture window writes the play mark and how long it runs; **Play in the default player** in the Capture window writes the
video out to the temporary folder and opens it. video out to the temporary folder and opens it. **Export...** (any capture) writes the file as it
was imported to a folder it asks for: one of the `folder:` lines under `[export]` in the settings
file (the first is the default; none: the Downloads folder), or **Custom...**, which can be added to
them. A file of that name already there, and different, is not overwritten.
- **Snagit `.snagx`:** the same `.mcap` as `snagit-converter mcap` makes, byte for byte, named as - **Snagit `.snagx`:** the same `.mcap` as `snagit-converter mcap` makes, byte for byte, named as
the `.snagx` is. the `.snagx` is.
- **Duplicates:** an image already in the library, in any of its folders, is not stored again; the - **Duplicates:** an image already in the library, in any of its folders, is not stored again; the

View File

@ -47,7 +47,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap"; APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check"; GIT_BRANCH :: "check";
GIT_HASH :: "-"; GIT_HASH :: "-";
VERSION :: "0.1.22"; VERSION :: "0.1.23";
BUILD_DATE :: "-"; BUILD_DATE :: "-";
DEBUG :: true; DEBUG :: true;
COMPILE_TESTS :: true; COMPILE_TESTS :: true;

View File

@ -8,7 +8,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap"; APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap"; APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.1.22"; VERSION :: "0.1.23";
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated // Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
// builds never write over the ones you run by hand. // builds never write over the ones you run by hand.
build_directory := "bin"; build_directory := "bin";

View File

@ -149,15 +149,24 @@ viewer: struct {
full_fit := Viewer_Fit.WINDOW; full_fit := Viewer_Fit.WINDOW;
full_zoom: float = 100; // percent of the size it was captured at; set by the fit, unless OFF full_zoom: float = 100; // percent of the size it was captured at; set by the fit, unless OFF
// A video or animation's original, written out to open in the default player (viewer_open_original). // A capture's original, written out to open in the default player (viewer_open_original), or
// exported to a folder (viewer_export_original).
open_thread: Thread; open_thread: Thread;
open_started: bool; open_started: bool;
open_busy: bool; // set by the main thread, cleared by the open thread when it ends open_busy: bool; // set by the main thread, cleared by the open thread when it ends
open_source: string; // the .mcap; heap, main thread's until the thread starts open_source: string; // the .mcap; heap, main thread's until the thread starts
open_name: string; open_name: string;
open_folder: string; // where to export it; "" to write it to the temporary folder and open it
open_result: string; // the file written, or ""; heap, the open thread's until it ends open_result: string; // the file written, or ""; heap, the open thread's until it ends
open_error: string; open_error: string;
// The export being asked about (viewer_export_popup).
export_index: s32 = -1; // the capture to export, while the popup is up
export_choice: s64; // which of viewer_export_choices, or -1 for Custom...
export_buffer: [1024] u8; // the folder typed in for Custom...
export_add: bool; // Custom...: add the folder to [export]
export_make_default: bool;
// The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image). // The selected capture's image, decoded off the main thread for the clipboard (viewer_copy_image).
copy_thread: Thread; copy_thread: Thread;
copy_started: bool; copy_started: bool;
@ -1135,6 +1144,7 @@ viewer_ui :: () { using ImGui;
viewer_grid(); viewer_grid();
EndChild(); EndChild();
viewer_delete_popup(); viewer_delete_popup();
viewer_export_popup();
viewer_full_window(); viewer_full_window();
} }
@ -1197,17 +1207,28 @@ viewer_duration_text :: (seconds: float64) -> string {
// Writes the capture's original (a video, an animated GIF) as it was imported to a file in the // Writes the capture's original (a video, an animated GIF) as it was imported to a file in the
// temporary folder, on a thread of its own, as it may be large; viewer_open_frame opens it. // temporary folder, on a thread of its own, as it may be large; viewer_open_frame opens it.
viewer_open_original :: (index: s32) { viewer_open_original :: (index: s32) {
viewer_write_original(index, "");
}
// Writes the capture's original as it was imported into folder, on a thread of its own, as it may be
// large; viewer_open_frame says where it went.
viewer_export_original :: (index: s32, folder: string) {
viewer_write_original(index, folder);
}
viewer_write_original :: (index: s32, folder: string) {
if viewer.open_started return; if viewer.open_started return;
capture := *viewer.captures[index]; capture := *viewer.captures[index];
push_allocator(context.default_allocator); push_allocator(context.default_allocator);
free(viewer.open_source); free(viewer.open_name); free(viewer.open_result); free(viewer.open_error); free(viewer.open_source); free(viewer.open_name); free(viewer.open_folder); free(viewer.open_result); free(viewer.open_error);
viewer.open_source = copy_string(capture.path); viewer.open_source = copy_string(capture.path);
viewer.open_name = copy_string(capture.name); viewer.open_name = copy_string(capture.name);
viewer.open_folder = copy_string(folder);
viewer.open_result = ""; viewer.open_result = "";
viewer.open_error = ""; viewer.open_error = "";
viewer.open_busy = true; viewer.open_busy = true;
viewer.open_started = thread_init(*viewer.open_thread, viewer_open_proc, temporary_storage_size = 256 * 1024, name = "open original"); viewer.open_started = thread_init(*viewer.open_thread, viewer_open_proc, temporary_storage_size = 256 * 1024, name = "write original");
if !viewer.open_started { viewer.open_busy = false; toast_show(.Error, "Could not open %", capture.name); return; } if !viewer.open_started { viewer.open_busy = false; toast_show(.Error, "Could not write % out", capture.name); return; }
thread_start(*viewer.open_thread); thread_start(*viewer.open_thread);
} }
@ -1217,14 +1238,28 @@ viewer_open_proc :: (thread: *Thread) -> s64 {
m, original, ok, error := mcap_read_without_original(viewer.open_source,, temp); m, original, ok, error := mcap_read_without_original(viewer.open_source,, temp);
if !ok { viewer.open_error = copy_string(error); return 0; } if !ok { viewer.open_error = copy_string(error); return 0; }
exporting := viewer.open_folder.count > 0;
folder := viewer.open_folder;
if !exporting {
#if OS == .WINDOWS base := windows_expand_environment("%TEMP%"); #if OS == .WINDOWS base := windows_expand_environment("%TEMP%");
else base := "/tmp"; else base := "/tmp";
folder := tprint("%/%", base, APP_FOLDER_NAME); folder = tprint("%/%", base, APP_FOLDER_NAME);
}
make_directory_if_it_does_not_exist(folder, recursive = true); make_directory_if_it_does_not_exist(folder, recursive = true);
if !is_directory(folder) { viewer.open_error = copy_string(tprint("% is not a folder that can be written to", folder)); return 0; }
extension := path_extension(m.original_name); extension := path_extension(m.original_name);
path := tprint("%/%.%", folder, viewer.open_name, ifx extension then extension else "bin"); if !extension extension = "bin";
path := tprint("%/%.%", folder, viewer.open_name, extension);
_, size, exists := file_modtime_and_size(path); _, size, exists := file_modtime_and_size(path);
if !exists || size != original.size { // written out before, it is used again // Written out before, it is used again. An export does not overwrite another file of that name.
if exporting && exists && size != original.size {
for 2..1000 {
path = tprint("%/% (%).%", folder, viewer.open_name, it, extension);
_, size, exists = file_modtime_and_size(path);
if !exists || size == original.size break;
}
}
if !exists || size != original.size {
ok, error = mcap_extract_original(original, path); ok, error = mcap_extract_original(original, path);
if !ok { viewer.open_error = copy_string(error); return 0; } if !ok { viewer.open_error = copy_string(error); return 0; }
} }
@ -1232,14 +1267,141 @@ viewer_open_proc :: (thread: *Thread) -> s64 {
return 0; return 0;
} }
// Each frame: opens what viewer_open_proc wrote, once it is done. // Each frame: opens what viewer_open_proc wrote, or says where it was exported, once it is done.
viewer_open_frame :: () { viewer_open_frame :: () {
if !viewer.open_started || viewer.open_busy return; if !viewer.open_started || viewer.open_busy return;
thread_is_done(*viewer.open_thread, -1); thread_is_done(*viewer.open_thread, -1);
thread_deinit(*viewer.open_thread); thread_deinit(*viewer.open_thread);
viewer.open_started = false; viewer.open_started = false;
if viewer.open_result platform_open_file_in_default_program(viewer.open_result, path_strip_filename(viewer.open_result)); if !viewer.open_result toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error);
else toast_show(.Error, "Could not write % out: %", viewer.open_name, viewer.open_error); else if viewer.open_folder toast_show(.Success, "Exported % to %", viewer.open_name, viewer.open_result);
else platform_open_file_in_default_program(viewer.open_result, path_strip_filename(viewer.open_result));
}
// The folders Export offers: the [export] folder lines in settings.config, else the Downloads folder.
viewer_export_choices :: () -> [] string {
if app_user_settings.export_folders return app_user_settings.export_folders;
#if OS == .WINDOWS downloads := tprint("%/Downloads", windows_expand_environment("%USERPROFILE%"));
else downloads := posix_expand_home("~/Downloads");
choices := NewArray(1, string,, temp);
choices[0] = downloads;
return choices;
}
// Asks where to export the capture's original, starting from the default folder.
viewer_export_ask :: (index: s32) {
viewer.export_index = index;
viewer.export_choice = 0;
viewer.export_add = false;
viewer.export_make_default = false;
}
viewer_export_set_buffer :: (folder: string) {
count := min(folder.count, viewer.export_buffer.count - 1);
memcpy(viewer.export_buffer.data, folder.data, count);
viewer.export_buffer[count] = 0;
}
// Puts folder in [export]: first when it is to be the default, else at the end if it is not there yet.
viewer_export_remember :: (folder: string, make_default: bool) {
folders := *app_user_settings.export_folders;
changed := false;
if !folders.count {
// The list was only the Downloads folder: it stays offered.
array_add(folders, copy_string(viewer_export_choices()[0],, context.default_allocator));
changed = true;
}
at := -1;
for folders.* if it == folder { at = it_index; break; }
if make_default && at != 0 {
copy := copy_string(folder,, context.default_allocator);
if at > 0 { free(folders.*[at],, context.default_allocator); array_ordered_remove_by_index(folders, at); }
array_insert_at(folders, copy, 0);
changed = true;
} else if at < 0 {
array_add(folders, copy_string(folder,, context.default_allocator));
changed = true;
}
if changed app_save_user_settings_file_and_toast("the export folders");
}
// The folder for viewer.export_index, as a modal popup.
viewer_export_popup :: () { using ImGui;
POPUP :: "Export capture";
if viewer.export_index >= 0 && !IsPopupOpen(POPUP) OpenPopup(POPUP);
if !BeginPopupModal(POPUP, null, .AlwaysAutoResize) return;
index := viewer.export_index;
if index < 0 || index >= viewer.captures.count || viewer.captures[index].removed {
viewer.export_index = -1;
CloseCurrentPopup();
EndPopup();
return;
}
capture := *viewer.captures[index];
Text("Export %", capture.name);
TextColored(app_theme.palette.dim, "Writes the file as it was imported, unchanged, into the folder.");
Spacing();
choices := viewer_export_choices();
if viewer.export_choice >= choices.count viewer.export_choice = 0;
custom := viewer.export_choice < 0;
SetNextItemWidth(GetFontSize() * 30);
if BeginCombo("##folders", temp_c_string(ifx custom then "Custom..." else choices[viewer.export_choice])) {
for choices {
if Selectable(temp_c_string(it), it_index == viewer.export_choice) viewer.export_choice = it_index;
if it_index == 0 && IsItemHovered() SetTooltip("The default");
}
Separator();
if Selectable("Custom...", custom) && !custom {
viewer.export_choice = -1;
viewer_export_set_buffer(choices[0]); // somewhere to start from
}
EndCombo();
}
SameLine();
Text("Folder");
custom = viewer.export_choice < 0;
folder: string;
if custom {
SetNextItemWidth(GetFontSize() * 30);
InputTextWithHint("##folder", "The folder to export to", viewer.export_buffer.data, viewer.export_buffer.count);
#if OS == .WINDOWS {
SameLine();
if Button("Browse...") {
chosen := windows_choose_folder("Choose a folder to export to");
if chosen viewer_export_set_buffer(chosen);
}
}
folder = trim(to_string(viewer.export_buffer.data));
Checkbox("Add it to the list", *viewer.export_add);
if IsItemHovered() SetTooltip("Adds a folder line under [export] in %", APP_USER_SETTINGS_FILE_NAME);
} else {
folder = choices[viewer.export_choice];
}
if custom || viewer.export_choice > 0 {
Checkbox("Make it the default", *viewer.export_make_default);
if IsItemHovered() SetTooltip("Puts it first under [export] in %", APP_USER_SETTINGS_FILE_NAME);
}
Spacing();
// Enter and Escape are the open dropdown's while it is up.
keys := IsWindowFocused();
if !folder BeginDisabled();
if Button("Export") || (folder && keys && IsKeyPressed(.Enter)) {
viewer.export_index = -1;
CloseCurrentPopup();
if (custom || viewer.export_choice > 0) && viewer.export_make_default viewer_export_remember(folder, true);
else if custom && viewer.export_add viewer_export_remember(folder, false);
viewer_export_original(index, folder);
}
if !folder EndDisabled();
SameLine();
if Button("Cancel") || (keys && IsKeyPressed(.Escape)) {
viewer.export_index = -1;
CloseCurrentPopup();
}
EndPopup();
} }
// ---------------------------------------------------------------- copying the selected capture // ---------------------------------------------------------------- copying the selected capture
@ -1413,6 +1575,13 @@ viewer_full_window :: () { using ImGui;
TextColored(app_theme.palette.dim, "% x %", viewer.full_source_width, viewer.full_source_height); TextColored(app_theme.palette.dim, "% x %", viewer.full_source_width, viewer.full_source_height);
} }
if viewer.full_loading { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); } if viewer.full_loading { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); }
SameLine();
exporting := viewer.open_started;
if exporting BeginDisabled();
if SmallButton("Export...") viewer_export_ask(viewer.full_index);
if exporting EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported, unchanged, to a folder");
if exporting && viewer.open_folder { SameLine(); TextColored(app_theme.palette.dim, "Exporting..."); }
TextColored(app_theme.palette.dim, "Left / Right: previous / next"); TextColored(app_theme.palette.dim, "Left / Right: previous / next");
// Zoom is a percentage of the size the capture was taken at. A fit sets it from the window, larger // Zoom is a percentage of the size the capture was taken at. A fit sets it from the window, larger
@ -1445,7 +1614,7 @@ viewer_full_window :: () { using ImGui;
if SmallButton(temp_c_string(ifx entry.media == .VIDEO then "Play in the default player" else "Open in the default viewer")) viewer_open_original(viewer.full_index); if SmallButton(temp_c_string(ifx entry.media == .VIDEO then "Play in the default player" else "Open in the default viewer")) viewer_open_original(viewer.full_index);
if busy EndDisabled(); if busy EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported to the temporary folder, and opens it"); if IsItemHovered(.AllowWhenDisabled) SetTooltip("Writes the file as it was imported to the temporary folder, and opens it");
if busy { SameLine(); TextColored(app_theme.palette.dim, "Writing it out..."); } if busy && !viewer.open_folder { SameLine(); TextColored(app_theme.palette.dim, "Writing it out..."); }
} }
ocr_ui_capture_controls(viewer.full_index, capture.path); // app/ocr_ui.jai ocr_ui_capture_controls(viewer.full_index, capture.path); // app/ocr_ui.jai
labels_ui_capture_line(viewer.full_index); // app/labels_ui.jai labels_ui_capture_line(viewer.full_index); // app/labels_ui.jai

View File

@ -230,6 +230,19 @@ settings_tests :: () {
expect(ok && !seen.event_log && settings.event_log_capacity == EVENT_LOG_CAPACITY_DEFAULT, expect(ok && !seen.event_log && settings.event_log_capacity == EVENT_LOG_CAPACITY_DEFAULT,
"a file without the key gave seen % and capacity %", seen.event_log, settings.event_log_capacity); "a file without the key gave seen % and capacity %", seen.event_log, settings.event_log_capacity);
{
group("[export] folder lines");
data := tprint("@version %\n[export]\nfolder: D:\\Exports\nfolder:\nfolder: \" E:\\Shared #2\"\n", APP_USER_SETTINGS_VERSION);
settings: App_User_Settings;
ok, seen := settings_parse("logger_tests", data, *settings);
defer settings_free(*settings);
expect(ok && seen.export_folder && settings.export_folders.count == 2, "gave seen % and % folders", seen.export_folder, settings.export_folders.count);
if settings.export_folders.count == 2 {
expect(settings.export_folders[0] == "D:\\Exports" && settings.export_folders[1] == " E:\\Shared #2",
"gave % and %", settings.export_folders[0], settings.export_folders[1]);
}
}
// A section that does not mention an action gets its default installed right away, since the save // A section that does not mention an action gets its default installed right away, since the save
// that writes it into the file does not reload it; a pre-upgrade file gets "none" instead. // that writes it into the file does not reload it; a pre-upgrade file gets "none" instead.
{ {

View File

@ -293,6 +293,9 @@ app_user_settings: App_User_Settings;
App_User_Settings :: struct { App_User_Settings :: struct {
theme: string; // "" means the default theme; heap-allocated theme: string; // "" means the default theme; heap-allocated
ocr_model: string; // [ocr] model: the OCR model's name in models.config; "" means the default; heap-allocated ocr_model: string; // [ocr] model: the OCR model's name in models.config; "" means the default; heap-allocated
// [export] folder, one line each: the folders Export offers for a capture's original, the first
// the default. None means the Downloads folder. Heap-allocated.
export_folders: [..] string;
// Of the monitor the window first opens on. // Of the monitor the window first opens on.
window_width_fraction: float = 0.80; window_width_fraction: float = 0.80;
@ -323,6 +326,8 @@ App_User_Settings :: struct {
settings_free :: (s: *App_User_Settings) { settings_free :: (s: *App_User_Settings) {
free(s.theme,, context.default_allocator); free(s.theme,, context.default_allocator);
free(s.ocr_model,, context.default_allocator); free(s.ocr_model,, context.default_allocator);
for s.export_folders free(it,, context.default_allocator);
array_free(s.export_folders);
free(s.video_capture,, context.default_allocator); free(s.video_capture,, context.default_allocator);
array_free(s.hotkey_bindings); array_free(s.hotkey_bindings);
array_free(s.global_hotkey_bindings); array_free(s.global_hotkey_bindings);
@ -382,11 +387,11 @@ app_load_user_settings_file :: () -> write_back: bool {
if hotkeys_missing.count || global_missing.count return true; if hotkeys_missing.count || global_missing.count return true;
#if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet #if OS == .LINUX seen.global_hotkeys = true; // not written there: Linux has no global hotkeys yet
return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone); return !(seen.theme && seen.width && seen.height && seen.font_size && seen.font_scale && seen.event_log && seen.hotkeys && seen.global_hotkeys && seen.ocr_model && seen.export_folder && seen.video_fps && seen.video_cursor && seen.video_capture && seen.video_system_audio && seen.video_microphone);
} }
Settings_Seen :: struct { Settings_Seen :: struct {
theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model: bool; theme, width, height, font_size, font_scale, event_log, hotkeys, global_hotkeys, ocr_model, export_folder: bool;
video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool; video_fps, video_cursor, video_capture, video_system_audio, video_microphone: bool;
} }
@ -398,13 +403,14 @@ settings_file_hash: u64;
Settings_App_Values :: struct { Settings_App_Values :: struct {
theme: string; theme: string;
ocr_model: string; ocr_model: string;
export_folders: string; // the list, a line each
font_size_base: float; font_size_base: float;
font_scale_main: float; font_scale_main: float;
} }
// The app values as of the last load or save. A save keeps what the file says for a setting that // The app values as of the last load or save. A save keeps what the file says for a setting that
// has not changed in the app since, so an edit made by hand while the app runs is not overwritten. // has not changed in the app since, so an edit made by hand while the app runs is not overwritten.
settings_synced: Settings_App_Values; // theme heap-allocated settings_synced: Settings_App_Values; // its strings heap-allocated
settings_app_values :: () -> Settings_App_Values { settings_app_values :: () -> Settings_App_Values {
values: Settings_App_Values; values: Settings_App_Values;
@ -412,6 +418,7 @@ settings_app_values :: () -> Settings_App_Values {
values.theme = ifx entry then entry.name else app_user_settings.theme; values.theme = ifx entry then entry.name else app_user_settings.theme;
if !values.theme.count values.theme = APP_DEFAULT_THEME_NAME; if !values.theme.count values.theme = APP_DEFAULT_THEME_NAME;
values.ocr_model = ifx app_user_settings.ocr_model.count then app_user_settings.ocr_model else OCR_DEFAULT_MODEL; values.ocr_model = ifx app_user_settings.ocr_model.count then app_user_settings.ocr_model else OCR_DEFAULT_MODEL;
values.export_folders = join(..app_user_settings.export_folders, "\n",, temp);
values.font_size_base = app_user_settings.font_size_base; values.font_size_base = app_user_settings.font_size_base;
values.font_scale_main = app_user_settings.font_scale_main; values.font_scale_main = app_user_settings.font_scale_main;
@ -428,9 +435,11 @@ settings_app_values :: () -> Settings_App_Values {
settings_note_synced :: () { settings_note_synced :: () {
free(settings_synced.theme,, context.default_allocator); free(settings_synced.theme,, context.default_allocator);
free(settings_synced.ocr_model,, context.default_allocator); free(settings_synced.ocr_model,, context.default_allocator);
free(settings_synced.export_folders,, context.default_allocator);
settings_synced = settings_app_values(); settings_synced = settings_app_values();
settings_synced.theme = copy_string(settings_synced.theme,, context.default_allocator); settings_synced.theme = copy_string(settings_synced.theme,, context.default_allocator);
settings_synced.ocr_model = copy_string(settings_synced.ocr_model,, context.default_allocator); settings_synced.ocr_model = copy_string(settings_synced.ocr_model,, context.default_allocator);
settings_synced.export_folders = copy_string(settings_synced.export_folders,, context.default_allocator);
} }
// Parses the file's contents into out, replacing its theme and hotkey bindings. ok is false when // Parses the file's contents into out, replacing its theme and hotkey bindings. ok is false when
@ -445,6 +454,9 @@ settings_parse :: (path: string, file_data: string, out: *App_User_Settings) ->
array_reset(*out.hotkey_bindings); array_reset(*out.hotkey_bindings);
out.global_hotkey_bindings.allocator = context.default_allocator; out.global_hotkey_bindings.allocator = context.default_allocator;
array_reset(*out.global_hotkey_bindings); array_reset(*out.global_hotkey_bindings);
for out.export_folders free(it,, context.default_allocator);
out.export_folders.allocator = context.default_allocator;
array_reset(*out.export_folders);
seen_version := false; seen_version := false;
version: s64; version: s64;
@ -506,6 +518,10 @@ settings_parse :: (path: string, file_data: string, out: *App_User_Settings) ->
free(out.ocr_model,, context.default_allocator); free(out.ocr_model,, context.default_allocator);
out.ocr_model = copy_string(settings_unquote(value),, context.default_allocator); out.ocr_model = copy_string(settings_unquote(value),, context.default_allocator);
seen.ocr_model = true; seen.ocr_model = true;
} else if section == "[export]" && key == "folder" {
folder := settings_unquote(value); // an empty line says the list is empty
if folder.count array_add(*out.export_folders, copy_string(folder,, context.default_allocator));
seen.export_folder = true;
} else if section == "[ui]" && key == "theme" { } else if section == "[ui]" && key == "theme" {
free(out.theme,, context.default_allocator); free(out.theme,, context.default_allocator);
out.theme = copy_string(settings_unquote(value),, context.default_allocator); out.theme = copy_string(settings_unquote(value),, context.default_allocator);
@ -633,6 +649,7 @@ app_save_user_settings_file :: () -> bool {
values := settings_app_values(); values := settings_app_values();
theme := values.theme; theme := values.theme;
ocr_model := values.ocr_model; ocr_model := values.ocr_model;
export_folders: [] string = app_user_settings.export_folders;
font_size_base := values.font_size_base; font_size_base := values.font_size_base;
font_scale_main := values.font_scale_main; font_scale_main := values.font_scale_main;
@ -663,6 +680,7 @@ app_save_user_settings_file :: () -> bool {
if disk_ok { if disk_ok {
if seen.theme && values.theme == settings_synced.theme && on_disk.theme.count theme = on_disk.theme; if seen.theme && values.theme == settings_synced.theme && on_disk.theme.count theme = on_disk.theme;
if seen.ocr_model && values.ocr_model == settings_synced.ocr_model && on_disk.ocr_model.count ocr_model = on_disk.ocr_model; if seen.ocr_model && values.ocr_model == settings_synced.ocr_model && on_disk.ocr_model.count ocr_model = on_disk.ocr_model;
if seen.export_folder && values.export_folders == settings_synced.export_folders export_folders = on_disk.export_folders;
if seen.font_size && values.font_size_base == settings_synced.font_size_base font_size_base = on_disk.font_size_base; if seen.font_size && values.font_size_base == settings_synced.font_size_base font_size_base = on_disk.font_size_base;
if seen.font_scale && values.font_scale_main == settings_synced.font_scale_main font_scale_main = on_disk.font_scale_main; if seen.font_scale && values.font_scale_main == settings_synced.font_scale_main font_scale_main = on_disk.font_scale_main;
if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction; if seen.width app_user_settings.window_width_fraction = on_disk.window_width_fraction;
@ -695,6 +713,12 @@ app_save_user_settings_file :: () -> bool {
append(*builder, "[ocr]\n"); append(*builder, "[ocr]\n");
append(*builder, "# The OCR model, by its name in ocr-models/models.config: ppocr-v5-mobile (light) or ppocr-v5-server (heavy).\n"); append(*builder, "# The OCR model, by its name in ocr-models/models.config: ppocr-v5-mobile (light) or ppocr-v5-server (heavy).\n");
print_to_builder(*builder, "model: %\n\n", settings_quote_if_needed(ocr_model)); print_to_builder(*builder, "model: %\n\n", settings_quote_if_needed(ocr_model));
append(*builder, "[export]\n");
append(*builder, "# The folders Export (in the Capture window) offers to write a capture to as it was imported, a\n");
append(*builder, "# folder: line each; the first is the default. None: the Downloads folder.\n");
for export_folders print_to_builder(*builder, "folder: %\n", settings_quote_if_needed(it));
if !export_folders append(*builder, "folder:\n");
append(*builder, "\n");
append(*builder, "[video]\n"); append(*builder, "[video]\n");
append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n"); append(*builder, "# Screen recording (Ctrl+PrintScreen; Windows). fps: frames a second at most, written only when the\n");
append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n"); append(*builder, "# screen changes. cursor: true or false. capture: auto (Desktop Duplication, else\n");