From 049b364ac505989cb47e57c4e0c70dab84164c1b Mon Sep 17 00:00:00 2001 From: uncor3 Date: Mon, 23 Mar 2026 11:59:30 -0700 Subject: [PATCH] refactor(cleanup) : finish statusballoon impl, remove dead code , visual improvements --- resources.qrc | 1 + .../icons/MaterialSymbolsCloseRounded.png | Bin 0 -> 9029 bytes src/afcexplorerwidget.cpp | 17 +- src/afcexplorerwidget.h | 2 - src/airplaywidget.cpp | 2 +- src/appcontext.cpp | 13 +- src/appswidget.cpp | 4 - src/base/tool.cpp | 24 +- src/base/tool.h | 2 +- src/core/services/init_device.cpp | 6 - src/devicemenuwidget.cpp | 7 + src/diskusagewidget.cpp | 5 +- src/exportalbum.cpp | 5 +- src/exportmanager.cpp | 51 +- src/exportmanager.h | 20 +- src/exportmanagerthread.cpp | 30 +- src/exportprogressdialog.cpp | 487 ------------------ src/exportprogressdialog.h | 101 ---- src/fileexplorerwidget.cpp | 41 +- src/fileexplorerwidget.h | 22 + src/gallerywidget.cpp | 15 +- src/gallerywidget.h | 2 - src/heartbeat.h | 21 +- src/iDescriptor.h | 1 + src/installedappswidget.cpp | 33 +- src/installedappswidget.h | 14 + src/livescreenwidget.cpp | 2 +- src/mainwindow.cpp | 6 +- src/qballoontip.cpp | 44 +- src/sponsorwidget.cpp | 1 + src/sshterminaltool.cpp | 1 + src/statusballoon.cpp | 481 ++++++++--------- src/statusballoon.h | 75 +-- src/ztabwidget.cpp | 25 +- 34 files changed, 530 insertions(+), 1031 deletions(-) create mode 100644 resources/icons/MaterialSymbolsCloseRounded.png delete mode 100644 src/exportprogressdialog.cpp delete mode 100644 src/exportprogressdialog.h diff --git a/resources.qrc b/resources.qrc index 241b05b..4b2c392 100644 --- a/resources.qrc +++ b/resources.qrc @@ -39,6 +39,7 @@ resources/icons/UimProcess.png resources/icons/LetsIconsHorizontalDownLeftMainLight.png resources/icons/MaterialSymbolsDelete.png + resources/icons/MaterialSymbolsCloseRounded.png qml/MapView.qml resources/iphone.png resources/ios-wallpapers/iphone-ios4.png diff --git a/resources/icons/MaterialSymbolsCloseRounded.png b/resources/icons/MaterialSymbolsCloseRounded.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3567544c6ac121eaa5e8ffac4cc5b7754a66ba GIT binary patch literal 9029 zcmeG?`#;nB|J&PKCgrk-2$9JYatXP;$w?~gWJM_y33H21gqp>vkW|`nXVsB)=kqzqL|3QP((9#BDAa1_ z-H!WFC^)=?rckqRd+VA9mD(g}E1paV5;prUU;({W8XDJl=QV}XGlK~KGJNC`XiP%SR>?#L-b^t@bZnGN#HgTJ|zB@yesVx)YBRB(#*Vwh3BjU`1?{$bJpCPW%ISUC`)$XT-mtBBIl$f<;GmQCFQ#NPC1MuHBtQV6A`JW zCXDs{M@Y}YMB^7hoUI_}AHWIM)%J2~n1i^$UxZecl-xO*evjRU5gs>$vY(>~`%^Xt4u^u4`GIQ>mO&&aq|X3)O*=W?bbY&C7brzK7lz8g@!&f z>+c_$`@+lNvAp}{lWNQ@q~x+l9GuEPZP0@NdKgq|+}__g@Z`e5>>N+81*miNnf8DPuARgGB!(I0a;t4HrG z)KIMGkvpX1Vj5xYA2k;X9tq3p%coRAc@c7&Mhxa9+(J1JRsKj^Io`MLx%S`g;)HO! z5&`@EVoh|lO$%U=0NL&9GEZIXD$hwk>!x3G!!a5c{~n`M`TX)XEZ*{1n|*JGYkF=@q9h4CMvJIkVi=Fj6v_ zGo>wqdtXv)*SIiIxZRg8ts;#K`th3&HHW&LlGE&~XRb;vZ)g8thisZE!PDoQH#P1A zl76Wd)S9v8Z`RZT%YXFbq$W?Lwbl&ykf9v~jt*A)=(Bf6QbpbBujZA2VK%Iax2t@= zWpJc97`)%@z&c8wz4&Vig|&BRWK0vL`%&4OYDQLxRL5UqPFrUu)#Sq5=h}**ngG_y zdv|I&zRX-{;Men|*pH_ju;g{rPIo7Lp*hgP4`^|E^USA~(^W*LrN6KrBOI_nQPfVH z(^g@qTTt8;KLs?UBk$|IzmBmb%)hI@ZsWw@>F6vnVL9r;Xdr~&eS71u0wdmh zCqFzf=U95sc!VP?W;9?L)4S=}OU%9-yPpYocCT`Z1huDo9<*oW$oHYT%vK9cUPhRr zaXL{iZ;{$;-1*@_c|qXpDY>j&921r2fs`fDtr$`MwzsA##@7yDhVnwA7H%!t3{8fn zBmTI>Y{$J0RPEH^15@UOI6QKv&pyA>+Fbg!rX6twiL;RW<;LwDLuNS%uDW>@VOY}R z+=qhNrdIm|xE-?F9dbu;Vej}`U}t{PJexu?BlE+_)l*wRTI^5~>_6U6O}`qGkEf46 zuZ;+#RrVfPM#B3aFE*M~{xF!l-1_5HmKl138&iB|$(6J7W9*-CaD>y4%zJfw$LHU2 z5_)t;wMGxLS;q0h;~am++T#|Br9!CM#TjSB^v|HRAwe`hZuiK_!7l)wpo9otY}=d->6?^}^<8B<3Sc<2d3Wr1mo z=*)J7xfhm(TmRJdDaI;CTv($VsWWi^gqhWlItVjgB6YrSIz%Mvt-WqxuipShW{0yR zwn1T%(AqquBCKjo+_&$TLY*5&buogLwig${-Z%3Oq@{UT^~IrrQ9wX}+G%lch&dFq z0aGK#ux=T8=kUrzO3uQCJ{zF=E@;`?nhxs(kOP=4*lZNld==@u|IWCo)Izx!Z~Ckl znR`}|jng!$hIL|9S$%qImwO=Eq?fjpcAPaDnv^1WlM}7lN4%Dl7)d8XzvgSdl-G>*jhHG!%^`*;b6cFB+fJ2T@GkdjIArKOLA8swk#fm|XE7 z!K@;3J}sX)2GRIrNU0E$x`sXMW6HnilEC}|(R_j-?}eF?MRCGT)5*!+DtQIX2rA57 zuR119G*I!T8$(g&1g7gj@8~5>QV;MO-v6d57RSK&UtRb^kq?6KcUVVS)qmYRxt2Dd zK{iYEPOKUlio%jE`rXOXFD^-M%kw@bp=^hS)~zCyy}=ym;y%49PN>X&u|aotAV%t| zE)n~@?VXResyFW@0Y3QM#33JS-o#i3)@>7CV7@~Q0zo#nTNf72cj*;ZAJ!NR9 z^~j4pd)+dwHr)-}<`I=JcaQ4l)*}YiztU&JqQ13YV$9Ic9FW4_FW<1-B&|a&UoW@K zAp!P)Xo05aqeYHJy%>T|x65lP(9KGkR1kXSz5L)EjjhD`XL<3axvGHdOboSi>-ORH zhI+p^YNvL0>(o)3>i|7knq*taA9+5WD25^Mj3i|f=A}VzZEZ+gq^=eK&uVNWo=vWg zPO6RujW=fvwX-+0C7L7P8$h(;oT!Gr*v+5~Ro5lf4_{oiF!8W+p-1M{&zo9B%Y(>5 z3R-9vK>BDx?evK1It_Yv_!30R3hCQ`54doxs}y@9Pzoc}wUww+fcKqN)3DhQ^*Qhq zPa95P;=~CHK~TZv$r>++#_KBb+VprMIL?M>Ga7GV*SUy_*vXjMc_Js_#r=S$PPHtvsI+RPFCwtL;)HdRxA z7hx@WmadpT=c{}kaRzv33}IU^B(eJq&fz9YmtOxnz(ew;1e>Kl9=XHAHtiySG+;>u z_pyU%3w@F=xVMJAcY%|9kRpAUEBQ{AYF3ony1>>!5Y3znxsEq~ZjFpqEX)PQgXs=* ztTI`>$CNJas{A696J~HfB;~4R8F=|#DvNXO-g(4GR#3h(JnUi1@ zv0b?BOPamSRiH7Z496HQ!`Ai8+-gv<+w>s{oRqWpg+6=Gg=zaWWZ~2|kbP2egS+Xo zYA(!OS-tAdPJ6kzfsjK{^D%_px0sN`7m=z?ZYv5Vg0y&vIdZ9cW`|ug&@O@L4AB;X zp@NIO(?Ma=TF5C^4B=EDq@F1gkUkd)k`-Lt1Y$BMwTB`-k4}6bAncSK=?cYoj?jPG zBX)Z+BD)a-)(LZAdg-Qjdo>93kYoF_h$@%xoHNGR!}Bv6Rv-kTN+zE3_QQjiw@*Z% znj|o_Alj+25sZJ;vUk%3Ty)41w->Ic*SLsD<*yj(If+*c?6GqlCgxS>uXN!%rHZ< z%+V>LF!%n%6_`G?Gtm^yG6S%KCa+{nlJ>}e$0^(0E_E(>;+{AMSTYP(l;3EVFb_3#c&e> zYZ50I_F;zKBY3@VFUVjN^)-eN-j8Xl2ljKzz2UtNv7Zwt8-HSBngA?4=j_N%1XjF? zq}7LM4DQ6>8^hk1%Mthmw6SjE=x&Kyxptza3~WXYynLfmGzdCtCCoJtN(R z6XMp!+-j${I3nc!p%FmsP+y7S!pxa_&p-^O3DKgs zW98TinHA2K)XvL_6wyXH7ylTIRE`sXjG|~Dwb>5qdDcwX3XoTlXIydTQ=<3-m@N-b zG8urSzBEaqRKPyEY=+h^llH@d`1}A`0zIMZI>770swXoD^BeGqiWCvI)dyGc>a)>7 zAm~0Mm&QZ_AVAh>du1g+{h%B}WPE@%3AGf_JfLp$9NH!WsCR89s@xoF_5q~2>VmH%15$~rNMjz$ z3x+~4$*fD60TwXIfKM2uh)yBrUVq-E1kTBTQsKq7dn_PB5=V`mBHGA6H2clMNrazF zNry7RFALW3ZWS#9(Un6+r7{6=?Ny`;9?MI5fcSNfj6sC>Wmw0xwYeEV@<(G4Q)x?+ zE*xB5+6s{5J!n|~sjnW^v2AS@0i=)d8?osK5|(t~z;gQvrGuHC2+}=R$Dozch#;M| z$EG4kQltx{<@Oay2eXwCq-U@;$g!glFle|7c@+AZVUMy79z;RFTUgmEqljJxFNnl_Kq` zkL5Qzo2IH_eE>(;H<;S_ajl4@7ZAyM7LCN1Q8s+SIz{BAOZ#sTl`$YD`|z9XlCMlJ z@K?&J44#2LoQ zIpC!3h;%Na7=paM@LAE+L1tu3>N9i1t~1wQF4QX8ShUSH^TmG%<^k)&d^?-fxe}Vi zxskETK(YxOL&lk3XjmTVNOKR%O81ZNHTweXe>q17a6B| zuQI3SH!Xnj6;&{IF-6qVk#FPOW3~5k1mGJ{4Re1_5xx9;uF-9JsUvYS0;_|$X(^)7 zzmGQcseL4DxPTJDV|>ZudG#rPnGJKDQ$zuI>ZfnR-1if+ z$=I1-2-7CdND6+Yrt711&XXRQZ26-iLmw0)PrC6LRqj)q@7S$e7EX7d4|oS;V#63E z|5n4wBztw?pGmd?=APT$miV%18red1e{=uXbQ47#!4S~zzA=FZDhSUO%VLh~duy-H>AQR~d8ENIu zTBJTjfR>{4_h;{#uA!(V7=q!$(fp0P{3dA)Q{5OTi2ZH; z&Sq*5##@mQU-=|^HD(&Li2iBAQ{y-gdTSXiA%3bxryOjL&}Vm%xe;eTSK%v5?c5m} z7}pUyaUY^l51CX24Ae<&0S#$!TF7;O)rq@c;5%fMdjrW* zwLUoRBG_*}5x30{h_dHs;ypF?9`kEy4!I?P6IWdpW{PWy_C9*H%g&M!La@ zy2r7^6pgIyE`!5p@$)yc6h&y9?uRmQ!pqWEnEx&W_co@8e4TOAMi6bcM-VjRjqE+- zs0%thY;Il?I#4x&ds@z7Cw=ynVopL$X0;7i3o%g;+vQ4%2$ifbHUrV*@Am2yt%T|R+slR)u@cwpzS?)iVc7{4N*_4HHLA3Rv(wb7^`FFVqKlg!>$)Wuc zFrODQhMj7rXq=e9(|@j*&K=LKL=%^-3Qjx3fyGJ5p`WKnoblwne4eT0)5E*y#;*|g z%1Vn!|50ppl8jpJX?~WB|5H+&;L$aR5%h}LWqm(o5OXY#_)9F6DLN9-y*qPR_lYpP zCv%td;rke^nf*Q6qgiSNpRz*Jv-Ql=)l(%nFa3|QjofV&MPd0E0N{W93>s+V; zG4P*lI2|&?Pd9U|?|>FvGT-#+v;$qC^$<|jWM~B0%|%el@j?pM`T;gj)NadG8+H+3 zdXxgmjM5_QfiRZ-&kbI=zZ#|cY|F0M^v20A8FeZW)GG0z!@=awmmjj`FV}d0C2H0_ z@Jm8(Z`5@C^=0E;(Fgyqi#s_X)SA0PLqkzhH9vnuP>gYEdn@$hbMw5y@RY8($N6(d zEjM7KLJa%*4|@59mWI}!`QOhIo-pT!%6N37aQmcL?sJVu(L&^TqhgTFKLC3&xvaE_ z+#lw(6e=4SNA z`v4H|6CdIY+R>^?SvBYFa_5>Xc{_v!vgdPF04%%xfs`kGm5c-l-W$n^(Lw%&#rYRk$1(@s3;zc&QzhyE literal 0 HcmV?d00001 diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index f261dd9..cdc976c 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -281,9 +281,8 @@ void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos) ExportItem(devicePath, fileName, m_device->udid)); } - // Start export with singleton - manager will show its own dialog - ExportManager::sharedInstance()->startExport(m_device, exportItems, dir, - m_afc); + ExportManager::sharedInstance()->startExport( + m_device, exportItems, dir, "Exporting from file Explorer", m_afc); } else if (selectedAction == openAction) { onItemDoubleClicked(item); } else if (selectedAction == openNativeAction) { @@ -328,8 +327,8 @@ void AfcExplorerWidget::onExportClicked() } // Start export - ExportManager::sharedInstance()->startExport(m_device, exportItems, dir, - m_afc); + ExportManager::sharedInstance()->startExport( + m_device, exportItems, dir, "Exporting from file Explorer", m_afc); } void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item, @@ -363,8 +362,8 @@ void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item, "Failed to export file for opening."); } })); - ExportManager::sharedInstance()->startExport(m_device, exportItems, - directory, m_afc); + ExportManager::sharedInstance()->startExport( + m_device, exportItems, directory, "Exporting to open file", m_afc); } // FIXME: should be disabled if there is an error loading afc @@ -395,8 +394,8 @@ void AfcExplorerWidget::onImportClicked() } })); } - ExportManager::sharedInstance()->startImport(m_device, importItems, - currPath, m_afc); + ExportManager::sharedInstance()->startImport( + m_device, importItems, currPath, "Importing Files", m_afc); } void AfcExplorerWidget::setupFileExplorer() diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h index 4516d80..3c050e4 100644 --- a/src/afcexplorerwidget.h +++ b/src/afcexplorerwidget.h @@ -40,7 +40,6 @@ #include class ExportManager; -class ExportProgressDialog; class AfcExplorerWidget : public QWidget { @@ -96,7 +95,6 @@ private: // Export system ExportManager *m_exportManager; - ExportProgressDialog *m_exportProgressDialog; void setupFileExplorer(); void loadPath(const QString &path); diff --git a/src/airplaywidget.cpp b/src/airplaywidget.cpp index 6f8f1ad..819f5b4 100644 --- a/src/airplaywidget.cpp +++ b/src/airplaywidget.cpp @@ -138,7 +138,7 @@ AirPlaySettings AirPlaySettingsDialog::getSettings() const } AirPlayWidget::AirPlayWidget(QWidget *parent) - : Tool(parent), m_stackedWidget(nullptr), m_tutorialWidget(nullptr), + : Tool(parent, false), m_stackedWidget(nullptr), m_tutorialWidget(nullptr), m_streamingWidget(nullptr), m_loadingIndicator(nullptr), m_loadingLabel(nullptr), m_tutorialPlayer(nullptr), m_tutorialVideoWidget(nullptr), m_videoLabel(nullptr), diff --git a/src/appcontext.cpp b/src/appcontext.cpp index f133556..d16cbd5 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -260,18 +260,9 @@ void AppContext::addDevice(iDescriptor::Uniq uniq, .diagRelay = initResult->diagRelay, .heartbeatThread = initResult->heartbeatThread}; - /* - sometimes heartbeat thread can fail before we even add - the device, in that case we should not add the device at all - this seems to happen on Windows for some reason - */ if (initResult->deviceInfo.isWireless && - initResult->heartbeatThread && - initResult->heartbeatThread->exited()) { - qDebug() << "Heartbeat thread already exited for device" - << uniq << " Not adding device."; - freeDevice(device); - return; + initResult->heartbeatThread) { + device->heartbeatThread->start(); } m_devices[device->udid] = device; diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 0501f1a..9d48181 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -98,11 +98,7 @@ void AppsWidget::setupUI() m_loginButton = new QPushButton(); m_searchEdit = new QLineEdit(); -#ifndef WIN32 - m_searchEdit->setMaximumWidth(350); -#else m_searchEdit->setMaximumWidth(200); -#endif // --- Status and Login Button --- m_manager = AppStoreManager::sharedInstance(); diff --git a/src/base/tool.cpp b/src/base/tool.cpp index 056077a..a542c81 100644 --- a/src/base/tool.cpp +++ b/src/base/tool.cpp @@ -23,14 +23,32 @@ #include "../platform/windows/win_common.h" #endif -Tool::Tool(QWidget *parent) : QWidget(parent) +Tool::Tool(QWidget *parent, bool noMaximize) : QWidget(parent) { #ifdef __APPLE__ setupToolFrame(this); + if (noMaximize) { + setWindowFlags(Qt::Window | Qt::WindowTitleHint | + Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | + Qt::WindowMinimizeButtonHint); + } else { + setWindowFlags(Qt::Window | Qt::WindowTitleHint | + Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | + Qt::WindowMinimizeButtonHint | + Qt::WindowMaximizeButtonHint); + } #elif defined(WIN32) setupWinWindow(this); - setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | - Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); + if (noMaximize) { + setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinimizeButtonHint | + Qt::WindowCloseButtonHint); + } else { + setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinimizeButtonHint | + Qt::WindowCloseButtonHint | + Qt::WindowMaximizeButtonHint); + } #else #endif diff --git a/src/base/tool.h b/src/base/tool.h index 491ab89..85f7e5c 100644 --- a/src/base/tool.h +++ b/src/base/tool.h @@ -33,7 +33,7 @@ class Tool : public QWidget { public: - explicit Tool(QWidget *parent = nullptr); + explicit Tool(QWidget *parent = nullptr, bool noMaximize = true); }; #endif // TOOL_H \ No newline at end of file diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 5f7fe61..a1c8a1d 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -450,12 +450,6 @@ void init_idescriptor_device(const iDescriptor::Uniq &uniq, } heartbeatThread = new HeartbeatThread(heartbeat, uniq); - heartbeatThread->start(); - - while (!heartbeatThread->initialCompleted()) { - // sleep(1); - } - } else { UsbmuxdDeviceHandle **devices; diff --git a/src/devicemenuwidget.cpp b/src/devicemenuwidget.cpp index 1c180fa..374c5de 100644 --- a/src/devicemenuwidget.cpp +++ b/src/devicemenuwidget.cpp @@ -85,6 +85,13 @@ void DeviceMenuWidget::init() if (stackedWidget->widget(index) == m_galleryWidget) { // Gallery tab m_galleryWidget->load(); + } else if (stackedWidget->widget(index) == + m_fileExplorerWidget) { // Files tab + QTimer::singleShot( + 200, this, [this]() { m_fileExplorerWidget->init(); }); + } else if (stackedWidget->widget(index) == + m_installedAppsWidget) { // Apps tab + m_installedAppsWidget->init(); } }); diff --git a/src/diskusagewidget.cpp b/src/diskusagewidget.cpp index 4379dc2..a7a4f29 100644 --- a/src/diskusagewidget.cpp +++ b/src/diskusagewidget.cpp @@ -41,7 +41,7 @@ DiskUsageWidget::DiskUsageWidget(const iDescriptorDevice *device, { setMinimumHeight(80); setupUI(); - fetchData(); + QTimer::singleShot(100, this, &DiskUsageWidget::fetchData); } void DiskUsageWidget::setupUI() @@ -516,6 +516,9 @@ void DiskUsageWidget::fetchData() return result; } + result["galleryUsage"] = QVariant::fromValue(uint64_t(0)); + return result; + const size_t CHUNK_SIZE = 256 * 1024; uint8_t *db_data = nullptr; size_t db_size = 0; diff --git a/src/exportalbum.cpp b/src/exportalbum.cpp index 95d723f..a41778d 100644 --- a/src/exportalbum.cpp +++ b/src/exportalbum.cpp @@ -149,10 +149,9 @@ void ExportAlbum::updateInfoLabel(size_t photoCount) void ExportAlbum::startExport() { - // qDebug() << "Starting export of selected files:" << exportItems.size() - // << "items to" << exportDir; ExportManager::sharedInstance()->startExport( - m_device, m_exportItems, m_dirPickerLabel->getOutputDir()); + m_device, m_exportItems, m_dirPickerLabel->getOutputDir(), + "Exporting Album(s)"); } void ExportAlbum::calculateTotalExportSize() diff --git a/src/exportmanager.cpp b/src/exportmanager.cpp index d242549..6cec2ab 100644 --- a/src/exportmanager.cpp +++ b/src/exportmanager.cpp @@ -20,13 +20,6 @@ #include "exportmanager.h" #include "servicemanager.h" #include "statusballoon.h" -#include -#include -#include -#include -#include -#include -#include ExportManager *ExportManager::sharedInstance() { @@ -37,6 +30,7 @@ ExportManager *ExportManager::sharedInstance() ExportManager::ExportManager(QObject *parent) : QObject(parent) {} +// FIXME ExportManager::~ExportManager() { // Cancel all active jobs @@ -52,21 +46,25 @@ ExportManager::~ExportManager() m_activeJobs.clear(); } -// FIXME: show error on ui QUuid ExportManager::startExport(const iDescriptorDevice *device, const QList &items, const QString &destinationPath, + const QString &exportTitle, std::optional altAfc) { qDebug() << "startExport() entry - items:" << items.size() << "dest:" << destinationPath; if (!device) { qWarning() << "Invalid device provided to ExportManager"; + QMessageBox::critical(nullptr, "Export Error", + "Invalid device specified for export."); return QUuid(); } if (items.isEmpty()) { qWarning() << "No items provided for export"; + QMessageBox::information(nullptr, "Export Error", + "No items selected for export."); return QUuid(); } @@ -76,6 +74,9 @@ QUuid ExportManager::startExport(const iDescriptorDevice *device, if (!destDir.mkpath(".")) { qWarning() << "Could not create destination directory:" << destinationPath; + QMessageBox::critical(nullptr, "Export Error", + "Could not create destination directory."); + return QUuid(); } } @@ -89,11 +90,8 @@ QUuid ExportManager::startExport(const iDescriptorDevice *device, job->d_udid = device->udid; job->statusBalloonProcessId = StatusBalloon::sharedInstance()->startProcess( - QString("Exporting %1 item(s)").arg(items.size()), items.size(), - destinationPath, ProcessType::Export); - - // Use ExportManager's own jobId for its internal tracking and signals - const QUuid managerJobId = job->jobId; + exportTitle, items.size(), destinationPath, ProcessType::Export, + job->jobId); // todo:cleanupJob ? // connect(job->watcher, &QFutureWatcher::finished, this, @@ -102,30 +100,34 @@ QUuid ExportManager::startExport(const iDescriptorDevice *device, // Store job before starting { QMutexLocker locker(&m_jobsMutex); - m_activeJobs[managerJobId] = job; + m_activeJobs[job->jobId] = job; } m_exportThread->executeExportJob(job); - qDebug() << "Started export job" << managerJobId << "for" << items.size() + qDebug() << "Started export job" << job->jobId << "for" << items.size() << "items"; - return managerJobId; + return job->jobId; } -// FIXME: show error on ui QUuid ExportManager::startImport(const iDescriptorDevice *device, const QList &items, const QString &destinationPath, + const QString &importTitle, std::optional altAfc) { qDebug() << "startExport() entry - items:" << items.size() << "dest:" << destinationPath; if (!device) { qWarning() << "Invalid device provided to ExportManager"; + QMessageBox::critical(nullptr, "Import Error", + "Invalid device specified for import."); return QUuid(); } if (items.isEmpty()) { qWarning() << "No items provided for export"; + QMessageBox::information(nullptr, "Import Error", + "No items selected for import."); return QUuid(); } @@ -138,11 +140,8 @@ QUuid ExportManager::startImport(const iDescriptorDevice *device, job->d_udid = device->udid; job->statusBalloonProcessId = StatusBalloon::sharedInstance()->startProcess( - QString("Importing %1 item(s)").arg(items.size()), items.size(), - destinationPath, ProcessType::Import); - - // Use ExportManager's own jobId for its internal tracking and signals - const QUuid managerJobId = job->jobId; + importTitle, items.size(), destinationPath, ProcessType::Import, + job->jobId); // todo:cleanupJob ? // connect(job->watcher, &QFutureWatcher::finished, this, @@ -151,16 +150,16 @@ QUuid ExportManager::startImport(const iDescriptorDevice *device, // Store job before starting { QMutexLocker locker(&m_jobsMutex); - m_activeJobs[managerJobId] = job; + m_activeJobs[job->jobId] = job; } m_exportThread->executeImportJob(job); - qDebug() << "Started import job" << managerJobId << "for" << items.size() + qDebug() << "Started import job" << job->jobId << "for" << items.size() << "items"; - return managerJobId; + return job->jobId; } -void ExportManager::cancelExport(const QUuid &jobId) +void ExportManager::cancel(const QUuid &jobId) { QMutexLocker locker(&m_jobsMutex); auto it = m_activeJobs.find(jobId); diff --git a/src/exportmanager.h b/src/exportmanager.h index 46e0475..ce76d46 100644 --- a/src/exportmanager.h +++ b/src/exportmanager.h @@ -22,20 +22,25 @@ #include "exportmanagerthread.h" #include "iDescriptor.h" +#include +#include +#include +#include #include #include #include +#include #include +#include #include +#include #include #include +#include #include #include #include -// Forward declaration -class ExportProgressDialog; - class ExportManager : public QObject { Q_OBJECT @@ -50,15 +55,15 @@ public: QUuid startExport(const iDescriptorDevice *device, const QList &items, - const QString &destinationPath, + const QString &destinationPath, const QString &jobTitle, std::optional altAfc = std::nullopt); QUuid startImport(const iDescriptorDevice *device, const QList &items, - const QString &destinationPath, + const QString &destinationPath, const QString &jobTitle, std::optional altAfc = std::nullopt); - void cancelExport(const QUuid &jobId); + void cancel(const QUuid &jobId); void cancelAllJobs(); bool isJobRunning(const QUuid &jobId) const; static QString generateUniqueOutputPath(const QString &basePath); @@ -96,9 +101,6 @@ private: // Thread-safe storage for active jobs mutable QMutex m_jobsMutex; QMap m_activeJobs; - - // Manager owns the dialog - ExportProgressDialog *m_exportProgressDialog; }; #endif // EXPORTMANAGER_H \ No newline at end of file diff --git a/src/exportmanagerthread.cpp b/src/exportmanagerthread.cpp index 0c6d51c..e07e60c 100644 --- a/src/exportmanagerthread.cpp +++ b/src/exportmanagerthread.cpp @@ -38,14 +38,9 @@ void ExportManagerThread::executeExportJobInternal(ExportJob *job) << job->items.size() << "items"; for (int i = 0; i < job->items.size(); ++i) { - if (job->cancelRequested.load() || - StatusBalloon::sharedInstance()->isCancelRequested( - job->statusBalloonProcessId)) { + if (job->cancelRequested.load()) { summary.wasCancelled = true; - qDebug() << "Export job" << job->jobId << "was cancelled"; - - emit exportCancelled(job->jobId); - return; + goto exit; } const ExportItem &item = job->items.at(i); @@ -60,15 +55,21 @@ void ExportManagerThread::executeExportJobInternal(ExportJob *job) summary.failedItems++; } + if (result.cancelled) { + summary.wasCancelled = true; + goto exit; + } + emit itemExported(job->statusBalloonProcessId, result); } +exit: qDebug() << "Export job" << job->jobId << "completed - Success:" << summary.successfulItems << "Failed:" << summary.failedItems << "Bytes:" << summary.totalBytesTransferred; - emit exportFinished(job->jobId, summary); + emit exportFinished(job->statusBalloonProcessId, summary); } ExportResult ExportManagerThread::exportSingleItem( @@ -76,7 +77,7 @@ ExportResult ExportManagerThread::exportSingleItem( std::optional altAfc, std::atomic &cancelRequested, const QUuid &statusBalloonProcessId) { - ExportResult result; + ExportResult result; // result.sourceFilePath = item.sourcePathOnDevice; // Generate output path @@ -119,7 +120,12 @@ ExportResult ExportManagerThread::exportSingleItem( result.errorMessage = QString("Failed to export file: %1").arg(err->message); qDebug() << result.errorMessage; - idevice_error_free(err); + if (cancelRequested.load()) { + result.cancelled = true; + } + // FIXME: THIS WILL FREE C STRING FROM RUST ?? + // because error may not be returned from rust, but from C++ wrapper + // idevice_error_free(err); return result; } @@ -161,9 +167,7 @@ void ExportManagerThread::executeImportJobInternal(ImportJob *job) << job->items.size() << "items"; for (int i = 0; i < job->items.size(); ++i) { - if (job->cancelRequested.load() || - StatusBalloon::sharedInstance()->isCancelRequested( - job->statusBalloonProcessId)) { + if (job->cancelRequested.load()) { summary.wasCancelled = true; qDebug() << "Import job" << job->jobId << "was cancelled"; diff --git a/src/exportprogressdialog.cpp b/src/exportprogressdialog.cpp deleted file mode 100644 index 9c3903b..0000000 --- a/src/exportprogressdialog.cpp +++ /dev/null @@ -1,487 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "exportprogressdialog.h" -#include "exportmanager.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -ExportProgressDialog::ExportProgressDialog(ExportManager *exportManager, - QWidget *parent) - : QDialog(parent), m_exportManager(exportManager), m_totalItems(0), - m_completedItems(0), m_totalBytesTransferred(0), - m_lastBytesTransferred(0), m_jobCompleted(false), m_jobCancelled(false) -{ - setupUI(); - - // Connect to export manager signals - connect(m_exportManager, &ExportManager::exportStarted, this, - &ExportProgressDialog::onExportStarted); - connect(m_exportManager, &ExportManager::exportProgress, this, - &ExportProgressDialog::onExportProgress); - connect(m_exportManager, &ExportManager::fileTransferProgress, this, - &ExportProgressDialog::onFileTransferProgress); - connect(m_exportManager, &ExportManager::itemExported, this, - &ExportProgressDialog::onItemExported); - connect(m_exportManager, &ExportManager::exportFinished, this, - &ExportProgressDialog::onExportFinished); - connect(m_exportManager, &ExportManager::exportCancelled, this, - &ExportProgressDialog::onExportCancelled); - - // Setup transfer rate timer - m_transferRateTimer = new QTimer(this); - m_transferRateTimer->setInterval(1000); // Update every second - connect(m_transferRateTimer, &QTimer::timeout, this, - &ExportProgressDialog::updateTransferRate); - - // FIXME:Listen for palette changes - // connect(qApp, &QApplication::paletteChanged, this, - // &ExportProgressDialog::updateColors); - - updateColors(); -} - -void ExportProgressDialog::setupUI() -{ - setWindowTitle("Exporting Files"); - setModal(true); - setFixedSize(480, 280); - setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); - - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setSpacing(16); - m_mainLayout->setContentsMargins(24, 24, 24, 24); - - // Title - m_titleLabel = new QLabel("Exporting Files"); - m_titleLabel->setAlignment(Qt::AlignCenter); - QFont titleFont = m_titleLabel->font(); - titleFont.setPointSize(titleFont.pointSize() + 4); - titleFont.setWeight(QFont::Medium); - m_titleLabel->setFont(titleFont); - m_mainLayout->addWidget(m_titleLabel); - - // Status label - m_statusLabel = new QLabel("Preparing export..."); - m_statusLabel->setAlignment(Qt::AlignCenter); - m_statusLabel->setWordWrap(true); - m_mainLayout->addWidget(m_statusLabel); - - // Current file label - m_currentFileLabel = new QLabel(); - m_currentFileLabel->setAlignment(Qt::AlignCenter); - m_currentFileLabel->setWordWrap(true); - QFont fileFont = m_currentFileLabel->font(); - fileFont.setWeight(QFont::Medium); - m_currentFileLabel->setFont(fileFont); - m_mainLayout->addWidget(m_currentFileLabel); - - // Progress bar - m_progressBar = new QProgressBar(); - m_progressBar->setRange(0, 100); - m_progressBar->setValue(0); - m_progressBar->setFixedHeight(8); - m_progressBar->setTextVisible(false); - m_mainLayout->addWidget(m_progressBar); - - // Stats label (items completed) - m_statsLabel = new QLabel("0 of 0 items"); - m_statsLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_statsLabel); - - // Transfer rate - m_transferRateLabel = new QLabel(); - m_transferRateLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_transferRateLabel); - - // Time remaining - m_timeRemainingLabel = new QLabel(); - m_timeRemainingLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_timeRemainingLabel); - - // Add stretch before buttons - m_mainLayout->addStretch(); - - // Buttons layout - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - - m_cancelButton = new QPushButton("Cancel"); - m_cancelButton->setMaximumWidth(m_cancelButton->sizeHint().width()); - connect(m_cancelButton, &QPushButton::clicked, this, - &ExportProgressDialog::onCancelClicked); - buttonLayout->addWidget(m_cancelButton); - - m_closeButton = new QPushButton("Close"); - m_closeButton->setMaximumWidth(m_closeButton->sizeHint().width()); - m_closeButton->setVisible(false); - connect(m_closeButton, &QPushButton::clicked, this, &QDialog::accept); - buttonLayout->addWidget(m_closeButton); - - m_openDirButton = new QPushButton("Show Files"); - m_openDirButton->setMaximumWidth(m_openDirButton->sizeHint().width()); - m_openDirButton->setVisible(false); - connect(m_openDirButton, &QPushButton::clicked, this, - &ExportProgressDialog::onOpenDirectoryClicked); - buttonLayout->addWidget(m_openDirButton); - - buttonLayout->addStretch(); - m_mainLayout->addLayout(buttonLayout); -} - -// TODO: -void ExportProgressDialog::updateColors() -{ - // QPalette palette = qApp->palette(); - - // Apply Apple-style colors - // QString dialogStyle = - // QString("QDialog {" - // " background-color: %1;" - // " border-radius: 12px;" - // "}" - // "QLabel {" - // " color: %2;" - // "}" - // "QProgressBar {" - // " border: none;" - // " border-radius: 4px;" - // " background-color: %3;" - // "}" - // "QProgressBar::chunk {" - // " background-color: %4;" - // " border-radius: 4px;" - // "}" - // "QPushButton {" - // " background-color: %5;" - // " color: %6;" - // " border: 1px solid %7;" - // " border-radius: 6px;" - // " padding: 6px 12px;" - // " font-weight: 500;" - // "}" - // "QPushButton:hover {" - // " background-color: %8;" - // "}" - // "QPushButton:pressed {" - // " background-color: %9;" - // "}") - // .arg(palette.color(QPalette::Base).name()) - // .arg(palette.color(QPalette::WindowText).name()) - // .arg(palette.color(QPalette::AlternateBase).name()) - // .arg(palette.color(QPalette::Highlight).name()) - // .arg(palette.color(QPalette::Button).name()) - // .arg(palette.color(QPalette::ButtonText).name()) - // .arg(palette.color(QPalette::Mid).name()) - // .arg(palette.color(QPalette::Button).lighter(110).name()) - // .arg(palette.color(QPalette::Button).darker(110).name()); - - // setStyleSheet(dialogStyle); - - // // Special styling for title - // m_titleLabel->setStyleSheet( - // QString("color: %1; font-weight: 600;") - // .arg(palette.color(QPalette::WindowText).name())); -} - -void ExportProgressDialog::showForJob(const QUuid &jobId) -{ - m_currentJobId = jobId; - m_jobCompleted = false; - m_jobCancelled = false; - m_totalBytesTransferred = 0; - m_lastBytesTransferred = 0; - m_completedItems = 0; - - // Reset UI - m_progressBar->setValue(0); - m_statusLabel->setText("Preparing export..."); - m_currentFileLabel->clear(); - m_statsLabel->setText("0 of 0 items"); - m_transferRateLabel->clear(); - m_timeRemainingLabel->clear(); - m_cancelButton->setVisible(true); - m_closeButton->setVisible(false); - m_openDirButton->setVisible(false); - - show(); - raise(); - activateWindow(); -} - -void ExportProgressDialog::onExportStarted(const QUuid &jobId, int totalItems, - const QString &destinationPath) -{ - if (jobId != m_currentJobId) - return; - - m_totalItems = totalItems; - m_completedItems = 0; - m_destinationPath = destinationPath; - m_startTime = QDateTime::currentDateTime(); - m_lastUpdateTime = m_startTime; - - m_statusLabel->setText(QString("Exporting %1 items to %2") - .arg(totalItems) - .arg(QFileInfo(destinationPath).baseName())); - m_statsLabel->setText(QString("0 of %1 items").arg(totalItems)); - - m_transferRateTimer->start(); -} - -void ExportProgressDialog::onExportProgress(const QUuid &jobId, int currentItem, - int totalItems, - const QString ¤tFileName) -{ - if (jobId != m_currentJobId) - return; - - // Update current file - m_currentFileLabel->setText(currentFileName); - - // Update stats - m_statsLabel->setText( - QString("%1 of %2 items").arg(currentItem).arg(totalItems)); -} - -void ExportProgressDialog::onFileTransferProgress(const QUuid &jobId, - const QString &fileName, - qint64 bytesTransferred, - qint64 totalFileSize) -{ - if (jobId != m_currentJobId) - return; - - // Update progress bar based on current file transfer - int progress = - totalFileSize > 0 ? (bytesTransferred * 100) / totalFileSize : 0; - m_progressBar->setValue(progress); - - // Update transfer info - QString transferInfo = QString("%1 / %2") - .arg(formatFileSize(bytesTransferred)) - .arg(formatFileSize(totalFileSize)); - m_transferRateLabel->setText(transferInfo); -} - -void ExportProgressDialog::onItemExported(const QUuid &jobId, - const ExportResult &result) -{ - if (jobId != m_currentJobId) - return; - - if (result.success) { - m_completedItems++; - m_totalBytesTransferred += result.bytesTransferred; - } -} - -void ExportProgressDialog::onExportFinished(const QUuid &jobId, - const ExportJobSummary &summary) -{ - if (jobId != m_currentJobId) - return; - - m_jobCompleted = true; - m_transferRateTimer->stop(); - - m_destinationPath = summary.destinationPath; - - m_progressBar->setValue(100); - m_currentFileLabel->clear(); - - QString message; - if (summary.failedItems == 0) { - message = QString("Successfully exported %1 items") - .arg(summary.successfulItems); - m_titleLabel->setText("Export Complete"); - } else { - message = QString("Exported %1 items (%2 failed)") - .arg(summary.successfulItems) - .arg(summary.failedItems); - m_titleLabel->setText("Export Completed with Errors"); - } - - m_statusLabel->setText(message); - m_transferRateLabel->setText( - QString("Total: %1") - .arg(formatFileSize(summary.totalBytesTransferred))); - m_timeRemainingLabel->clear(); - - // Show close button, hide cancel - m_cancelButton->setVisible(false); - m_closeButton->setVisible(true); - m_openDirButton->setVisible(true); - m_closeButton->setDefault(true); - m_closeButton->setFocus(); -} - -void ExportProgressDialog::onExportCancelled(const QUuid &jobId) -{ - if (jobId != m_currentJobId) - return; - - m_jobCancelled = true; - m_transferRateTimer->stop(); - - m_titleLabel->setText("Export Cancelled"); - m_statusLabel->setText("Export was cancelled by user"); - m_currentFileLabel->clear(); - m_transferRateLabel->clear(); - m_timeRemainingLabel->clear(); - - // Show close button, hide cancel - m_cancelButton->setVisible(false); - m_closeButton->setVisible(true); - m_closeButton->setDefault(true); - m_closeButton->setFocus(); -} - -void ExportProgressDialog::onCancelClicked() -{ - int reply = QMessageBox::question( - this, "Cancel Export", "Are you sure you want to cancel the export?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_exportManager->cancelExport(m_currentJobId); - m_cancelButton->setEnabled(false); - m_cancelButton->setText("Cancelling..."); - } -} - -void ExportProgressDialog::onOpenDirectoryClicked() -{ - qDebug() << "Opening export directory:" << m_destinationPath; - if (!m_destinationPath.isEmpty()) { - QDesktopServices::openUrl(QUrl::fromLocalFile(m_destinationPath)); - QTimer::singleShot(100, this, &QDialog::accept); - } -} - -void ExportProgressDialog::updateTransferRate() -{ - if (m_jobCompleted || m_jobCancelled) { - return; - } - - QDateTime now = QDateTime::currentDateTime(); - qint64 elapsed = m_lastUpdateTime.msecsTo(now); - - if (elapsed > 0) { - qint64 bytesDiff = m_totalBytesTransferred - m_lastBytesTransferred; - qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed; - - m_transferRateLabel->setText(formatTransferRate(bytesPerSecond)); - - // Calculate time remaining - if (bytesPerSecond > 0 && m_completedItems > 0) { - qint64 avgBytesPerItem = m_totalBytesTransferred / m_completedItems; - qint64 remainingBytes = - (m_totalItems - m_completedItems) * avgBytesPerItem; - int secondsRemaining = - static_cast(remainingBytes / bytesPerSecond); - - if (secondsRemaining > 0 && - secondsRemaining < 3600) { // Only show if less than 1 hour - m_timeRemainingLabel->setText( - formatTimeRemaining(secondsRemaining)); - } - } - } - - m_lastBytesTransferred = m_totalBytesTransferred; - m_lastUpdateTime = now; -} - -void ExportProgressDialog::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::PaletteChange) { - updateColors(); - } - QDialog::changeEvent(event); -} - -void ExportProgressDialog::closeEvent(QCloseEvent *event) -{ - if (!m_jobCompleted && !m_jobCancelled) { - // Ask user if they want to cancel the ongoing export - int reply = QMessageBox::question( - this, "Export in Progress", - "An export is currently in progress. Do you want to cancel it?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_exportManager->cancelExport(m_currentJobId); - event->accept(); - } else { - event->ignore(); - } - } else { - event->accept(); - } -} - -QString ExportProgressDialog::formatFileSize(qint64 bytes) const -{ - const qint64 KB = 1024; - const qint64 MB = KB * 1024; - const qint64 GB = MB * 1024; - - if (bytes >= GB) { - return QString("%1 GB").arg( - QString::number(bytes / double(GB), 'f', 2)); - } else if (bytes >= MB) { - return QString("%1 MB").arg( - QString::number(bytes / double(MB), 'f', 1)); - } else if (bytes >= KB) { - return QString("%1 KB").arg( - QString::number(bytes / double(KB), 'f', 0)); - } else { - return QString("%1 bytes").arg(bytes); - } -} - -QString ExportProgressDialog::formatTransferRate(qint64 bytesPerSecond) const -{ - return QString("%1/s").arg(formatFileSize(bytesPerSecond)); -} - -QString ExportProgressDialog::formatTimeRemaining(int secondsRemaining) const -{ - if (secondsRemaining < 60) { - return QString("%1 seconds remaining").arg(secondsRemaining); - } else { - int minutes = secondsRemaining / 60; - int seconds = secondsRemaining % 60; - return QString("%1:%2 remaining") - .arg(minutes) - .arg(seconds, 2, 10, QLatin1Char('0')); - } -} \ No newline at end of file diff --git a/src/exportprogressdialog.h b/src/exportprogressdialog.h deleted file mode 100644 index c57ebe7..0000000 --- a/src/exportprogressdialog.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef EXPORTPROGRESSDIALOG_H -#define EXPORTPROGRESSDIALOG_H - -#include -#include -#include -#include -#include -#include -#include -#include - -// Forward declarations -class ExportManager; -struct ExportResult; -struct ExportJobSummary; - -class ExportProgressDialog : public QDialog -{ - Q_OBJECT - -public: - // Constructor with ExportManager parameter since it's owned by manager - explicit ExportProgressDialog(ExportManager *exportManager, - QWidget *parent = nullptr); - - void showForJob(const QUuid &jobId); - -protected: - void changeEvent(QEvent *event) override; - void closeEvent(QCloseEvent *event) override; - -private slots: - void onExportStarted(const QUuid &jobId, int totalItems, - const QString &destinationPath); - void onExportProgress(const QUuid &jobId, int currentItem, int totalItems, - const QString ¤tFileName); - void onFileTransferProgress(const QUuid &jobId, const QString &fileName, - qint64 bytesTransferred, qint64 totalFileSize); - void onItemExported(const QUuid &jobId, const ExportResult &result); - void onExportFinished(const QUuid &jobId, const ExportJobSummary &summary); - void onExportCancelled(const QUuid &jobId); - void onCancelClicked(); - void onOpenDirectoryClicked(); - void updateTransferRate(); - -private: - void setupUI(); - void updateColors(); - QString formatFileSize(qint64 bytes) const; - QString formatTransferRate(qint64 bytesPerSecond) const; - QString formatTimeRemaining(int secondsRemaining) const; - - ExportManager *m_exportManager; - QUuid m_currentJobId; - - QVBoxLayout *m_mainLayout; - QLabel *m_titleLabel; - QLabel *m_statusLabel; - QLabel *m_currentFileLabel; - QProgressBar *m_progressBar; - QLabel *m_statsLabel; - QLabel *m_transferRateLabel; - QLabel *m_timeRemainingLabel; - QPushButton *m_cancelButton; - QPushButton *m_closeButton; - QPushButton *m_openDirButton; - - QString m_destinationPath; - int m_totalItems = 0; - int m_completedItems = 0; - qint64 m_totalBytesTransferred = 0; - QTimer *m_transferRateTimer; - qint64 m_lastBytesTransferred = 0; - QDateTime m_startTime; - QDateTime m_lastUpdateTime; - - bool m_jobCompleted = false; - bool m_jobCancelled = false; -}; - -#endif // EXPORTPROGRESSDIALOG_H diff --git a/src/fileexplorerwidget.cpp b/src/fileexplorerwidget.cpp index db7df22..684df6f 100644 --- a/src/fileexplorerwidget.cpp +++ b/src/fileexplorerwidget.cpp @@ -23,36 +23,34 @@ #include "iDescriptor.h" #include "mediapreviewdialog.h" #include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include FileExplorerWidget::FileExplorerWidget(const iDescriptorDevice *device, QWidget *parent) : QWidget(parent), m_device(device) { - m_mainSplitter = new ModernSplitter(Qt::Horizontal, this); + QVBoxLayout *rootLayout = new QVBoxLayout(this); + rootLayout->setContentsMargins(0, 0, 0, 0); + + m_loadingWidget = new ZLoadingWidget(true, this); + rootLayout->addWidget(m_loadingWidget); +} + +void FileExplorerWidget::init() { + if (m_loaded) { + qDebug() << "[FileExplorerWidget]: Already initialized, skipping init()"; + return; + } + m_loaded = true; + m_mainSplitter = new ModernSplitter(Qt::Horizontal); // Main layout - QHBoxLayout *mainLayout = new QHBoxLayout(this); + QWidget *contentContainer = new QWidget(this); + QHBoxLayout *mainLayout = new QHBoxLayout(contentContainer); mainLayout->addWidget(m_mainSplitter); mainLayout->setContentsMargins(0, 0, 0, 0); + m_loadingWidget->setupContentWidget(contentContainer); + setupSidebar(); // Create stacked widget with AFC explorers @@ -85,7 +83,10 @@ FileExplorerWidget::FileExplorerWidget(const iDescriptorDevice *device, connect(SettingsManager::sharedInstance(), &SettingsManager::favoritePlacesChanged, this, &FileExplorerWidget::loadFavoritePlaces); + + m_loadingWidget->stop(true); } + void FileExplorerWidget::setupSidebar() { m_sidebarTree = new QTreeWidget(); diff --git a/src/fileexplorerwidget.h b/src/fileexplorerwidget.h index 2a0a898..23b384b 100644 --- a/src/fileexplorerwidget.h +++ b/src/fileexplorerwidget.h @@ -34,6 +34,25 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zloadingwidget.h" + class FileExplorerWidget : public QWidget { @@ -41,11 +60,13 @@ class FileExplorerWidget : public QWidget public: explicit FileExplorerWidget(const iDescriptorDevice *device, QWidget *parent = nullptr); + void init(); private slots: void onSidebarItemClicked(QTreeWidgetItem *item, int column); private: + ZLoadingWidget *m_loadingWidget; QSplitter *m_mainSplitter; QStackedWidget *m_stackedWidget; AfcClientHandle *currentAfcClient; @@ -62,6 +83,7 @@ private: void saveFavoritePlace(const QString &alias, const QString &path); void saveFavoritePlaceAfc2(const QString &alias, const QString &path); void onSidebarContextMenuRequested(const QPoint &pos); + bool m_loaded = false; }; #endif // FILEEXPLORERWIDGET_H diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index 7200b93..7a78707 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -287,8 +287,8 @@ void GalleryWidget::onExportSelected() qDebug() << "Starting export of selected files:" << exportItems.size() << "items to" << exportDir; - ExportManager::sharedInstance()->startExport(m_device, exportItems, - exportDir); + ExportManager::sharedInstance()->startExport( + m_device, exportItems, exportDir, "Exporting from gallery"); } void GalleryWidget::onExportAll() @@ -345,8 +345,8 @@ void GalleryWidget::onExportAll() << exportDir; // Start export and the manager will show its own dialog - ExportManager::sharedInstance()->startExport(m_device, exportItems, - exportDir); + ExportManager::sharedInstance()->startExport( + m_device, exportItems, exportDir, "Exporting from gallery"); } QString GalleryWidget::selectExportDirectory() @@ -376,7 +376,8 @@ void GalleryWidget::setupAlbumSelectionView() m_albumListView->setFlow(QListView::LeftToRight); m_albumListView->setWrapping(true); m_albumListView->setResizeMode(QListView::Adjust); - m_albumListView->setIconSize(QSize(120, 120)); + m_albumListView->setIconSize(QSize(250, 250)); + m_albumListView->setGridSize(QSize(260, 280)); m_albumListView->setSpacing(10); m_albumListView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_albumListView->setUniformItemSizes(true); @@ -387,8 +388,8 @@ void GalleryWidget::setupAlbumSelectionView() " padding: 0px;" "} " "QListView::item { " - " width: 150px; " - " height: 150px; " + " width: 250px; " + " height: 250px; " " margin: 2px; " "}"); diff --git a/src/gallerywidget.h b/src/gallerywidget.h index 0adf7f7..27d7595 100644 --- a/src/gallerywidget.h +++ b/src/gallerywidget.h @@ -42,7 +42,6 @@ class QStandardItemModel; QT_END_NAMESPACE class ExportManager; -class ExportProgressDialog; class GalleryWidget : public QWidget { @@ -105,7 +104,6 @@ private: // Export manager ExportManager *m_exportManager; - ExportProgressDialog *m_exportProgressDialog; }; #endif // GALLERYWIDGET_H diff --git a/src/heartbeat.h b/src/heartbeat.h index a74e1b7..71051d3 100644 --- a/src/heartbeat.h +++ b/src/heartbeat.h @@ -25,9 +25,8 @@ public: u_int64_t interval = 15; while (!isInterruptionRequested()) { - // 1. Wait for Marco with current interval Result result = m_hb.get_marco(interval); - if (result.is_err()) { + if (result.is_err() && !isInterruptionRequested()) { qDebug() << "Failed to get marco:" << QString::fromStdString(result.unwrap_err().message); @@ -38,21 +37,21 @@ public: << "Maximum heartbeat retries reached, exiting for " "device" << m_macAddress; + m_exited = true; emit heartbeatThreadExited(m_macAddress); break; } - // If get_marco failed, skip the rest of this iteration - // and try again with the current interval. continue; } - // 2. Get the new interval from device interval = result.unwrap(); qDebug() << "Received marco, new interval:" << interval; - // 3. Send Polo response + if (isInterruptionRequested()) { + break; + } Result polo_result = m_hb.send_polo(); - if (polo_result.is_err()) { + if (polo_result.is_err() && !isInterruptionRequested()) { qDebug() << "Failed to send polo:" << QString::fromStdString( polo_result.unwrap_err().message); @@ -67,16 +66,12 @@ public: emit heartbeatThreadExited(m_macAddress); break; } - // If send_polo failed, skip the rest of this iteration - // and try again with the current interval. continue; } - // If both marco and polo succeeded: qDebug() << "Sent polo successfully"; - interval += 5; // Increment interval for the next cycle - m_initialCompleted = true; // Mark as initially completed after - // first successful full cycle + interval += 5; + m_initialCompleted = true; } } catch (const std::exception &e) { qDebug() << "Heartbeat error:" << e.what(); diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 88e34b1..0cc9cbf 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -649,6 +649,7 @@ struct ExportResult { QString sourceFilePath; QString outputFilePath; bool success = false; + bool cancelled = false; QString errorMessage; qint64 bytesTransferred = 0; }; diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp index 7c5b29f..5c6f993 100644 --- a/src/installedappswidget.cpp +++ b/src/installedappswidget.cpp @@ -24,17 +24,6 @@ #include "qprocessindicator.h" #include "servicemanager.h" #include "zlineedit.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include AppTabWidget::AppTabWidget(const QString &appName, const QString &bundleId, const QString &version, const QPixmap &icon, @@ -152,6 +141,22 @@ InstalledAppsWidget::InstalledAppsWidget(const iDescriptorDevice *device, QWidget *parent) : QWidget(parent), m_device(device) { + QVBoxLayout *rootLayout = new QVBoxLayout(this); + rootLayout->setContentsMargins(0, 0, 0, 0); + + m_zloadingWidget = new ZLoadingWidget(true, this); + rootLayout->addWidget(m_zloadingWidget); +} + +void InstalledAppsWidget::init() +{ + if (m_loaded) { + qDebug() + << "[InstalledAppsWidget]: Already initialized, skipping init()"; + return; + } + m_loaded = true; + m_watcher = new QFutureWatcher(this); m_containerWatcher = new QFutureWatcher(this); setupUI(); @@ -183,10 +188,13 @@ InstalledAppsWidget::~InstalledAppsWidget() void InstalledAppsWidget::setupUI() { - m_mainLayout = new QHBoxLayout(this); + QWidget *contentContainer = new QWidget(this); + m_mainLayout = new QHBoxLayout(contentContainer); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->setSpacing(0); + m_zloadingWidget->setupContentWidget(contentContainer); + // Create stacked widget for different states m_stackedWidget = new QStackedWidget(this); m_mainLayout->addWidget(m_stackedWidget); @@ -482,6 +490,7 @@ void InstalledAppsWidget::onAppsDataReady() // Switch to content view once data is loaded m_stackedWidget->setCurrentWidget(m_contentWidget); + m_zloadingWidget->stop(true); // Sort apps by display name std::sort(apps.begin(), apps.end(), diff --git a/src/installedappswidget.h b/src/installedappswidget.h index 609e34f..1ed7e00 100644 --- a/src/installedappswidget.h +++ b/src/installedappswidget.h @@ -22,14 +22,22 @@ #include "iDescriptor.h" #include "zlineedit.h" +#include "zloadingwidget.h" +#include +#include #include +#include #include #include #include #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -40,8 +48,10 @@ #include #include #include +#include #include #include +#include class AppTabWidget : public QWidget { @@ -94,6 +104,7 @@ class InstalledAppsWidget : public QWidget public: explicit InstalledAppsWidget(const iDescriptorDevice *device, QWidget *parent = nullptr); + void init(); ~InstalledAppsWidget(); private slots: @@ -139,12 +150,15 @@ private: QFutureWatcher *m_watcher; QFutureWatcher *m_containerWatcher; QSplitter *m_splitter; + ZLoadingWidget *m_zloadingWidget; + AfcClientHandle *m_houseArrestAfcClient = nullptr; // App data storage QList m_appTabs; AppTabWidget *m_selectedTab = nullptr; SpringBoardServicesClientHandle *m_springboardClient = nullptr; bool m_loadingContainer = false; + bool m_loaded = false; }; #endif // INSTALLEDAPPSWIDGET_H \ No newline at end of file diff --git a/src/livescreenwidget.cpp b/src/livescreenwidget.cpp index 17e9c6b..631fa1c 100644 --- a/src/livescreenwidget.cpp +++ b/src/livescreenwidget.cpp @@ -33,7 +33,7 @@ #include LiveScreenWidget::LiveScreenWidget(iDescriptorDevice *device, QWidget *parent) - : Tool{parent}, m_device(device) + : Tool{parent, false}, m_device(device) { setWindowTitle("Live Screen - iDescriptor"); setAttribute(Qt::WA_DeleteOnClose); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4af17a3..6c3863a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -162,13 +162,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) []() { QDesktopServices::openUrl(QUrl(REPO_URL)); }); m_connectedDeviceCountLabel = new QLabel("iDescriptor: no devices"); - m_connectedDeviceCountLabel->setContentsMargins(5, 0, 5, 0); + m_connectedDeviceCountLabel->setContentsMargins(0, 0, 0, 0); m_connectedDeviceCountLabel->setStyleSheet( "QLabel:hover { background-color : #13131319; }"); QWidget *statusbar = new QWidget(); QHBoxLayout *statusLayout = new QHBoxLayout(statusbar); - statusLayout->setContentsMargins(0, 0, 0, 0); + statusLayout->setContentsMargins(10, 0, 10, 0); + statusLayout->setSpacing(1); statusbar->setObjectName("StatusBar"); statusbar->setStyleSheet( "QWidget#StatusBar { background-color: transparent; }"); @@ -205,7 +206,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) statusLayout->addWidget(welcomeMenuSwitch); statusLayout->addStretch(1); - statusLayout->setContentsMargins(0, 0, 0, 0); QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION)); appVersionLabel->setContentsMargins(5, 0, 5, 0); appVersionLabel->setStyleSheet( diff --git a/src/qballoontip.cpp b/src/qballoontip.cpp index 411d93a..385c308 100644 --- a/src/qballoontip.cpp +++ b/src/qballoontip.cpp @@ -47,8 +47,16 @@ QBalloonTip::QBalloonTip(QWidget *widget) Qt::ToolTip), widget(widget) { - // setAttribute(Qt::WA_DeleteOnClose); - // setAttribute(Qt::WA_TranslucentBackground); + setObjectName("balloonTip"); +#ifdef WIN32 + setAttribute(Qt::WA_TranslucentBackground); +#else + setAttribute(Qt::WA_StyledBackground, true); + setStyleSheet("QWidget#balloonTip { " + " background-color: transparent;" + "}"); +#endif + if (widget) { connect(widget, &QWidget::destroyed, this, &QBalloonTip::close); } else if (QApplication::activeWindow()) { @@ -218,7 +226,39 @@ bool QBalloonTip::eventFilter(QObject *obj, QEvent *event) close(); return false; } + } else if (event->type() == QEvent::ApplicationDeactivate) { + // App went to background → hide balloon + if (m_visible) { + m_visible = false; + close(); + } + return false; // let others handle it too } + + // handle macOS tab switch and Escape key handling + else if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); +#ifdef __APPLE__ + if (auto *ke = dynamic_cast(event)) { + const Qt::KeyboardModifiers mods = ke->modifiers(); + if ((mods & (Qt::MetaModifier | Qt::ControlModifier))) { + if (m_visible) { + m_visible = false; + close(); + return true; + } + } + } +#endif + if (keyEvent->key() == Qt::Key_Escape) { + if (m_visible) { + m_visible = false; + close(); + return true; + } + } + } + return QWidget::eventFilter(obj, event); } diff --git a/src/sponsorwidget.cpp b/src/sponsorwidget.cpp index f979f9f..5389c01 100644 --- a/src/sponsorwidget.cpp +++ b/src/sponsorwidget.cpp @@ -23,6 +23,7 @@ SponsorWidget::SponsorWidget(QWidget *parent) : Tool(parent) { setMaximumSize(600, 400); setLayout(new QVBoxLayout(this)); + setWindowTitle("Sponsor Us - iDescriptor"); QLabel *sponsorTitle = new QLabel("Would you like to sponsor us?"); sponsorTitle->setStyleSheet("font-weight: bold; font-size: 16pt;"); sponsorTitle->setAlignment(Qt::AlignCenter); diff --git a/src/sshterminaltool.cpp b/src/sshterminaltool.cpp index e9aa368..50f71e4 100644 --- a/src/sshterminaltool.cpp +++ b/src/sshterminaltool.cpp @@ -25,6 +25,7 @@ SSHTerminalTool::SSHTerminalTool(QWidget *parent) : Tool{parent}, m_selectedUniq(QString(""), false) { + setWindowTitle("SSH Terminal - iDescriptor"); QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(2, 2, 2, 2); mainLayout->setSpacing(2); diff --git a/src/statusballoon.cpp b/src/statusballoon.cpp index 239b183..ad525e6 100644 --- a/src/statusballoon.cpp +++ b/src/statusballoon.cpp @@ -31,75 +31,145 @@ #include "platform/windows/win_common.h" #endif -BalloonProcess::BalloonProcess(ProcessItem *item, QWidget *parent) - : QWidget(parent), m_item(item) +BalloonProcess::BalloonProcess(std::shared_ptr item, + QWidget *parent) + : QWidget(parent), m_item(std::move(item)) { auto *layout = new QVBoxLayout(this); - layout->setSpacing(6); - layout->setContentsMargins(15, 15, 15, 15); + layout->setSpacing(5); + layout->setContentsMargins(15, 5, 5, 15); m_lastBytesTransferred = 0; m_lastUpdateTime = QDateTime::currentDateTime(); // Title - item->titleLabel = new QLabel(m_item->title); - QFont titleFont = item->titleLabel->font(); + m_titleLabel = new QLabel(m_item->title); + QFont titleFont = m_titleLabel->font(); titleFont.setBold(true); - item->titleLabel->setFont(titleFont); - layout->addWidget(item->titleLabel); + m_titleLabel->setFont(titleFont); + + QHBoxLayout *titleLayout = new QHBoxLayout(); + titleLayout->addWidget(m_titleLabel); + titleLayout->addStretch(); + + m_removeBtn = new ZIconWidget( + QIcon(":/resources/icons/MaterialSymbolsCloseRounded.png"), "Remove"); + auto *opacity = new QGraphicsOpacityEffect(m_removeBtn); + opacity->setOpacity(0.0); + m_removeBtn->setGraphicsEffect(opacity); + + m_removeBtn->setEnabled(false); + + connect(m_removeBtn, &ZIconWidget::clicked, this, [this]() { + StatusBalloon::sharedInstance()->removeProcess(m_item->processId); + }); + titleLayout->addWidget(m_removeBtn); + + layout->addLayout(titleLayout); // Status - item->statusLabel = new QLabel("Starting..."); - layout->addWidget(item->statusLabel); + m_statusLabel = new QLabel("Starting..."); + layout->addWidget(m_statusLabel); // Progress bar - item->progressBar = new QProgressBar(); - item->progressBar->setRange(0, 100); - item->progressBar->setValue(0); - item->progressBar->setTextVisible(true); // show text for debugging - item->progressBar->setFixedHeight(12); // make it visible - layout->addWidget(item->progressBar); + m_progressBar = new QProgressBar(); +#ifdef __APPLE__ + m_progressBar->setStyleSheet(QString("QProgressBar {" + " border-radius: 4px;" + " background: #eee;" + "}" + "QProgressBar::chunk {" + " background-color: %1;" + " border-radius: 4px;" + "}") + .arg(COLOR_ACCENT_BLUE.name())); +#endif + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_progressBar->setTextVisible(false); + m_progressBar->setFixedHeight(12); + layout->addWidget(m_progressBar); // Stats - item->statsLabel = new QLabel(); - QFont statsFont = item->statsLabel->font(); + m_statsLabel = new QLabel(); + QFont statsFont = m_statsLabel->font(); statsFont.setPointSize(statsFont.pointSize() - 1); - item->statsLabel->setFont(statsFont); - layout->addWidget(item->statsLabel); + m_statsLabel->setFont(statsFont); + layout->addWidget(m_statsLabel); // Buttons layout auto *buttonsLayout = new QHBoxLayout(); buttonsLayout->setSpacing(6); - // Action button (Open Folder for export, hidden initially) - item->actionButton = new QPushButton(); - item->actionButton->setVisible(false); - if (item->type == ProcessType::Export) { - item->actionButton->setText("Open Folder"); - connect(item->actionButton, &QPushButton::clicked, - StatusBalloon::sharedInstance(), - &StatusBalloon::onOpenFolderClicked); + // Action button + m_actionButton = new QPushButton(); + m_actionButton->setVisible(false); + if (m_item->type == ProcessType::Export) { + m_actionButton->setText("Open Folder"); + connect(m_actionButton, &QPushButton::clicked, this, + &BalloonProcess::onOpenFolderClicked); } - buttonsLayout->addWidget(item->actionButton); + buttonsLayout->addWidget(m_actionButton); buttonsLayout->addStretch(); // Cancel button - item->cancelButton = new QPushButton("Cancel"); - connect(item->cancelButton, &QPushButton::clicked, - StatusBalloon::sharedInstance(), &StatusBalloon::onCancelClicked); - buttonsLayout->addWidget(item->cancelButton); + m_cancelButton = new QPushButton("Cancel"); + connect(m_cancelButton, &QPushButton::clicked, this, + &BalloonProcess::onCancelClicked); + buttonsLayout->addWidget(m_cancelButton); layout->addLayout(buttonsLayout); + layout->addStretch(); + + setObjectName("BalloonProcess"); + setAttribute(Qt::WA_StyledBackground, true); + updateStyles(); } -void BalloonProcess::setProgress(int progress) +void BalloonProcess::updateStyles() { - m_item->progressBar->setValue(progress); + QString style; + bool dark = isDarkMode(); + + if (!dark) { + style = "QWidget#BalloonProcess { background-color: " + "rgba(0, 0, 0, 10); border-radius: 5px; }"; + } else { + style = "QWidget#BalloonProcess { background-color: rgba(255, " + "255, 255, 16); border-radius: 5px; }"; + } + if (style != styleSheet()) + setStyleSheet(style); } -void BalloonProcess::updateStats() +void BalloonProcess::onCancelClicked() { + m_cancelButton->setEnabled(false); + m_cancelButton->setText("Cancelling..."); + ExportManager::sharedInstance()->cancel(m_item->jobId); +} + +void BalloonProcess::updateUI() +{ + QString statusText; + if (m_item->status == ProcessStatus::Running) { + statusText = m_item->currentFile.isEmpty() ? "Processing..." + : m_item->currentFile; + } else if (m_item->status == ProcessStatus::Completed) { + statusText = "Completed successfully"; + } else if (m_item->status == ProcessStatus::Failed) { + statusText = "Failed"; + } else if (m_item->status == ProcessStatus::Cancelled) { + statusText = "Cancelled"; + } + m_statusLabel->setText(statusText); + + if (m_item->totalBytes > 0 && m_item->transferredBytes > 0) { + int progress = (m_item->transferredBytes * 100) / m_item->totalBytes; + m_progressBar->setValue(progress); + } + QString statsText = QString("%1 of %2 items") .arg(m_item->completedItems) .arg(m_item->totalItems); @@ -109,7 +179,6 @@ void BalloonProcess::updateStats() if (m_item->status == ProcessStatus::Running && m_item->transferredBytes > 0) { - // Calculate transfer rate QDateTime now = QDateTime::currentDateTime(); qint64 elapsed = m_lastUpdateTime.msecsTo(now); if (elapsed > 0) { @@ -123,25 +192,51 @@ void BalloonProcess::updateStats() m_lastUpdateTime = now; } } + m_statsLabel->setText(statsText); - m_item->statsLabel->setText(statsText); -} - -void BalloonProcess::updateButtons() -{ - // Update buttons if (m_item->status == ProcessStatus::Running) { - m_item->cancelButton->setVisible(true); - m_item->actionButton->setVisible(false); + m_cancelButton->setVisible(true); + m_actionButton->setVisible(false); } else { - m_item->cancelButton->setVisible(false); + m_cancelButton->setVisible(false); if (m_item->type == ProcessType::Export && m_item->status == ProcessStatus::Completed) { - m_item->actionButton->setVisible(true); + m_actionButton->setVisible(true); } } } +void BalloonProcess::onOpenFolderClicked() +{ + if (!m_item->destinationPath.isEmpty() && + m_item->type == ProcessType::Export) { + QDesktopServices::openUrl(QUrl::fromLocalFile(m_item->destinationPath)); + } +} + +void BalloonProcess::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + if (m_item->status == ProcessStatus::Completed || + m_item->status == ProcessStatus::Failed || + m_item->status == ProcessStatus::Cancelled) { + if (auto *eff = qobject_cast( + m_removeBtn->graphicsEffect())) { + eff->setOpacity(1.0); + } + m_removeBtn->setEnabled(true); + } +} + +void BalloonProcess::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + if (auto *eff = qobject_cast( + m_removeBtn->graphicsEffect())) { + eff->setOpacity(0.0); + } +} + StatusBalloon *StatusBalloon::sharedInstance() { static StatusBalloon instance; @@ -152,16 +247,28 @@ StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent) { setMinimumHeight(300); setMinimumWidth(300); -#ifdef WIN32 - setAttribute(Qt::WA_TranslucentBackground); + + auto *outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); + outerLayout->setSpacing(0); + + QWidget *container = new QWidget; +#ifndef WIN32 + container->setObjectName("StatusBalloon"); + container->setStyleSheet(QString("QWidget#StatusBalloon { " + " background-color: %1;" + " border-radius: 8px;" + "border: 1px solid #ccc;" + "}") + .arg(QApplication::palette() + .color(QPalette::Window) + .name(QColor::HexArgb))); #endif - setObjectName("StatusBalloon"); - setStyleSheet("QWidget#StatusBalloon { border-radius: 8px; border: " - "1px solid #ccc; }"); - // Create main layout - m_mainLayout = new QVBoxLayout(); + outerLayout->addWidget(container); + + m_mainLayout = new QVBoxLayout(container); m_mainLayout->setSpacing(8); - m_mainLayout->setContentsMargins(5, 5, 5, 5); + m_mainLayout->setContentsMargins(15, 15, 15, 15); m_noProcesesLabel = new QLabel("Export & Import processes will appear here", this); @@ -191,7 +298,7 @@ StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent) scrollArea->viewport()->setStyleSheet("background: transparent;"); m_processesLayout->setSpacing(12); - m_processesLayout->setContentsMargins(5, 5, 5, 5); + m_processesLayout->setContentsMargins(10, 10, 10, 10); m_mainLayout->addWidget(scrollArea); setLayout(m_mainLayout); @@ -218,7 +325,7 @@ void StatusBalloon::connectExportThreadSignals() // QTimer::singleShot(3000, this, [this]() { // // test // startProcess("Test Export Process", 10, "/path/to/destination", - // ProcessType::Export); + // ProcessType::Export, QUuid()); // }); } @@ -227,84 +334,54 @@ void StatusBalloon::onFileTransferProgress(const QUuid &processId, qint64 bytesTransferred, qint64 totalBytes) { - qDebug() << "StatusBalloon::updateProcessProgress"; - // FIXME - // QMutexLocker locker(&m_processesMutex); - - ProcessItem *item = m_processes[processId]; - if (!item) { - qDebug() << "StatusBalloon::updateProcessProgress: unknown processId" - << processId; + QMutexLocker locker(&m_processesMutex); + if (!m_processes.contains(processId)) return; - } + auto item = m_processes[processId]; item->currentFile = currentFile; item->transferredBytes = bytesTransferred; item->totalBytes = totalBytes; - if (!item->processWidget) { - qDebug() - << "StatusBalloon::updateProcessProgress: no widget for processId" - << processId; - return; - } - handleJobUpdate(item); } void StatusBalloon::onExportFinished(const QUuid &processId, const ExportJobSummary &summary) { - qDebug() << "StatusBalloon::onExportFinished entry:" << processId - << "WasCancelled:" << summary.wasCancelled; QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(processId)) { - qDebug() << "StatusBalloon::onExportFinished: unknown processId" - << processId; + if (!m_processes.contains(processId)) return; - } - // todo: handle failed ? - ProcessItem *item = m_processes[processId]; + auto item = m_processes[processId]; if (summary.wasCancelled) { item->status = ProcessStatus::Cancelled; } else { - item->status = ProcessStatus::Completed; - } - item->endTime = QDateTime::currentDateTime(); - - updateHeader(); -} - -void StatusBalloon::onItemExported(const QUuid &processId, - const ExportResult &result) -{ - qDebug() << "StatusBalloon::onItemExported entry:" << processId - << "Success:" << result.success; - QMutexLocker locker(&m_processesMutex); - - if (!m_processes.contains(processId)) { - qDebug() << "StatusBalloon::onItemExported: unknown processId" - << processId; - return; - } - - ProcessItem *item = m_processes[processId]; - if (result.success) { - item->completedItems += 1; - } else { - item->failedItems += 1; - } - - if (item->completedItems + item->failedItems == item->totalItems) { - // meaning all items are processed, but we don't know if the overall - // status is if (item->failedItems > 0) { item->status = ProcessStatus::Failed; } else { item->status = ProcessStatus::Completed; } } + item->endTime = QDateTime::currentDateTime(); + + handleJobUpdate(item); + updateHeader(); +} + +void StatusBalloon::onItemExported(const QUuid &processId, + const ExportResult &result) +{ + QMutexLocker locker(&m_processesMutex); + if (!m_processes.contains(processId)) + return; + + auto item = m_processes[processId]; + if (result.success) + item->completedItems++; + else + item->failedItems++; + handleJobUpdate(item); updateHeader(); } @@ -312,105 +389,84 @@ void StatusBalloon::onItemExported(const QUuid &processId, void StatusBalloon::onItemImported(const QUuid &processId, const ImportResult &result) { - qDebug() << "StatusBalloon::onItemImported entry:" << processId - << "Success:" << result.success; QMutexLocker locker(&m_processesMutex); - - if (!m_processes.contains(processId)) { - qDebug() << "StatusBalloon::onItemImported: unknown processId" - << processId; + if (!m_processes.contains(processId)) return; - } - ProcessItem *item = m_processes[processId]; - if (result.success) { - item->completedItems += 1; - } else { - item->failedItems += 1; - } + auto item = m_processes[processId]; + if (result.success) + item->completedItems++; + else + item->failedItems++; - if (item->completedItems + item->failedItems == item->totalItems) { - // meaning all items are processed, but we don't know if the overall - // status is - if (item->failedItems > 0) { - item->status = ProcessStatus::Failed; - } else { - item->status = ProcessStatus::Completed; - } - } handleJobUpdate(item); updateHeader(); } QUuid StatusBalloon::startProcess(const QString &title, int totalItems, const QString &destinationPath, - ProcessType type) + ProcessType type, const QUuid &jobId) { - qDebug() << "StatusBalloon::startExportProcess entry:" << title - << totalItems << destinationPath; + handleShow(true); - handleShow(true); // ensure balloon is visible when process starts - - auto *item = new ProcessItem(); + auto item = std::make_shared(); item->processId = QUuid::createUuid(); item->type = type; item->status = ProcessStatus::Running; item->title = title; item->totalItems = totalItems; - item->completedItems = 0; - item->failedItems = 0; - item->totalBytes = 0; - item->transferredBytes = 0; item->startTime = QDateTime::currentDateTime(); item->destinationPath = destinationPath; + item->jobId = jobId; - { // scope the lock only for shared-state mutation + { QMutexLocker locker(&m_processesMutex); m_processes[item->processId] = item; - m_currentProcessId = item->processId; - } // mutex released here + } - // UI work must run without holding m_processesMutex to avoid re-locking - // deadlock createProcessWidget(item); updateHeader(); - // show blue dot when there is at least one running process if (m_button) m_button->setIndicatorVisible(true); - return item->processId; } -void StatusBalloon::createProcessWidget(ProcessItem *item) +void StatusBalloon::createProcessWidget(std::shared_ptr item) { + // Pass shared_ptr to widget BalloonProcess *processWidget = new BalloonProcess(item); item->processWidget = processWidget; - m_processesLayout->addWidget(item->processWidget); + m_processesLayout->addWidget(processWidget); + m_processesLayout->addStretch(); } void StatusBalloon::updateHeader() { // QMutexLocker locker(&m_processesMutex); - // Update header - int running = 0, completed = 0, failed = 0; - for (auto *item : m_processes) { + int running = 0, completed = 0, failed = 0, canceled = 0; + for (const auto &item : m_processes) { if (item->status == ProcessStatus::Running) running++; else if (item->status == ProcessStatus::Completed) completed++; else if (item->status == ProcessStatus::Failed) failed++; + else if (item->status == ProcessStatus::Cancelled) + canceled++; } - int total = running + completed + failed; + int total = running + completed + failed + canceled; QString headerText = QString("Processes: %1 running").arg(running); - if (completed > 0 || failed > 0) { + if (completed > 0 || failed > 0 || canceled > 0) { headerText += QString(" • %1 completed").arg(completed); if (failed > 0) { headerText += QString(" • %1 failed").arg(failed); } + if (canceled > 0) { + headerText += QString(" • %1 cancelled").arg(canceled); + } } m_headerLabel->setText(headerText); @@ -432,19 +488,10 @@ void StatusBalloon::handleShow(bool forceVisible) toggleBaloon(pos, -1, forceVisible); } -bool StatusBalloon::isProcessRunning(const QUuid &processId) const -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(processId)) { - return false; - } - return m_processes[processId]->status == ProcessStatus::Running; -} - bool StatusBalloon::hasActiveProcesses() const { QMutexLocker locker(&m_processesMutex); - for (auto *item : m_processes) { + for (const auto &item : m_processes) { if (item->status == ProcessStatus::Running) { return true; } @@ -452,115 +499,43 @@ bool StatusBalloon::hasActiveProcesses() const return false; } -bool StatusBalloon::isCancelRequested(const QUuid &processId) const +void StatusBalloon::removeProcess(const QUuid &processId) { - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(processId)) { - return false; - } - return m_processes[processId]->cancelRequested.load(); -} + std::shared_ptr item; + { + QMutexLocker locker(&m_processesMutex); + if (!m_processes.contains(processId)) + return; -void StatusBalloon::onCancelClicked() -{ - QPushButton *button = qobject_cast(sender()); - if (!button) - return; - - QMutexLocker locker(&m_processesMutex); - - // Find which process this button belongs to - for (auto *item : m_processes) { - if (item->cancelButton == button) { - item->cancelRequested.store(true); - button->setEnabled(false); - button->setText("Cancelling..."); - break; - } - } -} - -void StatusBalloon::onOpenFolderClicked() -{ - QPushButton *button = qobject_cast(sender()); - if (!button) - return; - - QMutexLocker locker(&m_processesMutex); - - for (auto *item : m_processes) { - if (item->actionButton == button && item->type == ProcessType::Export) { - QDesktopServices::openUrl( - QUrl::fromLocalFile(item->destinationPath)); - break; - } - } -} - -void StatusBalloon::removeProcessWidget(const QUuid &processId) -{ - QMutexLocker locker(&m_processesMutex); - - if (!m_processes.contains(processId)) { - return; + item = m_processes[processId]; + m_processes.remove(processId); } - ProcessItem *item = m_processes[processId]; if (item->processWidget) { m_processesLayout->removeWidget(item->processWidget); item->processWidget->deleteLater(); + item->processWidget = nullptr; } - m_processes.remove(processId); - // hide dot if no active processes left if (m_button && !hasActiveProcesses()) m_button->setIndicatorVisible(false); - if (m_processes.isEmpty()) { - hide(); - } + updateHeader(); } -void StatusBalloon::handleJobUpdate(ProcessItem *item) +void StatusBalloon::handleJobUpdate(const std::shared_ptr &item) { - // QMutexLocker locker(&m_processesMutex); - - // Update status label - QString statusText; - if (item->status == ProcessStatus::Running) { - if (!item->currentFile.isEmpty()) { - // FIXME :Exporting... filename.ext or / Importing ... filename.ext - statusText = item->currentFile; - } else { - statusText = "Processing..."; - } - } else if (item->status == ProcessStatus::Completed) { - statusText = "Completed successfully"; - } else if (item->status == ProcessStatus::Failed) { - statusText = "Failed"; - } else if (item->status == ProcessStatus::Cancelled) { - statusText = "Cancelled"; + if (item->processWidget) { + item->processWidget->updateUI(); } - item->statusLabel->setText(statusText); - - // Update progress bar - // progess should be based on exported bytes vs total bytes of the current - // file - if (item->totalItems > 0) { - int progress = (item->transferredBytes * 100) / item->totalBytes; - item->processWidget->setProgress(progress); - } - - item->processWidget->updateStats(); - item->processWidget->updateButtons(); } #ifdef WIN32 void StatusBalloon::showEvent(QShowEvent *event) { QBalloonTip::showEvent(event); - // HWND changes after hide/show, so have reapply acrylic here + // HWND changes after hide/show, have to reapply acrylic here enableMica((HWND)winId()); SetCorner((HWND)winId(), CornerPreference::Corner_Round); } diff --git a/src/statusballoon.h b/src/statusballoon.h index 1416c87..488a36f 100644 --- a/src/statusballoon.h +++ b/src/statusballoon.h @@ -16,6 +16,8 @@ #include #include #include +#include + class BalloonProcess; enum class ProcessType { Export, Import }; @@ -28,39 +30,55 @@ struct ProcessItem { ProcessStatus status; QString title; QString currentFile; - int totalItems; - int completedItems; - int failedItems; - qint64 totalBytes; - qint64 transferredBytes; + int totalItems = 0; + int completedItems = 0; + int failedItems = 0; + qint64 totalBytes = 0; + qint64 transferredBytes = 0; QDateTime startTime; QDateTime endTime; - QString destinationPath; // For export - BalloonProcess *processWidget; - QLabel *titleLabel; - QLabel *statusLabel; - QLabel *statsLabel; - QProgressBar *progressBar; - QPushButton *actionButton; - QPushButton *cancelButton; - std::atomic cancelRequested{false}; + QString destinationPath; + QUuid jobId; + + BalloonProcess *processWidget = nullptr; }; class BalloonProcess : public QWidget { Q_OBJECT public: - explicit BalloonProcess(ProcessItem *item, QWidget *parent = nullptr); + explicit BalloonProcess(std::shared_ptr item, + QWidget *parent = nullptr); - void setProgress(int progress); - void updateStats(); - void updateButtons(); - void done(); + void updateUI(); private: - ProcessItem *m_item; + void onCancelClicked(); + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void onOpenFolderClicked(); + void updateStyles(); + + std::shared_ptr m_item; QDateTime m_lastUpdateTime; qint64 m_lastBytesTransferred; + + QLabel *m_titleLabel; + QLabel *m_statusLabel; + QLabel *m_statsLabel; + QProgressBar *m_progressBar; + QPushButton *m_actionButton; + QPushButton *m_cancelButton; + ZIconWidget *m_removeBtn; + +protected: + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::PaletteChange) { + updateStyles(); + } + QWidget::changeEvent(event); + } }; class StatusBalloon : public QBalloonTip @@ -72,44 +90,39 @@ public: // Process management QUuid startProcess(const QString &title, int totalItems, - const QString &destinationPath, ProcessType type); + const QString &destinationPath, ProcessType type, + const QUuid &jobId); void onFileTransferProgress(const QUuid &processId, const QString ¤tFile, qint64 bytesTransferred, qint64 totalBytes); - bool isProcessRunning(const QUuid &processId) const; bool hasActiveProcesses() const; - bool isCancelRequested(const QUuid &processId) const; - void onCancelClicked(); - void onOpenFolderClicked(); + void removeProcess(const QUuid &processId); protected: #ifdef WIN32 void showEvent(QShowEvent *event) override; -// void paintEvent(QPaintEvent *event) override; #endif void resizeEvent(QResizeEvent *event) override; private: void updateHeader(); void handleShow(bool forceVisible = false); - void createProcessWidget(ProcessItem *item); - void removeProcessWidget(const QUuid &processId); + void createProcessWidget(std::shared_ptr item); void connectExportThreadSignals(); void onExportFinished(const QUuid &processId, const ExportJobSummary &summary); void onItemExported(const QUuid &processId, const ExportResult &result); void onItemImported(const QUuid &processId, const ImportResult &result); - void handleJobUpdate(ProcessItem *item); + void handleJobUpdate(const std::shared_ptr &item); QVBoxLayout *m_mainLayout; QLabel *m_headerLabel; QWidget *m_processesContainer; QVBoxLayout *m_processesLayout; - QMap m_processes; - QUuid m_currentProcessId; + QMap> m_processes; mutable QMutex m_processesMutex; QLabel *m_noProcesesLabel; }; diff --git a/src/ztabwidget.cpp b/src/ztabwidget.cpp index c443c33..1028a87 100644 --- a/src/ztabwidget.cpp +++ b/src/ztabwidget.cpp @@ -165,26 +165,31 @@ void ZTabWidget::finalizeStyles() if (m_tabs.isEmpty()) return; - ZTab *tab = m_tabs[0]; - if (tab) { - tab->setChecked(true); + m_currentIndex = 0; + ZTab *tab = m_tabs[m_currentIndex]; + if (!tab) + return; - QTimer::singleShot(0, [this, tab]() { - if (!tab) - return; + tab->setChecked(true); + QTimer::singleShot(0, this, [this]() { + if (m_currentIndex >= 0 && m_currentIndex < m_tabs.size()) { +#ifndef WIN32 + animateGlider(m_currentIndex, true); + m_glider->show(); +#else + ZTab *tab = m_tabs[m_currentIndex]; const QRect endRect = gliderEndRectForTab(tab); - if (m_gliderAnimation) { m_gliderAnimation->stop(); delete m_gliderAnimation; m_gliderAnimation = nullptr; } - m_glider->setGeometry(endRect); m_glider->show(); - }); - } +#endif + } + }); updateTabStyles(); }