// package_export_action.cpp — see package_export_action.h for the contract this TU // preserves. main.cpp owns the API pointers; this TU gets them extern. #include "shell/actions/package_export_action.h" #include #include #include #include #include #include "core/capture/capture_paths.h" // projectDirOfRpp, sanitizeStem #include "shell/package/export_bank.h" #include "shell/package/package_pickers.h" #include "shell/persist/session.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_ShowConsoleMsg #define REAPERAPI_WANT_ShowMessageBox #include "reaper_plugin_functions.h" namespace reasampler { namespace { constexpr const char* kUnsavedProjectMsg = "ReaSampler export: save the project first -- an unsaved project has no bank folder " "to read from.\n"; std::string currentProjectDir() { std::vector buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(buf.size())); return capture::projectDirOfRpp(std::string(buf.data())); } // 6 == YES; anything else cancels (SDK ~6544). bool confirmed(const std::string& msg, const char* title) { return ShowMessageBox(msg.c_str(), title, 4) == 6; } std::string entryLine(const package::ExcludedEntry& e) { const char* why = e.reason == package::ExclusionReason::FileMissing ? "missing" : e.reason == package::ExclusionReason::FileUnreadable ? "unreadable" : "unusable index record"; return " " + (e.displayName.empty() ? e.sampleId : e.displayName) + " [" + why + "] " + e.relativePath + "\n"; } // `maxLines` == 0 lists everything (the console record); a positive cap keeps a // confirm dialog readable on a bank with hundreds of absent files, prune's own // truncate-the-confirm-not-the-report discipline. std::string excludedManifest(const std::vector& excluded, std::size_t maxLines) { std::string msg; std::size_t shown = 0; for (const package::ExcludedEntry& e : excluded) { if (maxLines != 0 && shown == maxLines) { msg += " ... (" + std::to_string(excluded.size() - shown) + " more, listed in the console)\n"; break; } msg += entryLine(e); ++shown; } return msg; } void reportOutcome(const ExportOutcome& out, const std::string& destPath) { switch (out.status) { case ExportStatus::Written: ShowConsoleMsg(("ReaSampler export: wrote " + std::to_string(out.entriesWritten) + " entry/entries (" + std::to_string(out.bytesWritten) + " bytes) to " + destPath + "\n") .c_str()); return; case ExportStatus::SourceReadFailed: ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + out.offendingName + "\" could not be read. Nothing was written.\n") .c_str()); return; case ExportStatus::SourceChanged: ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + out.offendingName + "\" changed on disk while the package was being written. " "Nothing was written; run the export again.\n") .c_str()); return; case ExportStatus::EncodeFailed: ShowConsoleMsg("ReaSampler export: ABORTED -- this bank could not be encoded " "as a package. Nothing was written.\n"); return; // Both refusals are re-derived from a FRESH plan, so reaching them after the // survey means the bank changed under the export, not that the user declined. case ExportStatus::RefusedIncomplete: case ExportStatus::RefusedUnrepresentable: ShowConsoleMsg("ReaSampler export: ABORTED -- the bank changed between the " "report and the write. Nothing was written; run the export " "again.\n"); return; case ExportStatus::RefusedDestinationExists: ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n"); return; case ExportStatus::NoSuchBank: ShowConsoleMsg("ReaSampler export: that bank no longer exists.\n"); return; case ExportStatus::NoProjectDir: ShowConsoleMsg(kUnsavedProjectMsg); return; case ExportStatus::WriteFailed: ShowConsoleMsg(("ReaSampler export: FAILED writing " + destPath + ". No package was left behind; any file already at that path is " "untouched.\n") .c_str()); return; } } } // namespace void doBankPackageExport(const ReaSamplerSession& session, const std::string& bankId) { const std::string projectDir = currentProjectDir(); if (projectDir.empty()) { ShowConsoleMsg(kUnsavedProjectMsg); return; } // Report before acting, and before the picker opens: a refusal the user cannot // act on should not cost them a trip through a save dialog first. const ExportSurvey survey = surveyBankExport(session, projectDir, bankId); if (!survey.bankFound) { ShowConsoleMsg("ReaSampler export: no such bank.\n"); return; } const std::string bankName = survey.plan.manifest.bankDisplayName; bool allowIncomplete = false; if (survey.plan.verdict == package::ExportVerdict::Refused) { ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + bankName + "\" holds index record(s) a package cannot carry. Nothing was " "written.\n" + excludedManifest(survey.plan.excluded, 0)) .c_str()); return; } if (survey.plan.verdict == package::ExportVerdict::Incomplete) { const std::string headline = "ReaSampler export: \"" + bankName + "\" has " + std::to_string(survey.plan.excluded.size()) + " entry/entries whose file is missing or unreadable:\n"; ShowConsoleMsg((headline + excludedManifest(survey.plan.excluded, 0)).c_str()); if (!confirmed(headline + excludedManifest(survey.plan.excluded, 10) + "\nExport the " + std::to_string(survey.plan.manifest.entries.size()) + " present entry/entries anyway?", "ReaSampler: incomplete bank")) { ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n"); return; } allowIncomplete = true; } // The bank's own name, not the project's: the artifact is a bank, and a user // exporting three banks from one project needs three distinguishable files. const std::string suggested = projectDir + "/" + capture::sanitizeStem(bankName) + ".rsbank"; std::string dest; bool appended = false; if (!pickPackageSavePath(suggested, dest, &appended)) return; // user cancelled the picker ExportRequest req; req.projectDir = projectDir; req.bankId = bankId; req.destAbsPath = dest; req.exportTimestamp = static_cast(std::time(nullptr)); req.allowIncomplete = allowIncomplete; // The dialog's own overwrite confirm covered exactly this path when the picker // did not need to append `.rsbank` to reach it — asking again would be a second // prompt for the same consent. An appended path is one the dialog never saw, so // that case still falls through to exportBank's own refusal and the confirm below. req.allowOverwrite = !appended; ExportOutcome out = exportBank(session, req); if (out.status == ExportStatus::RefusedDestinationExists) { if (!confirmed("A file already exists at:\n\n " + dest + "\n\nReplace it with this bank package?", "ReaSampler: replace package")) { ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n"); return; } req.allowOverwrite = true; out = exportBank(session, req); } reportOutcome(out, dest); } } // namespace reasampler