Centralize legacy cloud bridge

This commit is contained in:
2026-06-04 13:09:45 +02:00
parent a9ed201adf
commit 1984b71a0a
9 changed files with 475 additions and 107 deletions

View File

@@ -82,6 +82,8 @@ set(PP_PANOPAINTER_APP_SOURCES
src/app_layout.cpp src/app_layout.cpp
src/app_shaders.cpp src/app_shaders.cpp
src/app_vr.cpp src/app_vr.cpp
src/legacy_cloud_services.cpp
src/legacy_cloud_services.h
src/platform_legacy/legacy_platform_services.cpp src/platform_legacy/legacy_platform_services.cpp
src/platform_legacy/legacy_platform_services.h src/platform_legacy/legacy_platform_services.h
src/version.cpp src/version.cpp

View File

@@ -544,6 +544,12 @@ Known local toolchain state:
and selected-file download planning as JSON; the live cloud browse command and selected-file download planning as JSON; the live cloud browse command
consumes those contracts before reaching legacy dialog, network download, consumes those contracts before reaching legacy dialog, network download,
canvas project-open, layer UI, and action-history execution. canvas project-open, layer UI, and action-history execution.
- `src/legacy_cloud_services.*` is the current app-shell bridge for cloud
upload, bulk upload, browse dialog, and download execution. It keeps those
live paths on the `pp_app_core` `CloudServices` contract while legacy
save-before-upload, progress/message UI, network upload/download helpers,
OpenGL context guarding, `NodeDialogCloud`, project open, layer refresh, and
action-history reset remain tracked by `DEBT-0038`.
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open, - `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
app-close, save, save-as, save-version, and save-before-workflow decisions app-close, save, save-as, save-version, and save-before-workflow decisions
as JSON and is covered for clean, dirty, already-prompting, missing-canvas, as JSON and is covered for clean, dirty, already-prompting, missing-canvas,

View File

@@ -55,6 +55,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior | | DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters | | DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters |
| DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, but the bridge still uses legacy save-before-upload, `upload`/`download` network helpers, progress/message UI, OpenGL context guarding, `NodeDialogCloud`, `Canvas` project open, layer refresh, and `ActionManager` reset | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, save-before-upload, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters |
## Closed Debt ## Closed Debt

View File

@@ -720,6 +720,11 @@ network upload execution continue.
download decisions used by the live cloud browse command before legacy dialog, download decisions used by the live cloud browse command before legacy dialog,
network download, canvas project-open, layer UI, and action-history execution network download, canvas project-open, layer UI, and action-history execution
continue. continue.
Cloud upload, bulk upload, and browse/download live execution now flows through
the `CloudServices` app-core boundary and `src/legacy_cloud_services.*`, keeping
`App::cloud_upload`, `App::cloud_upload_all`, and `App::cloud_browse` as thin
planning adapters while legacy save, progress UI, network, dialog, canvas-open,
layer-refresh, and action-history work remains tracked under `DEBT-0038`.
`pano_cli parse-layout` exercises the XML layout path. Continue expanding `pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary. behind the renderer boundary.
@@ -1245,6 +1250,16 @@ Results:
`pano_cli_plan_cloud_browse_selected_smoke`, and `pano_cli_plan_cloud_browse_selected_smoke`, and
`pano_cli_plan_cloud_browse_no_canvas_smoke` passed and expose app-core cloud `pano_cli_plan_cloud_browse_no_canvas_smoke` passed and expose app-core cloud
browse/download-selection decisions as JSON. browse/download-selection decisions as JSON.
- `PanoPainter`, `pp_app_core_document_cloud_tests`, and `pano_cli` built after
live cloud upload, bulk upload, and browse/download execution moved behind
the `CloudServices` boundary and `src/legacy_cloud_services.*`.
- Focused cloud CTest coverage passed for `pp_app_core_document_cloud_tests`
and all `pano_cli_plan_cloud_*` smoke tests after the live bridge split.
- `ctest --preset desktop-fast --build-config Debug` passed with 243 tests
after the cloud bridge split.
- `scripts/automation/package-smoke.ps1 -Preset windows-msvc-default
-Configuration Debug` passed executable/data checks after the cloud bridge
split; package target migration blockers remain under `DEBT-0011`.
- `pp_app_core_document_recording_tests` passed, covering recording start/stop, - `pp_app_core_document_recording_tests` passed, covering recording start/stop,
clear, platform recorded-file cleanup, frame-count reset, export progress clear, platform recorded-file cleanup, frame-count reset, export progress
totals, and oversized progress-total clamping. totals, and oversized progress-total clamping.

View File

@@ -1,9 +1,8 @@
#include "pch.h" #include "pch.h"
#include "app.h" #include "app.h"
#include "app_core/document_cloud.h" #include "app_core/document_cloud.h"
#include "legacy_cloud_services.h"
#include "util.h" #include "util.h"
#include "node_progress_bar.h"
#include "node_dialog_cloud.h"
void App::cloud_upload() void App::cloud_upload()
{ {
@@ -13,45 +12,9 @@ void App::cloud_upload()
has_canvas && Canvas::I->m_newdoc, has_canvas && Canvas::I->m_newdoc,
has_canvas && Canvas::I->m_unsaved); has_canvas && Canvas::I->m_unsaved);
switch (plan.action) const auto status = pp::panopainter::execute_legacy_cloud_upload_plan(*this, plan);
{ if (!status.ok())
case pp::app::CloudUploadAction::unavailable_no_canvas: LOG("Cloud upload action failed: %s", status.message);
return;
case pp::app::CloudUploadAction::show_save_required_warning:
message_box("Warning", "This document needs to be saved before upload.");
return;
case pp::app::CloudUploadAction::prompt_publish:
break;
}
auto upload_thread = [this] {
BT_SetTerminate();
if (Canvas::I->m_unsaved)
{
Canvas::I->project_save_thread(doc_path, true);
}
auto pb = show_progress("Uploading");
upload(doc_path, doc_filename, [this,pb](float p){
pb->set_progress(p);
});
pb->destroy();
message_box("Success", "This document has been succesfully uploaded.");
};
auto m = message_box("Publish document", "Would you like to upload to the public domain?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, upload_thread](Node*) {
std::thread(upload_thread).detach();
m->destroy();
};
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
m->destroy();
};
} }
void App::cloud_upload_all() void App::cloud_upload_all()
@@ -62,76 +25,16 @@ void App::cloud_upload_all()
auto names = Asset::list_files(data_path, ".*\\.ppi"); auto names = Asset::list_files(data_path, ".*\\.ppi");
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded); const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
gl_state gl; const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
std::shared_ptr<NodeProgressBar> pb; if (!status.ok())
if (plan.show_progress) LOG("Cloud bulk upload action failed: %s", status.message);
pb = show_progress("Export Pano Image", plan.progress_total);
for (const auto& n : names)
{
std::string path = data_path + "/" + n;
upload(path);
if (plan.show_progress)
pb->increment();
}
if (plan.show_progress)
pb->destroy();
}).detach(); }).detach();
} }
void App::cloud_browse() void App::cloud_browse()
{ {
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr); const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
switch (browse_plan) const auto status = pp::panopainter::execute_legacy_cloud_browse_action(*this, browse_plan);
{ if (!status.ok())
case pp::app::CloudBrowseAction::unavailable_no_canvas: LOG("Cloud browse action failed: %s", status.message);
return;
case pp::app::CloudBrowseAction::show_browser:
break;
}
// load thumbnail test
auto dialog = std::make_shared<NodeDialogCloud>();
dialog->set_manager(&layout);
dialog->init();
dialog->create();
dialog->loaded();
layout[main_id]->add_child(dialog);
dialog->btn_ok->on_click = [this, dialog](Node*)
{
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
if (selection_plan == pp::app::CloudDownloadSelectionAction::wait_for_selection)
return;
dialog->destroy();
std::thread([this, dialog] {
BT_SetTerminate();
auto* m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Downloading");
m->m_message->set_text("Download in progress");
std::string url = "https://panopainter.com/cloud/cloud-dwl.php?file=" + dialog->selected_file;
download(url, dialog->selected_path, [this,m](float p){
static char progress[256];
sprintf(progress, "Download in progress %.2f%%", p * 100.f);
m->m_message->set_text(progress);
});
canvas->reset_camera();
layers->clear();
canvas->m_canvas->project_open_thread(dialog->selected_path);
doc_name = dialog->selected_name;
title_update();
for (auto& l : canvas->m_canvas->m_layers)
layers->add_layer(l->m_name.c_str(), false);
ActionManager::clear();
m->destroy();
}).detach();
};
} }

View File

@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "foundation/result.h"
#include <cstddef> #include <cstddef>
#include <limits> #include <limits>
#include <string>
#include <string_view> #include <string_view>
namespace pp::app { namespace pp::app {
@@ -33,6 +36,25 @@ struct CloudBulkUploadPlan {
bool show_progress = false; bool show_progress = false;
}; };
struct CloudDownloadRequest {
std::string selected_file;
std::string selected_path;
std::string selected_name;
};
class CloudServices {
public:
virtual ~CloudServices() = default;
virtual void show_save_required_warning() = 0;
virtual void prompt_publish(bool save_before_upload) = 0;
virtual void begin_bulk_upload(int progress_total, bool show_progress) = 0;
virtual void upload_all_bulk_files() = 0;
virtual void end_bulk_upload() = 0;
virtual void show_browser() = 0;
virtual void start_download(const CloudDownloadRequest& request) = 0;
};
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload( [[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
bool has_canvas, bool has_canvas,
bool is_new_document, bool is_new_document,
@@ -76,4 +98,66 @@ struct CloudBulkUploadPlan {
}; };
} }
[[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
const CloudUploadPlan& plan,
CloudServices& services)
{
switch (plan.action) {
case CloudUploadAction::unavailable_no_canvas:
return pp::foundation::Status::success();
case CloudUploadAction::show_save_required_warning:
services.show_save_required_warning();
return pp::foundation::Status::success();
case CloudUploadAction::prompt_publish:
services.prompt_publish(plan.save_before_upload);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud upload action");
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_bulk_upload_plan(
const CloudBulkUploadPlan& plan,
CloudServices& services)
{
services.begin_bulk_upload(plan.progress_total, plan.show_progress);
services.upload_all_bulk_files();
services.end_bulk_upload();
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_browse_action(
CloudBrowseAction action,
CloudServices& services)
{
switch (action) {
case CloudBrowseAction::unavailable_no_canvas:
return pp::foundation::Status::success();
case CloudBrowseAction::show_browser:
services.show_browser();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud browse action");
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_download_selection_action(
CloudDownloadSelectionAction action,
CloudServices& services,
const CloudDownloadRequest& request)
{
switch (action) {
case CloudDownloadSelectionAction::wait_for_selection:
return pp::foundation::Status::success();
case CloudDownloadSelectionAction::start_download:
if (request.selected_file.empty()) {
return pp::foundation::Status::invalid_argument("cloud download requires a selected file");
}
services.start_download(request);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud download selection action");
}
} }

View File

@@ -0,0 +1,187 @@
#include "pch.h"
#include "legacy_cloud_services.h"
#include "app.h"
#include "canvas.h"
#include "node_dialog_cloud.h"
#include "node_progress_bar.h"
#include "util.h"
namespace pp::panopainter {
namespace {
class LegacyCloudServices final : public pp::app::CloudServices {
public:
explicit LegacyCloudServices(App& app) noexcept
: app_(app)
{
}
void show_save_required_warning() override
{
app_.message_box("Warning", "This document needs to be saved before upload.");
}
void prompt_publish(bool save_before_upload) override
{
auto* app = &app_;
auto upload_thread = [app, save_before_upload] {
BT_SetTerminate();
if (save_before_upload)
{
Canvas::I->project_save_thread(app->doc_path, true);
}
auto pb = app->show_progress("Uploading");
app->upload(app->doc_path, app->doc_filename, [pb](float p) {
pb->set_progress(p);
});
pb->destroy();
app->message_box("Success", "This document has been succesfully uploaded.");
};
auto m = app_.message_box("Publish document", "Would you like to upload to the public domain?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [m, upload_thread](Node*) {
std::thread(upload_thread).detach();
m->destroy();
};
m->btn_cancel->on_click = [m](Node*) {
m->destroy();
};
}
void begin_bulk_upload(int progress_total, bool show_progress) override
{
bulk_progress_.reset();
if (show_progress) {
bulk_progress_ = app_.show_progress("Export Pano Image", progress_total);
}
}
void upload_all_bulk_files() override
{
auto names = Asset::list_files(app_.data_path, ".*\\.ppi");
[[maybe_unused]] gl_state gl;
for (const auto& n : names)
{
std::string path = app_.data_path + "/" + n;
app_.upload(path);
if (bulk_progress_) {
bulk_progress_->increment();
}
}
}
void end_bulk_upload() override
{
if (bulk_progress_) {
bulk_progress_->destroy();
}
bulk_progress_.reset();
}
void show_browser() override
{
auto dialog = std::make_shared<NodeDialogCloud>();
dialog->set_manager(&app_.layout);
dialog->init();
dialog->create();
dialog->loaded();
app_.layout[app_.main_id]->add_child(dialog);
auto* app = &app_;
dialog->btn_ok->on_click = [app, dialog](Node*) {
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
const auto status = execute_legacy_cloud_download_selection_action(*app, selection_plan, *dialog);
if (!status.ok())
LOG("Cloud download selection action failed: %s", status.message);
};
}
void start_download(const pp::app::CloudDownloadRequest& request) override
{
auto* app = &app_;
std::thread([app, request] {
BT_SetTerminate();
auto* m = app->layout[app->main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Downloading");
m->m_message->set_text("Download in progress");
std::string url = "https://panopainter.com/cloud/cloud-dwl.php?file=" + request.selected_file;
app->download(url, request.selected_path, [m](float p) {
static char progress[256];
sprintf(progress, "Download in progress %.2f%%", p * 100.f);
m->m_message->set_text(progress);
});
app->canvas->reset_camera();
app->layers->clear();
app->canvas->m_canvas->project_open_thread(request.selected_path);
app->doc_name = request.selected_name;
app->title_update();
for (auto& l : app->canvas->m_canvas->m_layers)
app->layers->add_layer(l->m_name.c_str(), false);
ActionManager::clear();
m->destroy();
}).detach();
}
private:
App& app_;
std::shared_ptr<NodeProgressBar> bulk_progress_;
};
} // namespace
pp::foundation::Status execute_legacy_cloud_upload_plan(
App& app,
const pp::app::CloudUploadPlan& plan)
{
LegacyCloudServices services(app);
return pp::app::execute_cloud_upload_plan(plan, services);
}
pp::foundation::Status execute_legacy_cloud_bulk_upload_plan(
App& app,
const pp::app::CloudBulkUploadPlan& plan)
{
LegacyCloudServices services(app);
return pp::app::execute_cloud_bulk_upload_plan(plan, services);
}
pp::foundation::Status execute_legacy_cloud_browse_action(
App& app,
pp::app::CloudBrowseAction action)
{
LegacyCloudServices services(app);
return pp::app::execute_cloud_browse_action(action, services);
}
pp::foundation::Status execute_legacy_cloud_download_selection_action(
App& app,
pp::app::CloudDownloadSelectionAction action,
NodeDialogCloud& dialog)
{
pp::app::CloudDownloadRequest request {
.selected_file = dialog.selected_file,
.selected_path = dialog.selected_path,
.selected_name = dialog.selected_name,
};
if (action == pp::app::CloudDownloadSelectionAction::start_download) {
dialog.destroy();
}
LegacyCloudServices services(app);
return pp::app::execute_cloud_download_selection_action(action, services, request);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,25 @@
#pragma once
#include "app_core/document_cloud.h"
#include "foundation/result.h"
class App;
class NodeDialogCloud;
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_cloud_upload_plan(
App& app,
const pp::app::CloudUploadPlan& plan);
[[nodiscard]] pp::foundation::Status execute_legacy_cloud_bulk_upload_plan(
App& app,
const pp::app::CloudBulkUploadPlan& plan);
[[nodiscard]] pp::foundation::Status execute_legacy_cloud_browse_action(
App& app,
pp::app::CloudBrowseAction action);
[[nodiscard]] pp::foundation::Status execute_legacy_cloud_download_selection_action(
App& app,
pp::app::CloudDownloadSelectionAction action,
NodeDialogCloud& dialog);
} // namespace pp::panopainter

View File

@@ -1,8 +1,77 @@
#include "app_core/document_cloud.h" #include "app_core/document_cloud.h"
#include "test_harness.h" #include "test_harness.h"
#include <limits>
#include <string>
namespace { namespace {
class FakeCloudServices final : public pp::app::CloudServices {
public:
void show_save_required_warning() override
{
warnings += 1;
call_order += "warn;";
}
void prompt_publish(bool save_before_upload) override
{
prompts += 1;
last_save_before_upload = save_before_upload;
call_order += "prompt;";
}
void begin_bulk_upload(int progress_total, bool show_progress) override
{
bulk_begins += 1;
last_progress_total = progress_total;
last_show_progress = show_progress;
call_order += "begin;";
}
void upload_all_bulk_files() override
{
bulk_uploads += 1;
call_order += "upload-all;";
}
void end_bulk_upload() override
{
bulk_ends += 1;
call_order += "end;";
}
void show_browser() override
{
browsers += 1;
call_order += "browse;";
}
void start_download(const pp::app::CloudDownloadRequest& request) override
{
downloads += 1;
last_selected_file = request.selected_file;
last_selected_path = request.selected_path;
last_selected_name = request.selected_name;
call_order += "download;";
}
int warnings = 0;
int prompts = 0;
int bulk_begins = 0;
int bulk_uploads = 0;
int bulk_ends = 0;
int browsers = 0;
int downloads = 0;
int last_progress_total = 0;
bool last_save_before_upload = false;
bool last_show_progress = false;
std::string last_selected_file;
std::string last_selected_path;
std::string last_selected_name;
std::string call_order;
};
void cloud_upload_is_unavailable_without_canvas(pp::tests::Harness& harness) void cloud_upload_is_unavailable_without_canvas(pp::tests::Harness& harness)
{ {
const auto plan = pp::app::plan_cloud_upload(false, false, false); const auto plan = pp::app::plan_cloud_upload(false, false, false);
@@ -88,6 +157,79 @@ void cloud_bulk_upload_clamps_progress_total(pp::tests::Harness& harness)
PP_EXPECT(harness, plan.show_progress); PP_EXPECT(harness, plan.show_progress);
} }
void executor_dispatches_cloud_upload_variants(pp::tests::Harness& harness)
{
FakeCloudServices services;
PP_EXPECT(harness, pp::app::execute_cloud_upload_plan(pp::app::plan_cloud_upload(false, false, false), services).ok());
PP_EXPECT(harness, pp::app::execute_cloud_upload_plan(pp::app::plan_cloud_upload(true, true, true), services).ok());
PP_EXPECT(harness, pp::app::execute_cloud_upload_plan(pp::app::plan_cloud_upload(true, false, true), services).ok());
PP_EXPECT(harness, services.warnings == 1);
PP_EXPECT(harness, services.prompts == 1);
PP_EXPECT(harness, services.last_save_before_upload);
PP_EXPECT(harness, services.call_order == "warn;prompt;");
}
void executor_dispatches_cloud_bulk_browse_and_download(pp::tests::Harness& harness)
{
FakeCloudServices services;
const auto bulk = pp::app::plan_cloud_bulk_upload(3, true);
PP_EXPECT(harness, pp::app::execute_cloud_bulk_upload_plan(bulk, services).ok());
PP_EXPECT(harness, pp::app::execute_cloud_browse_action(pp::app::plan_cloud_browse(false), services).ok());
PP_EXPECT(harness, pp::app::execute_cloud_browse_action(pp::app::plan_cloud_browse(true), services).ok());
pp::app::CloudDownloadRequest wait_request;
PP_EXPECT(
harness,
pp::app::execute_cloud_download_selection_action(
pp::app::plan_cloud_download_selection(""),
services,
wait_request)
.ok());
pp::app::CloudDownloadRequest download_request {
.selected_file = "demo.ppi",
.selected_path = "D:/Paint/demo.ppi",
.selected_name = "demo",
};
PP_EXPECT(
harness,
pp::app::execute_cloud_download_selection_action(
pp::app::plan_cloud_download_selection(download_request.selected_file),
services,
download_request)
.ok());
PP_EXPECT(harness, services.bulk_begins == 1);
PP_EXPECT(harness, services.last_progress_total == 3);
PP_EXPECT(harness, services.last_show_progress);
PP_EXPECT(harness, services.bulk_uploads == 1);
PP_EXPECT(harness, services.bulk_ends == 1);
PP_EXPECT(harness, services.browsers == 1);
PP_EXPECT(harness, services.downloads == 1);
PP_EXPECT(harness, services.last_selected_file == "demo.ppi");
PP_EXPECT(harness, services.last_selected_path == "D:/Paint/demo.ppi");
PP_EXPECT(harness, services.last_selected_name == "demo");
PP_EXPECT(harness, services.call_order == "begin;upload-all;end;browse;download;");
}
void executor_rejects_mismatched_download_action(pp::tests::Harness& harness)
{
FakeCloudServices services;
pp::app::CloudDownloadRequest empty_request;
PP_EXPECT(
harness,
!pp::app::execute_cloud_download_selection_action(
pp::app::CloudDownloadSelectionAction::start_download,
services,
empty_request)
.ok());
PP_EXPECT(harness, services.downloads == 0);
}
} }
int main() int main()
@@ -105,5 +247,8 @@ int main()
harness.run("cloud bulk upload runs without progress when ui unavailable", cloud_bulk_upload_runs_without_progress_when_ui_unavailable); harness.run("cloud bulk upload runs without progress when ui unavailable", cloud_bulk_upload_runs_without_progress_when_ui_unavailable);
harness.run("cloud bulk upload keeps zero file progress explicit", cloud_bulk_upload_keeps_zero_file_progress_explicit); harness.run("cloud bulk upload keeps zero file progress explicit", cloud_bulk_upload_keeps_zero_file_progress_explicit);
harness.run("cloud bulk upload clamps progress total", cloud_bulk_upload_clamps_progress_total); harness.run("cloud bulk upload clamps progress total", cloud_bulk_upload_clamps_progress_total);
harness.run("executor dispatches cloud upload variants", executor_dispatches_cloud_upload_variants);
harness.run("executor dispatches cloud bulk browse and download", executor_dispatches_cloud_bulk_browse_and_download);
harness.run("executor rejects mismatched download action", executor_rejects_mismatched_download_action);
return harness.finish(); return harness.finish();
} }