From cabfa447295f9cf39b4e64b32d8d010b48adb23e Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 16:58:05 +0200 Subject: [PATCH] Route prepared file targets through platform services --- docs/modernization/build-inventory.md | 5 ++- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 7 +++- src/app.h | 3 -- src/app_events.cpp | 37 ++++++++-------- src/platform_api/platform_services.h | 11 +++++ .../legacy_platform_services.cpp | 28 +++++++++++++ .../windows_platform_services.cpp | 13 ++++++ .../platform_api/platform_services_tests.cpp | 42 +++++++++++++++++++ 9 files changed, 122 insertions(+), 25 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 8daf776..a4d1393 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -544,8 +544,9 @@ Known local toolchain state: hooks, render debug callback hooks, per-frame platform hooks, picker callbacks, recording cleanup, exported-image publishing, persistent storage flushing, document browse roots, native UI/window state saving, live - asset/layout reload policy, diagnostic stacktrace/crash hooks, prepared-file - save/download handoff; + asset/layout reload policy, diagnostic stacktrace/crash hooks, + prepared-file writable target selection, and prepared-file save/download + handoff; Windows live app execution now uses injected `WindowsPlatformServices` from diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index a439a0c..23cb789 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -70,6 +70,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through `PlatformServices`, but non-Windows execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch | | DEBT-0051 | Open | Modernization | Document browser search roots now dispatch through `PlatformServices`, but iOS `Inbox` inclusion still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve current iOS document import/browse behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | | DEBT-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge | Preserve current Windows/macOS UI persistence while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple package smoke once root Apple builds exist | UI/window state persistence is owned by injected platform services with no legacy adapter branch | +| DEBT-0053 | Open | Modernization | Prepared-file writable target selection now dispatches through `PlatformServices`, but iOS/Web target selection still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection and save/download handoff are owned by injected platform services with no legacy adapter branch | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6a6eaca..b16da38 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -639,6 +639,10 @@ is preserved while their platform shell implementations are extracted. Prepared-file save/download handoff is now also part of the service contract, so iOS/Web export completion routes through `PlatformServices` after the app writes the temporary/exported payload. +Prepared-file writable target selection now also dispatches through +`PlatformServices`, preserving the existing iOS temporary background-write path +and Web data-path synchronous write path while removing those platform branches +from `App::pick_file_save`. Canvas image export publishing and explicit persistent-storage flushes now dispatch through `PlatformServices` too, preserving iOS photo-library export publication and WebGL filesystem sync behavior in the legacy adapter while @@ -1692,7 +1696,8 @@ Results: dispatch, render debug callback dispatch, render-capture frame hook dispatch, recording cleanup dispatch, exported-image publish dispatch, persistent storage flush dispatch, document browse-root dispatch, - native UI/window state save dispatch, live asset/layout reload policy dispatch, + native UI/window state save dispatch, prepared-file writable target dispatch, + live asset/layout reload policy dispatch, diagnostic hook dispatch, per-frame platform hook dispatch, picker callback dispatch, and prepared-file save/download callback dispatch. The live Windows app now diff --git a/src/app.h b/src/app.h index f4a68e4..353bd69 100644 --- a/src/app.h +++ b/src/app.h @@ -181,12 +181,9 @@ public: bool clipboard_set_text(const std::string& s); void pick_image(std::function callback); void pick_file(std::vector types, std::function callback); -#if __IOS__ || __WEB__ void pick_file_save(const std::string& type, const std::string& default_name, std::function writer, std::function callback); -#else void pick_file_save(std::vector types, std::function callback); -#endif void pick_dir(std::function callback); void display_file(std::string path); void share_file(std::string path); diff --git a/src/app_events.cpp b/src/app_events.cpp index 720e8e4..6a670e9 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -152,35 +152,34 @@ void App::pick_file(std::vector types, std::function writer, std::function callback) { redraw = true; - std::string ext = "." + type; - std::string path = tmp_path + "/" + default_name + ext; - std::thread([=]{ - writer(path); - save_prepared_file(path, default_name + ext, callback); - }).detach(); + const auto target = active_platform_services().prepare_writable_file(type, default_name, data_path, tmp_path); + if (target.path.empty()) + { + callback({}, false); + return; + } + + LOG("App::pick_file_save %s", target.path.c_str()); + auto write_and_save = [=] { + writer(target.path); + save_prepared_file(target.path, target.suggested_name, callback); + }; + + if (target.write_on_background_thread) + std::thread(write_and_save).detach(); + else + write_and_save(); } -#elif __WEB__ -void App::pick_file_save(const std::string& type, const std::string& default_name, - std::function writer, std::function callback) -{ - redraw = true; - auto path = data_path + "/" + default_name + "." + type; - LOG("App::pick_file_save %s", path.c_str()); - writer(path); - save_prepared_file(path, default_name + "." + type, std::move(callback)); -} -#else + void App::pick_file_save(std::vector types, std::function callback) { redraw = true; active_platform_services().pick_save_file(std::move(types), std::move(callback)); } -#endif void App::pick_dir(std::function callback) { diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h index fa9130f..42090f2 100644 --- a/src/platform_api/platform_services.h +++ b/src/platform_api/platform_services.h @@ -10,6 +10,12 @@ namespace pp::platform { using PickedPathCallback = std::function; using PreparedFileCallback = std::function; +struct PreparedFileTarget { + std::string path; + std::string suggested_name; + bool write_on_background_thread = false; +}; + struct PlatformStoragePaths { std::string data_path; std::string work_path; @@ -57,6 +63,11 @@ public: virtual void pick_file(std::vector file_types, PickedPathCallback callback) = 0; virtual void pick_save_file(std::vector file_types, PickedPathCallback callback) = 0; virtual void pick_directory(PickedPathCallback callback) = 0; + [[nodiscard]] virtual PreparedFileTarget prepare_writable_file( + std::string_view type, + std::string_view default_name, + std::string_view data_path, + std::string_view temporary_path) = 0; virtual void save_prepared_file( std::string_view path, std::string_view suggested_name, diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index 2237137..8d5863a 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -431,6 +431,34 @@ public: #endif } + [[nodiscard]] pp::platform::PreparedFileTarget prepare_writable_file( + std::string_view type, + std::string_view default_name, + std::string_view data_path, + std::string_view temporary_path) override + { + const std::string name = std::string(default_name) + "." + std::string(type); +#ifdef __IOS__ + (void)data_path; + return { + std::string(temporary_path) + "/" + name, + name, + true, + }; +#elif __WEB__ + (void)temporary_path; + return { + std::string(data_path) + "/" + name, + name, + false, + }; +#else + (void)data_path; + (void)temporary_path; + return {}; +#endif + } + void display_file(std::string_view path) override { const std::string value(path); diff --git a/src/platform_windows/windows_platform_services.cpp b/src/platform_windows/windows_platform_services.cpp index 5fe320d..2970158 100644 --- a/src/platform_windows/windows_platform_services.cpp +++ b/src/platform_windows/windows_platform_services.cpp @@ -448,6 +448,19 @@ public: invoke_selected_path(path, callback); } + [[nodiscard]] pp::platform::PreparedFileTarget prepare_writable_file( + std::string_view type, + std::string_view default_name, + std::string_view data_path, + std::string_view temporary_path) override + { + (void)type; + (void)default_name; + (void)data_path; + (void)temporary_path; + return {}; + } + void save_prepared_file( std::string_view path, std::string_view suggested_name, diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index e3cec2d..0a559fe 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -210,6 +210,20 @@ public: callback(directory_path); } + [[nodiscard]] pp::platform::PreparedFileTarget prepare_writable_file( + std::string_view type, + std::string_view default_name, + std::string_view data_path, + std::string_view temporary_path) override + { + ++prepare_writable_file_requests; + writable_file_type.assign(type); + writable_file_default_name.assign(default_name); + writable_file_data_path.assign(data_path); + writable_file_temporary_path.assign(temporary_path); + return writable_file_target; + } + void save_prepared_file( std::string_view path, std::string_view suggested_name, @@ -255,6 +269,7 @@ public: int pick_file_requests = 0; int pick_save_file_requests = 0; int pick_directory_requests = 0; + int prepare_writable_file_requests = 0; int save_prepared_file_requests = 0; bool cursor_visible = false; bool keyboard_visible = false; @@ -271,6 +286,10 @@ public: std::string exported_image_path; std::string browse_work_path; std::string browse_data_path; + std::string writable_file_type; + std::string writable_file_default_name; + std::string writable_file_data_path; + std::string writable_file_temporary_path; std::string picker_path = "D:/Paint/import.png"; std::string save_path = "D:/Paint/export.ppi"; std::string directory_path = "D:/Paint"; @@ -286,6 +305,11 @@ public: "D:/Paint/work", "D:/Paint/Inbox", }; + pp::platform::PreparedFileTarget writable_file_target{ + "D:/Paint/tmp/export.mp4", + "export.mp4", + true, + }; private: std::string clipboard_value_; @@ -542,6 +566,23 @@ void platform_services_dispatch_prepared_file_save(pp::tests::Harness& harness) PP_EXPECT(harness, saved); } +void platform_services_dispatch_writable_file_target(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + + const auto target = services.prepare_writable_file("mp4", "export", "D:/Paint", "D:/Paint/tmp"); + + PP_EXPECT(harness, fake.prepare_writable_file_requests == 1); + PP_EXPECT(harness, fake.writable_file_type == "mp4"); + PP_EXPECT(harness, fake.writable_file_default_name == "export"); + PP_EXPECT(harness, fake.writable_file_data_path == "D:/Paint"); + PP_EXPECT(harness, fake.writable_file_temporary_path == "D:/Paint/tmp"); + PP_EXPECT(harness, target.path == "D:/Paint/tmp/export.mp4"); + PP_EXPECT(harness, target.suggested_name == "export.mp4"); + PP_EXPECT(harness, target.write_on_background_thread); +} + } int main() @@ -564,5 +605,6 @@ int main() harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions); harness.run("platform services dispatch picker callbacks", platform_services_dispatch_picker_callbacks); harness.run("platform services dispatch prepared file save", platform_services_dispatch_prepared_file_save); + harness.run("platform services dispatch writable file target", platform_services_dispatch_writable_file_target); return harness.finish(); }