[0.0.1] Viewer: a Refresh button, for captures deleted or added on disk
This commit is contained in:
parent
cf3a745588
commit
a254e7a3dc
@ -46,6 +46,9 @@ of previews, and one capture at full size in the Capture window.
|
||||
so it can pause, cancel and pick up again. `-library FOLDER` shows another folder of `.mcap` files;
|
||||
`-scan` scans it with the chosen model once it has loaded and exits (0, or 1 if any capture
|
||||
failed), for a scan without the UI, such as on a copy.
|
||||
- **Refresh** above the grid looks at the folder again: captures whose file was deleted leave the
|
||||
grid, and new files are added (each capture keeps its index, as the workers, the OCR scan and Stats
|
||||
refer to captures by index). By hand for now; File_Watcher can call `viewer_refresh` later.
|
||||
- **Stats** above the grid: what OCR found in the captures shown, by the chosen model: captures with
|
||||
text, lines, words, characters, confidence, words per capture, a year table, the captures with
|
||||
the most words (click to open) and the commonest words (click to search). `-stats` logs the same
|
||||
|
||||
@ -29,7 +29,7 @@ APP_NAME :: "mcap";
|
||||
APP_FOLDER_NAME :: "mcap";
|
||||
GIT_BRANCH :: "check";
|
||||
GIT_HASH :: "-";
|
||||
VERSION :: "0.0.0";
|
||||
VERSION :: "0.0.1";
|
||||
BUILD_DATE :: "-";
|
||||
DEBUG :: true;
|
||||
COMPILE_TESTS :: true;
|
||||
|
||||
@ -8,7 +8,7 @@ APP_NAME :: "mcap";
|
||||
APP_FOLDER_NAME :: "mcap";
|
||||
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
||||
|
||||
VERSION :: "0.0.0";
|
||||
VERSION :: "0.0.1";
|
||||
// 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.
|
||||
build_directory := "bin";
|
||||
|
||||
@ -303,6 +303,7 @@ ocr_scan_start :: () {
|
||||
// Every capture without a reading by this model, in the order the grid shows them.
|
||||
for viewer.visible {
|
||||
capture := *viewer.captures[it];
|
||||
if capture.removed continue;
|
||||
if capture.catalog && catalog_section(capture.catalog, scan.model) continue;
|
||||
array_add(*scan.items, .{it, copy_string(capture.path,, context.default_allocator)});
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
// When the workers have no previews to decode they load the catalog (app/catalog.jai), what search
|
||||
// and sorting need from each capture. The grid shows viewer.visible: the captures in display order,
|
||||
// newest first once the catalog is complete, and only those matching the search.
|
||||
//
|
||||
// A capture keeps its index for as long as the app runs: the workers, the OCR scan and Stats refer
|
||||
// to captures by index. Refresh (viewer_refresh) marks the files gone from the folder as removed and
|
||||
// appends the new ones.
|
||||
|
||||
VIEWER_FOLDER :: "D:/SnagitIndex/mcap"; // hardcoded for now; -library FOLDER shows another
|
||||
|
||||
@ -45,6 +49,7 @@ Viewer_Capture :: struct {
|
||||
failed: bool;
|
||||
in_flight: bool; // under viewer.mutex: queued for a worker, or being decoded
|
||||
catalog: *Catalog_Entry; // main thread only; null until loaded
|
||||
removed: bool; // main thread only: its file was gone at a refresh; shown nowhere
|
||||
}
|
||||
|
||||
// A catalog entry made off the main thread, for the main thread to put in place.
|
||||
@ -101,6 +106,7 @@ viewer: struct {
|
||||
visible_dirty := true;
|
||||
visible_rebuilt_at: Apollo_Time;
|
||||
catalog_loaded: s64; // entries put in place, or found unreadable
|
||||
removed_count: s64; // captures marked removed
|
||||
catalog_generation: s64; // counts changes to catalog entries (loading, an OCR reading stored)
|
||||
visible_generation: s64; // counts rebuilds of visible
|
||||
sorted_by_date: bool;
|
||||
@ -195,7 +201,7 @@ viewer_worker :: (thread: *Thread) -> s64 {
|
||||
index = cast(s32) viewer.catalog_next;
|
||||
viewer.catalog_next += 1;
|
||||
}
|
||||
if index >= 0 path = viewer.captures[index].path; // the array no longer grows after startup
|
||||
if index >= 0 path = viewer.captures[index].path; // the array grows only under the mutex, and paths are never freed
|
||||
unlock(*viewer.mutex);
|
||||
if index < 0 break; // nothing to do: wait to be woken
|
||||
|
||||
@ -354,7 +360,9 @@ viewer_frame_gpu_work :: () {
|
||||
unlock(*viewer.mutex);
|
||||
|
||||
capture := *viewer.captures[d.index];
|
||||
if d.failed {
|
||||
if capture.removed {
|
||||
// Its file went at a refresh while it was being decoded.
|
||||
} else if d.failed {
|
||||
capture.failed = true;
|
||||
} else if !capture.texture.handle {
|
||||
capture.texture = gpu_texture_create(d.pixels, d.width, d.height);
|
||||
@ -393,7 +401,9 @@ viewer_apply_catalog_updates :: () {
|
||||
}
|
||||
}
|
||||
if updates.count viewer.catalog_generation += 1;
|
||||
if updates.count && viewer.search_terms.count viewer.visible_dirty = true;
|
||||
// Under a search, or once sorted by date, entries arriving can move captures (after a refresh,
|
||||
// the new ones get their dates).
|
||||
if updates.count && (viewer.search_terms.count || viewer.sorted_by_date) viewer.visible_dirty = true;
|
||||
if !viewer.sorted_by_date && viewer.catalog_loaded >= viewer.captures.count {
|
||||
viewer.sorted_by_date = true;
|
||||
viewer.visible_dirty = true;
|
||||
@ -404,6 +414,61 @@ viewer_apply_catalog_updates :: () {
|
||||
|
||||
// The captures in display order: by date, newest first, once every date is known (by name until
|
||||
// then), and only those matching the search.
|
||||
// Looks at the folder again: captures whose file is gone are marked removed, and new files are
|
||||
// added, their previews and catalog entries loaded as the others' were. Indices stay as they are.
|
||||
viewer_refresh :: () {
|
||||
start := current_time_monotonic();
|
||||
present: Table(string, bool); // the folder's .mcap files now
|
||||
present.allocator = temp;
|
||||
for file_list(viewer_folder(),, temp) {
|
||||
if ends_with_nocase(path_filename(it), tprint(".%", MCAP_EXTENSION)) table_set(*present, it, true);
|
||||
}
|
||||
|
||||
removed, added: s64;
|
||||
known: Table(string, bool); // the captures still there
|
||||
known.allocator = temp;
|
||||
for * capture, index: viewer.captures {
|
||||
if capture.removed continue;
|
||||
if table_contains(*present, capture.path) { table_set(*known, capture.path, true); continue; }
|
||||
capture.removed = true;
|
||||
removed += 1;
|
||||
if capture.texture.handle {
|
||||
gpu_texture_release(*capture.texture);
|
||||
found, position := array_find(viewer.with_texture, cast(s32) index);
|
||||
if found array_ordered_remove_by_index(*viewer.with_texture, position);
|
||||
}
|
||||
if viewer.full_index == index viewer.full_open = false;
|
||||
}
|
||||
|
||||
lock(*viewer.mutex);
|
||||
for _, path: present {
|
||||
if table_contains(*known, path) continue;
|
||||
capture := array_add(*viewer.captures);
|
||||
capture.* = .{};
|
||||
capture.path = copy_string(path,, context.default_allocator);
|
||||
capture.name = copy_string(path_strip_extension(path_filename(path)),, context.default_allocator);
|
||||
added += 1;
|
||||
}
|
||||
unlock(*viewer.mutex);
|
||||
if added signal(*viewer.wake); // their catalog entries
|
||||
|
||||
viewer.removed_count += removed;
|
||||
free(viewer.scan_error);
|
||||
viewer.scan_error = "";
|
||||
if !viewer_capture_count() viewer.scan_error = sprint("No .% files in %.", MCAP_EXTENSION, viewer_folder());
|
||||
viewer.catalog_generation += 1;
|
||||
viewer.visible_dirty = true;
|
||||
viewer.visible_rebuilt_at = .{};
|
||||
log("[viewer] Refreshed %: % removed, % added, % captures, in % ms.", viewer_folder(), removed, added,
|
||||
viewer.captures.count - viewer.removed_count, FF(to_float64_seconds(current_time_monotonic() - start) * 1000.0, 1));
|
||||
toast_show(.Success, "% removed, % added", removed, added);
|
||||
}
|
||||
|
||||
// The captures not removed.
|
||||
viewer_capture_count :: () -> s64 {
|
||||
return viewer.captures.count - viewer.removed_count;
|
||||
}
|
||||
|
||||
// Sets the search box to query and searches, as typing it would.
|
||||
viewer_set_search :: (query: string) {
|
||||
count := min(query.count, viewer.search_buffer.count - 1);
|
||||
@ -431,6 +496,7 @@ viewer_rebuild_visible :: () {
|
||||
|
||||
array_reset_keeping_memory(*viewer.visible);
|
||||
for viewer.captures {
|
||||
if it.removed continue;
|
||||
if viewer.search_terms.count && !search_matches(it.catalog, viewer.search_terms) continue;
|
||||
array_add(*viewer.visible, cast(s32) it_index);
|
||||
}
|
||||
@ -544,15 +610,20 @@ viewer_request_full :: (index: s32) {
|
||||
viewer_ui :: () { using ImGui;
|
||||
viewer_frame_gpu_work();
|
||||
|
||||
if viewer.scan_error { TextColored(app_theme.palette.warning, "%", viewer.scan_error); return; }
|
||||
if viewer.scan_error {
|
||||
TextColored(app_theme.palette.warning, "%", viewer.scan_error);
|
||||
SameLine();
|
||||
if Button("Refresh") viewer_refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
SetNextItemWidth(min(GetContentRegionAvail().x * 0.5, 520));
|
||||
if InputTextWithHint("##search", "Search text, titles, URLs, apps, notes. \"a phrase\" ocr: title: url: app: note:", viewer.search_buffer.data, viewer.search_buffer.count) {
|
||||
viewer_search_changed();
|
||||
}
|
||||
SameLine();
|
||||
if viewer.search_terms.count Text("% of % captures", viewer.visible.count, viewer.captures.count);
|
||||
else Text("% captures", viewer.captures.count);
|
||||
if viewer.search_terms.count Text("% of % captures", viewer.visible.count, viewer_capture_count());
|
||||
else Text("% captures", viewer_capture_count());
|
||||
if viewer.catalog_loaded < viewer.captures.count {
|
||||
SameLine();
|
||||
TextColored(app_theme.palette.dim, "(loading the catalog: % of %; sorted by name until then)", viewer.catalog_loaded, viewer.captures.count);
|
||||
@ -565,6 +636,12 @@ viewer_ui :: () { using ImGui;
|
||||
if Button("OCR") ocr_ui.window_open = !ocr_ui.window_open;
|
||||
SameLine();
|
||||
if Button("Stats") stats_ui.window_open = !stats_ui.window_open; // app/stats_ui.jai
|
||||
SameLine();
|
||||
busy := ocr_ui.thread_busy; // a scan or a reading refers to captures by index
|
||||
if busy BeginDisabled();
|
||||
if Button("Refresh") viewer_refresh();
|
||||
if busy EndDisabled();
|
||||
if IsItemHovered(.AllowWhenDisabled) SetTooltip(ifx busy then "After the OCR scan or reading" else "Look at the folder again: drop the captures deleted, add the new ones");
|
||||
if IsItemHovered() {
|
||||
engine: *Ocr_Engine;
|
||||
if !ocr_ui.thread_busy engine = ocr_ui.engine; // the background thread owns it while busy
|
||||
|
||||
Loading…
Reference in New Issue
Block a user