Files
FileManager/AppActions.cpp
T
2025-11-30 16:14:43 -08:00

949 lines
34 KiB
C++

#include "AppActions.h"
#include "FileBrowserApp.h"
#include "FsUtil.h"
#include "XBInput.h" // XBInput_GetInput, g_Gamepads
#include "xipslib.h"
#include "xipslibUtil.h"
#include "unzipLIB.h"
#include "unzipLIBUtil.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h> // toupper
/*
============================================================================
AppActions
- Centralized execution of menu actions for FileBrowserApp
- Cancel-aware copy/move with a "press B twice to cancel" toast window
- Uses FsUtil.* helpers for I/O and FileBrowserApp methods for UI refresh
============================================================================
*/
// ---- Cancel-aware progress context + thunk ---------------------------------
// CopyProgCtx: tracks progress, cancel state, and the 2-press B confirmation.
// The "toast window" behavior:
// 1) First B press shows "Press B again to cancel" and arms a window.
// 2) If the second B happens while the toast is still visible, we cancel.
// 3) If toast expires before second B, the next B just re-arms the window.
struct CopyProgCtx {
FileBrowserApp* app;
ULONGLONG base; // bytes completed from previous items (offset)
bool canceled; // set when user confirms cancel
// confirmation state (toast window)
bool confirmArmed; // true after the first B press
DWORD confirmUntil; // snapshot of when the toast will expire (informational)
bool prevB; // for rising-edge detection of B
};
// Progress callback used by file copy/move loops.
// - Pumps gamepad input to detect B presses
// - Updates the app's progress overlay
// - Implements the "press B twice" cancel logic
static bool CopyProgThunk(ULONGLONG done, ULONGLONG total, const char* label, void* user){
CopyProgCtx* c = (CopyProgCtx*)user;
// Poll controller to read B presses while copying
XBInput_GetInput();
const XBGAMEPAD& pad = g_Gamepads[0];
const bool bNow = (pad.bAnalogButtons[XINPUT_GAMEPAD_B] > 30);
const DWORD now = GetTickCount();
// Paint progress (done is per-current-item, base is previous items)
c->app->UpdateProgress(c->base + done, total, label);
// Rising-edge B?
if (bNow && !c->prevB){
const DWORD statusUntil = c->app->StatusUntilMs();
const bool toastAlive = (now < statusUntil);
if (c->confirmArmed && toastAlive){
// Second B while the toast is still up => cancel
c->canceled = true;
SetLastError(ERROR_OPERATION_ABORTED);
c->prevB = bNow;
return false; // abort copy/move
} else {
// First B: arm and show "press B again" toast
c->confirmArmed = true;
c->app->SetStatus("Press B again to cancel");
c->confirmUntil = c->app->StatusUntilMs(); // snapshot (optional)
}
}
// Disarm if toast has expired
if (c->confirmArmed && now >= c->app->StatusUntilMs()){
c->confirmArmed = false;
}
c->prevB = bNow;
return true; // keep going
}
// ---- local helpers ----------------------------------------------------------
// Return 1 if both paths are on the same drive letter (case-insensitive).
static int SameDriveLetter(const char* a, const char* b){
if (!a || !b || !a[0] || !b[0]) return 0;
return toupper((unsigned char)a[0]) == toupper((unsigned char)b[0]);
}
// Ensure trailing backslash on non-empty strings (local, VC7.1-safe).
static void NormalizeSlashEndLocal(char* s, size_t cap){
size_t n = s ? strlen(s) : 0;
if (n && s[n-1] != '\\' && n+1 < cap){ s[n] = '\\'; s[n+1] = 0; }
}
// Case-insensitive: is `child` a subpath of `parent`?
static int IsSubPathCaseI(const char* parent, const char* child){
char p[512], c[512];
_snprintf(p, sizeof(p), "%s", parent ? parent : ""); p[sizeof(p)-1] = 0;
_snprintf(c, sizeof(c), "%s", child ? child : ""); c[sizeof(c)-1] = 0;
NormalizeSlashEndLocal(p, sizeof(p));
NormalizeSlashEndLocal(c, sizeof(c));
return _strnicmp(p, c, (int)strlen(p)) == 0; // child starts with parent?
}
// Basename (pointer into input string), e.g., "E:\A\B\C" -> "C".
static const char* BaseNameOf(const char* path){
const char* s = path ? strrchr(path, '\\') : 0;
return s ? (s+1) : path;
}
namespace AppActions {
// Collects selected sources for copy/move/delete:
// - If any items are marked, returns all marked (excluding "..").
// - Else returns the single current selection (if not "..").
// - Writes full paths (dir + name) into 'out'.
static void GatherMarkedOrSelectedFullPaths(const Pane& src, std::vector<std::string>& out) {
if (src.mode != 1 || src.items.empty()) return;
bool any = false;
for (size_t i=0; i<src.items.size(); ++i) {
const Item& it = src.items[i];
if (it.marked && !it.isUpEntry) {
char full[512]; JoinPath(full, sizeof(full), src.curPath, it.name);
out.push_back(full);
any = true;
}
}
if (!any) { // fall back to single selection
const Item& sel = src.items[src.sel];
if (!sel.isUpEntry) {
char full[512]; JoinPath(full, sizeof(full), src.curPath, sel.name);
out.push_back(full);
}
}
}
// Main dispatcher: runs the action the user picked from the context menu.
void Execute(Action act, FileBrowserApp& app) {
Pane& src = app.m_pane[app.m_active];
Pane& dst = app.m_pane[1 - app.m_active];
const Item* sel = NULL;
const Item* sel2 = NULL;
if (!src.items.empty()) sel = &src.items[src.sel];
if (!dst.items.empty()) sel2 = &dst.items[dst.sel];
// Full path of selection (if any). Also supports drive-list selection.
char srcFull[512] = "";
char dstFull[512] = "";
const char* ext = NULL;
const char* ext2 = NULL;
if (sel) {
if (src.mode == 1 && !sel->isUpEntry) {
if (!sel->isDir) ext = GetExtension(sel->name);
// Normal directory listing: dir + name
JoinPath(srcFull, sizeof(srcFull), src.curPath, sel->name);
srcFull[sizeof(srcFull)-1] = 0;
} else if (src.mode == 0 && sel->isDir && !sel->isUpEntry) {
// Drive list: item name is already something like "E:\"
_snprintf(srcFull, sizeof(srcFull), "%s", sel->name);
srcFull[sizeof(srcFull)-1] = 0;
NormalizeDirA(srcFull); // ensure trailing slash
}
}
if (sel2) {
if (dst.mode == 1 && !sel2->isUpEntry) {
if (!sel2->isDir) ext2 = GetExtension(sel2->name);
// Normal directory listing: dir + name
JoinPath(dstFull, sizeof(dstFull), dst.curPath, sel2->name);
dstFull[sizeof(dstFull)-1] = 0;
} else if (dst.mode == 0 && sel2->isDir && !sel2->isUpEntry) {
// Drive list: item name is already something like "E:\"
_snprintf(dstFull, sizeof(dstFull), "%s", sel2->name);
dstFull[sizeof(dstFull)-1] = 0;
NormalizeDirA(dstFull); // ensure trailing slash
}
}
switch (act)
{
// ---- Open / Enter / Launch --------------------------------------------
case ACT_OPEN:
{
if (sel) {
if (sel->isUpEntry) { app.UpOne(src); }
else if (sel->isDir) { app.EnterSelection(src); }
else if (HasXbeExt(sel->name)) {
char full[512]; JoinPath(full, sizeof(full), src.curPath, sel->name);
if (!LaunchXbeA(full)) app.SetStatusLastErr("Launch failed");
}
}
break;
}
// ---- Copy -----------------------------------------------------------------
case ACT_COPY:
{
if (src.mode != 1) { app.SetStatus("Open a folder"); break; }
// Resolve destination (other pane preferred)
char dstDir[512];
if (!app.ResolveDestDir(dstDir, sizeof(dstDir))) { app.SetStatus("Pick a destination"); break; }
if ((dstDir[0] == 'D' || dstDir[0] == 'd') && dstDir[1] == ':') { app.SetStatus("Cannot copy to D:\\"); break; }
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)) { app.SetStatusLastErr("Dest not writable"); break; }
// Gather sources
std::vector<std::string> srcs;
GatherMarkedOrSelectedFullPaths(src, srcs);
if (srcs.empty()) { app.SetStatus("Nothing to copy"); break; }
// Compute total bytes for progress bar
ULONGLONG total = 0;
for (size_t i = 0; i < srcs.size(); ++i) total += DirSizeRecursiveA(srcs[i].c_str());
// --- preflight free-space check on destination (kept from your version) ---
{
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(dstDir, freeB, totB);
if (total > freeB) {
char need[64], have[64];
FormatSize(total, need, sizeof(need));
FormatSize(freeB, have, sizeof(have));
app.SetStatus("Not enough space: need %s, have %s", need, have);
break;
}
}
// --- end preflight ---
// Begin progress + set callback
app.BeginProgress(total, srcs[0].c_str(), "Copying...");
CopyProgCtx ctx = { &app, 0, false, false, 0, false };
SetCopyProgressCallback(CopyProgThunk, &ctx);
ULONGLONG base = 0; // cumulative bytes completed
char lastDstTop[512] = { 0 }; // for cancel cleanup
// NEW: track results so the final toast reflects reality
size_t copiedOk = 0, failed = 0, skipped = 0;
for (size_t i = 0; i < srcs.size(); ++i) {
const char* sp = srcs[i].c_str();
// Compute top-level destination path (dstDir\basename(sp)) for cleanup
const char* bn = BaseNameOf(sp);
JoinPath(lastDstTop, sizeof(lastDstTop), dstDir, bn);
ULONGLONG thisSize = DirSizeRecursiveA(sp);
ctx.base = base;
// --- prevent copying a folder into its own subfolder (or itself) ---
if (IsSubPathCaseI(sp, lastDstTop)) {
app.SetStatus("Cannot copy a folder into its own subfolder");
++skipped;
continue; // skip this item, proceed to next
}
// --- per-item free-space check (extra safety) ---
{
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(lastDstTop, freeB, totB); // any path on dest volume is fine
if (thisSize > freeB) {
char need[64], have[64];
FormatSize(thisSize, need, sizeof(need));
FormatSize(freeB, have, sizeof(have));
app.SetStatus("Not enough space for %s: need %s, have %s", bn, need, have);
break; // stop the whole batch (change to 'continue;' to try remaining items)
}
}
// --- end per-item check ---
if (!CopyRecursiveWithProgressA(sp, dstDir, total)) {
if (ctx.canceled) {
// Remove partial destination of the current item, then stop
DeleteRecursiveA(lastDstTop);
break;
}
// Non-cancel failure
++failed;
}
else {
base += thisSize;
++copiedOk;
}
}
// End progress and clear callback
SetCopyProgressCallback(NULL, NULL);
app.EndProgress();
if (ctx.canceled) {
app.SetStatus("Copy canceled (%u done, %u skipped, %u failed)",
(unsigned)copiedOk, (unsigned)skipped, (unsigned)failed);
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
break;
}
// If we copied into the currently displayed dest folder, refresh it
Pane& dstp = app.m_pane[1 - app.m_active];
if (dstp.mode == 1 && _stricmp(dstp.curPath, dstDir) == 0) ListDirectory(dstp.curPath, dstp.items);
// Clear marks and refresh both panes
for (size_t i = 0; i < src.items.size(); ++i) src.items[i].marked = false;
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
// Final toast that reflects what actually happened
if (failed == 0 && skipped == 0) {
app.SetStatus("Copied %u item(s)", (unsigned)copiedOk);
}
else {
app.SetStatus("Copied %u, %u skipped, %u failed",
(unsigned)copiedOk, (unsigned)skipped, (unsigned)failed);
}
break;
}
// ---- Move -----------------------------------------------------------------
case ACT_MOVE:
{
if (src.mode != 1) { app.SetStatus("Open a folder"); break; }
char dstDir[512];
if (!app.ResolveDestDir(dstDir, sizeof(dstDir))) { app.SetStatus("Pick a destination"); break; }
if ((dstDir[0] == 'D' || dstDir[0] == 'd') && dstDir[1] == ':') { app.SetStatus("Cannot move to D:\\"); break; }
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)) { app.SetStatusLastErr("Dest not writable"); break; }
std::vector<std::string> srcs;
GatherMarkedOrSelectedFullPaths(src, srcs);
if (srcs.empty()) { app.SetStatus("Nothing to move"); break; }
// Total bytes for progress
ULONGLONG total = 0;
for (size_t i = 0; i < srcs.size(); ++i) total += DirSizeRecursiveA(srcs[i].c_str());
// --- preflight space only when cross-volume (move will copy+delete) ---
{
const bool sameVol = SameDriveLetter(src.curPath, dstDir) != 0;
if (!sameVol) {
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(dstDir, freeB, totB);
if (total > freeB) {
char need[64], have[64];
FormatSize(total, need, sizeof(need));
FormatSize(freeB, have, sizeof(have));
app.SetStatus("Not enough space: need %s, have %s", need, have);
break;
}
}
}
// --- end preflight ---
app.BeginProgress(total, srcs[0].c_str(), "Moving...");
CopyProgCtx ctx = { &app, 0, false, false, 0, false };
SetCopyProgressCallback(CopyProgThunk, &ctx);
size_t movedOk = 0, failed = 0, skipped = 0; // NEW: track results
ULONGLONG base = 0;
for (size_t i = 0; i < srcs.size(); ++i) {
const char* sp = srcs[i].c_str();
ULONGLONG thisSize = DirSizeRecursiveA(sp);
ctx.base = base;
// Destination top (dstDir\basename(sp))
const char* baseName = BaseNameOf(sp);
char dstTop[512]; JoinPath(dstTop, sizeof(dstTop), dstDir, baseName);
// --- NEW: prevent moving a folder into its own subfolder (or onto itself)
if (IsSubPathCaseI(sp, dstTop)) {
app.SetStatus("Cannot move a folder into its own subfolder");
++skipped;
continue; // skip this item and go on
}
BOOL doneThis = FALSE;
// --- NEW: per-item free-space check only if not a fast same-volume rename
const BOOL canFastRename =
SameDriveLetter(sp, dstDir) && !IsSubPathCaseI(sp, dstTop);
if (!canFastRename) {
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(dstTop, freeB, totB);
if (thisSize > freeB) {
char need[64], have[64];
FormatSize(thisSize, need, sizeof(need));
FormatSize(freeB, have, sizeof(have));
app.SetStatus("Not enough space to move %s: need %s, have %s", baseName, need, have);
break; // stop the whole batch (use 'continue;' if you prefer to try remaining items)
}
}
// --- end NEW ---
// Fast path: same drive and not moving into own subfolder -> MoveFileA
if (SameDriveLetter(sp, dstDir) && !IsSubPathCaseI(sp, dstTop)) {
if (MoveFileA(sp, dstTop)) {
doneThis = TRUE; // instant rename/move within volume
}
}
// Fallback: copy -> delete original (only delete if copy succeeded)
if (!doneThis) {
if (!CopyRecursiveWithProgressA(sp, dstDir, total)) {
if (ctx.canceled) {
// Clean partial dest and stop
DeleteRecursiveA(dstTop);
break;
}
// Non-cancel failure
++failed;
}
else {
if (DeleteRecursiveA(sp)) {
doneThis = TRUE;
}
else {
// Optional: consider removing dstTop if source delete failed
++failed;
}
}
}
if (doneThis) ++movedOk;
base += thisSize;
}
SetCopyProgressCallback(NULL, NULL);
app.EndProgress();
if (ctx.canceled) {
app.SetStatus("Move canceled (%u done, %u skipped, %u failed)",
(unsigned)movedOk, (unsigned)skipped, (unsigned)failed);
// On cancel we keep originals; clear marks and refresh UI
for (size_t i = 0; i < src.items.size(); ++i) src.items[i].marked = false;
if (src.mode == 1) ListDirectory(src.curPath, src.items);
{
Pane& dstp = app.m_pane[1 - app.m_active];
if (dstp.mode == 1 && _stricmp(dstp.curPath, dstDir) == 0) ListDirectory(dstp.curPath, dstp.items);
}
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
break;
}
// Normal completion: refresh both panes, clear marks
for (size_t i = 0; i < src.items.size(); ++i) src.items[i].marked = false;
if (src.mode == 1) ListDirectory(src.curPath, src.items);
{
Pane& dstp = app.m_pane[1 - app.m_active];
if (dstp.mode == 1 && _stricmp(dstp.curPath, dstDir) == 0) ListDirectory(dstp.curPath, dstp.items);
}
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
// NEW: accurate final toast
if (failed == 0 && skipped == 0) {
app.SetStatus("Moved %u item(s)", (unsigned)movedOk);
}
else {
app.SetStatus("Moved %u, %u skipped, %u failed",
(unsigned)movedOk, (unsigned)skipped, (unsigned)failed);
}
break;
}
// ---- Delete ---------------------------------------------------------------
case ACT_DELETE:
{
if (src.mode != 1) { app.SetStatus("Open a folder"); break; }
std::vector<std::string> srcs;
GatherMarkedOrSelectedFullPaths(src, srcs);
if (srcs.empty()) { app.SetStatus("Nothing to delete"); break; }
int ok = 0;
for (size_t i = 0; i < srcs.size(); ++i) if (DeleteRecursiveA(srcs[i].c_str())) ++ok;
// Clear marks, refresh, and toast result
for (size_t i = 0; i < src.items.size(); ++i) src.items[i].marked = false;
if (src.mode == 1) ListDirectory(src.curPath, src.items);
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
app.SetStatus("Deleted %d / %d", ok, (int)srcs.size());
break;
}
// ---- Rename (opens modal OSK) ---------------------------------------------
case ACT_RENAME:
{
if (sel && src.mode == 1 && !sel->isUpEntry) {
app.BeginRename(src.curPath, sel->name);
}
else {
app.SetStatus("Open a folder and select an item");
}
break;
}
// ---- Make new folder ------------------------------------------------------
case ACT_MKDIR:
{
char baseDir[512] = { 0 };
if (src.mode == 1) {
_snprintf(baseDir, sizeof(baseDir), "%s", src.curPath);
}
else if (!src.items.empty()) {
const Item& di = src.items[src.sel];
if (di.isDir && !di.isUpEntry) {
_snprintf(baseDir, sizeof(baseDir), "%s", di.name); // drive root, e.g. "E:\"
}
}
baseDir[sizeof(baseDir) - 1] = 0;
if (!baseDir[0]) { app.SetStatus("Open a folder or select a drive first"); break; }
if ((baseDir[0] == 'D' || baseDir[0] == 'd') && baseDir[1] == ':') {
app.SetStatus("Cannot create on D:\\ (read-only)"); break;
}
NormalizeDirA(baseDir);
if (!CanWriteHereA(baseDir)) { app.SetStatusLastErr("Dest not writable"); break; }
// Auto-name NewFolder[/N] without clobbering existing names
char nameBuf[64];
char target[512];
int idx = 0;
for (;;) {
if (idx == 0) _snprintf(nameBuf, sizeof(nameBuf), "NewFolder");
else _snprintf(nameBuf, sizeof(nameBuf), "NewFolder%d", idx);
nameBuf[sizeof(nameBuf) - 1] = 0;
JoinPath(target, sizeof(target), baseDir, nameBuf);
if (!DirExistsA(target)) {
if (CreateDirectoryA(target, NULL)) {
app.SetStatus("Created %s", nameBuf);
}
else {
app.SetStatusLastErr("Create folder failed");
}
break;
}
if (++idx > 999) { app.SetStatus("Create folder failed (names exhausted)"); break; }
}
app.RefreshPane(app.m_pane[0]); app.RefreshPane(app.m_pane[1]);
// If we created inside the active folder, select the new dir
if (src.mode == 1 && _stricmp(src.curPath, baseDir) == 0) {
app.SelectItemInPane(src, nameBuf);
}
break;
}
// ---- Calculate size -------------------------------------------------------
case ACT_CALCSIZE:
{
if (!sel) break;
if (!srcFull[0]) {
// Try to derive from selection directly (covers any future cases)
char tmpPath[512] = "";
if (src.mode == 0 && sel->isDir && !sel->isUpEntry) {
_snprintf(tmpPath, sizeof(tmpPath), "%s", sel->name);
tmpPath[sizeof(tmpPath) - 1] = 0;
NormalizeDirA(tmpPath);
}
if (!tmpPath[0]) { app.SetStatus("Open a folder or select a drive"); break; }
ULONGLONG bytes = DirSizeRecursiveA(tmpPath);
char tmp[64]; FormatSize(bytes, tmp, sizeof(tmp));
app.SetStatus("%s", tmp);
break;
}
ULONGLONG bytes = DirSizeRecursiveA(srcFull);
char tmp[64]; FormatSize(bytes, tmp, sizeof(tmp));
app.SetStatus("%s", tmp);
break;
}
// ---- Go to root (or back to drive list) -----------------------------------
case ACT_GOROOT:
{
if (src.mode == 1) {
if (!IsDriveRoot(src.curPath)) {
char root[4] = { src.curPath[0], ':', '\\', 0 };
strncpy(src.curPath, root, sizeof(src.curPath) - 1);
src.curPath[3] = 0;
src.sel = 0; src.scroll = 0;
ListDirectory(src.curPath, src.items);
}
else {
// Already at root -> switch to drive list
src.mode = 0; src.curPath[0] = 0; src.sel = 0; src.scroll = 0;
BuildDriveItems(src.items);
}
}
else {
// In drive list: refresh it
BuildDriveItems(src.items);
}
break;
}
// ---- Marking operations ---------------------------------------------------
case ACT_MARK_ALL:
{
Pane& p = app.m_pane[app.m_active];
if (p.mode==1 && !p.items.empty()) {
int n=0;
for (size_t i=0; i<p.items.size(); ++i) {
if (!p.items[i].isUpEntry && !p.items[i].marked) {
p.items[i].marked = true; ++n;
}
}
if (n>0) app.SetStatus("Marked %d item%s", n, (n==1?"":"s"));
else app.SetStatus("All already marked");
}
break;
}
case ACT_INVERT_MARKS:
{
Pane& p = app.m_pane[app.m_active];
if (p.mode==1 && !p.items.empty()) {
int toggled=0;
for (size_t i=0;i<p.items.size(); ++i) {
if (!p.items[i].isUpEntry) {
p.items[i].marked = !p.items[i].marked;
++toggled;
}
}
app.SetStatus("Inverted marks (%d)", toggled);
}
break;
}
case ACT_CLEAR_MARKS:
{
Pane& p = app.m_pane[app.m_active];
if (p.mode==1 && !p.items.empty()) {
int cleared=0;
for (size_t i=0;i<p.items.size(); ++i) {
if (p.items[i].marked) {
p.items[i].marked=false;
++cleared;
}
}
app.SetStatus(cleared ? "Cleared %d" : "No marks", cleared);
}
break;
}
// ---- Format cache (X/Y/Z + clear E:\CACHE) --------------------------------
case ACT_FORMAT_CACHE:
{
app.SetStatus("Formatting cache partitions (X/Y/Z)...");
const bool ok = FormatCacheXYZ(0, true); // 0 => default 16KiB; also clears E:\CACHE
if (!ok) { app.SetStatus("Format cache failed"); break; }
// If either pane is browsing X:, Y:, or Z:, refresh it
for (int p = 0; p < 2; ++p) {
Pane& pane = app.m_pane[p];
if (pane.mode == 1 && pane.curPath[0]) {
char dl = (char)toupper((unsigned char)pane.curPath[0]);
if (dl == 'X' || dl == 'Y' || dl == 'Z') {
ListDirectory(pane.curPath, pane.items);
}
}
}
app.RefreshPane(app.m_pane[0]);
app.RefreshPane(app.m_pane[1]);
app.SetStatus("Formatted X/Y/Z and E:\\CACHE");
break;
}
// ---- Switch active pane ---------------------------------------------------
case ACT_SWITCHMEDIA:
{
app.m_active = 1 - app.m_active;
break;
}
// ---- Apply ips patch ------------------------------------------------------
case ACT_APPLYIPS:
if (ext && _stricmp(ext, "ips") == 0 && ext2 && _stricmp(ext2, "xbe") == 0) {
bool bkcr = false;
bool bkcrfail = false;
bool bkcrcanc = false;
bool ptchfail = false;
char dst[1024];
strcpy(dst, dstFull);
strcat(dst, ".bak");
while (true) {
// Resolve destination
char dstDir[512];
app.ResolveDestDir(dstDir, sizeof(dstDir));
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)) {
bkcrfail = true;
break;
}
ULONGLONG total = DirSizeRecursiveA(dstFull);
// --- preflight free-space check on destination ---
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(dstDir, freeB, totB);
if (total > freeB) {
bkcrfail = true;
break;
}
// --- end preflight ---
ULONGLONG base = 0; // cumulative bytes completed
char dstName[47];
strcpy(dstName, sel2->name);
strcat(dstName, ".bak");
// Begin progress + set callback
app.BeginProgress(total, dstName, "Creaking bak...");
CopyProgCtx ctx = { &app, 0, false, false, 0, false };
SetCopyProgressCallback(CopyProgThunk, &ctx);
int cb = CreateBakWithProgress(dstFull, false, base, total);
switch (cb) {
case E_NO_ERROR:
bkcr = true;
bkcrfail = false;
break;
case E_CANNOT_OVR:
bkcr = false;
bkcrfail = false;
break;
default:
bkcr = false;
bkcrfail = true;
}
// End progress and clear callback
SetCopyProgressCallback(NULL, NULL);
app.EndProgress();
if (bkcrfail) break;
if (ctx.canceled) {
bkcrcanc = true;
break;
}
if (ApplyIPS(srcFull, dstFull) != E_NO_ERROR) ptchfail = true;
break;
}//while
//Give result
if (bkcrcanc) {
remove(dst);
app.SetStatus("Bak canceled, patch skipped");
}
else if (bkcrfail) {
app.SetStatus("Bak failed, patch skipped");
}
else if (ptchfail) {
if (bkcr) {
if (RestoreBak(dst, true) == E_NO_ERROR) app.SetStatus("Patch failed, bak restored");
else app.SetStatus("Patch failed, bak restore failed");
}
else app.SetStatus("Failed to apply patch");
}
else if (!bkcr) {
app.SetStatus("Patch applied, bak skipped");
}
else {
app.SetStatus("Patch applied, bak created");
}
}
app.RefreshPane(app.m_pane[0]);
app.RefreshPane(app.m_pane[1]);
break;
// ---- Restore .bak file ----------------------------------------------------
case ACT_RESTOREBAK:
{
if (ext && _stricmp(ext, "bak") == 0) {
if (RestoreBak(srcFull, true) == E_NO_ERROR) app.SetStatus("Bak restored");
else app.SetStatus("Restore failed");
}
app.RefreshPane(app.m_pane[0]);
app.RefreshPane(app.m_pane[1]);
break;
}
// ---- Unzip zip file -------------------------------------------------------
case ACT_UNZIPHERE:
case ACT_UNZIPTO:
{
if (ext && _stricmp(ext, "zip") == 0) {
// Resolve destination
char dstDir[512];
if (act == ACT_UNZIPHERE) {
if (!app.ResolveSrcDir(dstDir, sizeof(dstDir))) {
app.SetStatus("Pick a destination");
break;
}
if ((dstDir[0] == 'D' || dstDir[0] == 'd') && dstDir[1] == ':') {
app.SetStatus("Cannot extract to D:\\");
break;
}
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)) {
app.SetStatusLastErr("Dest not writable");
break;
}
}
else if (act == ACT_UNZIPTO) {
if (!app.ResolveDestDir(dstDir, sizeof(dstDir))) {
app.SetStatus("Pick a destination");
break;
}
if ((dstDir[0] == 'D' || dstDir[0] == 'd') && dstDir[1] == ':') {
app.SetStatus("Cannot extract to D:\\");
break;
}
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)) {
app.SetStatusLastErr("Dest not writable");
break;
}
}
UNZIP* zip = new UNZIP;
if (zip->openZIP(srcFull, ZipFile_Open, ZipFile_Close, ZipFile_Read, ZipFile_Seek) != UNZ_OK) {
zip->closeZIP();
app.SetStatus("Bad zip file");
break;
}
int rc = zip->gotoFirstFile();
unz_file_info fi;
ULONGLONG total = 0;
char szName[512];
// Compute total bytes for progress bar
while (rc == UNZ_OK) {
rc = zip->getFileInfo(&fi, szName, 512, NULL, 0, NULL, 0);
if (rc == UNZ_OK) {
total += fi.uncompressed_size;
rc = zip->gotoNextFile();
}
}
if (total == 0) {
app.SetStatus("Bad zip file");
break;
}
// --- preflight free-space check on destination ---
ULONGLONG freeB = 0, totB = 0;
GetDriveFreeTotal(dstDir, freeB, totB);
if (total > freeB) {
zip->closeZIP();
char need[64], have[64];
FormatSize(total, need, sizeof(need));
FormatSize(freeB, have, sizeof(have));
app.SetStatus("Not enough space: need %s, have %s", need, have);
break;
}
// --- end preflight ---
if ((rc = zip->gotoFirstFile()) == UNZ_OK) {
rc = zip->getFileInfo(&fi, szName, 512, NULL, 0, NULL, 0);
}
// Begin progress + set callback
app.BeginProgress(total, szName, "Extracting...");
CopyProgCtx ctx = { &app, 0, false, false, 0, false };
SetCopyProgressCallback(CopyProgThunk, &ctx);
ULONGLONG base = 0; // cumulative bytes completed
size_t extractedOk = 0, skipped = 0;
while (rc == UNZ_OK) {
if (ctx.canceled) break;
if ((rc = ExtractCurrentFileWithProgress(zip, dstDir, true, base, total)) != UNZ_OK) skipped += 1;
else extractedOk += 1;
if ((rc = zip->gotoNextFile()) == UNZ_OK) {
rc = zip->getFileInfo(&fi, szName, 512, NULL, 0, NULL, 0);
}
if (CopyProgress::g_copyProgFn) {
if (!CopyProgress::g_copyProgFn(base, total, szName, CopyProgress::g_copyProgUser)) {
break; // canceled
}
}
} //end while
FreeUnZipBuffer();
// End progress and clear callback
SetCopyProgressCallback(NULL, NULL);
app.EndProgress();
if (ctx.canceled) {
while (rc == UNZ_OK) {
++skipped;
rc = zip->gotoNextFile();
}
app.SetStatus("Extraction canceled (%u extracted, %u skipped)", (unsigned)extractedOk, (unsigned)skipped);
}
else {
// Final toast that reflects what actually happened
app.SetStatus("%u extracted, %u skipped", (unsigned)extractedOk, (unsigned)skipped);
}
zip->closeZIP();
}
app.RefreshPane(app.m_pane[0]);
app.RefreshPane(app.m_pane[1]);
break;
}
} // switch
}
} // namespace AppActions