From ac7d3cb71b1e1ab0939452a7c1eef9f1f31a35ce Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:28:38 +0100 Subject: [PATCH] feat: add start-at-boot setting and fix tray icon to use app logo Add auto-launch settings (start at boot + start minimized) to the Desktop section in Account settings, using Electron's built-in setLoginItemSettings API across macOS, Windows, and Linux. Replace the fallback colored-circle tray icon with proper B logo assets: template images for macOS (adapts to light/dark menu bar) and colored icons for Windows/Linux. Fix BGRA channel order bug in fallback generator and move tray icons to resources/ so they're packaged into the app. --- packages/desktop/electron-builder.yml | 3 +- packages/desktop/resources/tray-icon.png | Bin 0 -> 2383 bytes packages/desktop/resources/tray-icon@2x.png | Bin 0 -> 6201 bytes .../desktop/resources/tray-iconTemplate.png | Bin 0 -> 349 bytes .../resources/tray-iconTemplate@2x.png | Bin 0 -> 426 bytes packages/desktop/src/main.ts | 130 ++++++++++++++++-- packages/desktop/src/preload.ts | 5 + .../modals/settingsPanels/AccountPanel.tsx | 72 +++++++++- packages/web/src/platform/electron.d.ts | 5 + 9 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 packages/desktop/resources/tray-icon.png create mode 100644 packages/desktop/resources/tray-icon@2x.png create mode 100644 packages/desktop/resources/tray-iconTemplate.png create mode 100644 packages/desktop/resources/tray-iconTemplate@2x.png diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 4e35d797..033ac68f 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -9,7 +9,7 @@ files: - "!node_modules" publish: - provider: generic - url: "${BACKSPACE_UPDATE_URL}" + url: "${env.BACKSPACE_UPDATE_URL}" useMultipleRangeRequest: false protocols: - name: Backspace @@ -24,6 +24,7 @@ mac: - dmg - zip win: + icon: ./build/icon.ico target: - nsis linux: diff --git a/packages/desktop/resources/tray-icon.png b/packages/desktop/resources/tray-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38d1d76c216a4051d562b6e98cf17057c2b4c842 GIT binary patch literal 2383 zcmZ`*XHb)i68(@!3B^PNA~#Yaf>J_<&^w`o4gn%93DTRPgc@Rkk)i?-qzFh?;R*^O z3WCxFBG-Zxr5I3<=DSq&3O9G&%zN{Gyq(!SvuEe*`M0xIoG{ixU!yK%oJ^4-Q3H27ovu0DSca0MtzY5RWc<;%vYf@C4df zTX2vQ-_4(WoU%8=xK6gx8&>Dw2O|rz22mvG_ z3Li`40HBUk*H%VqD646^scWE+Iw%djb4Vl#iEPBLc>IqbB8m_aME?JRk4Da4IRb_M zIFLdjh!;r#5z+tE=(urqYw4ko8viyi%*-tX06qz{g{d31x3(xO=e)bnf6BbtKfi5i@>VP+HKnIhFitFX^xMhc?w}jj~i8QwuIG zkR0`du^bh9NsR{&Qd?Y=_bWJfh+W+j$znq^K_<>1%}TJ00tN&SHr78`UO(vU7IdPl z{Z?!&T&&oj&_ERv2+SgLfui9=EMCAIr~~Bs_0*B?yYDar4Y(@rR;FIp3wHl=W{<1t zDB$kKJNtq`e4<5fF;i3#!e#RffCucL;7fTgmd?^3-)7l3i#Fe0J-cr*##mE+x(?_a zC6iLUdHYDcXs>yryp{#;?J?^smoM z9gK=iyDEo{p`QOF=Rq}tH)KpCa-r*T%>%E?i$A+o=ob0t7>tf05a*F%WhR8@JL$rR zBYxDH{OcYf54>$Xglg&rpGI}ExPMC@b4^dqnRb%f19b$rpFYts|7WEwR9>-60 z2e;i^uEnK%)FG2c1lC|uXR`3r%#0ZK{@WK%etb1c$ycZWf91{%7{vQ*1f&e!b3044 zIDrR+ZI$&iS0i}kez!DxC z!+C=nL07sBl2jdyb`)Z8rdra05bl8!Gz2w|F{1LFzy5yL^{EKgoV3==^E@LSX?&~j z*<{USnT}}KF}~|ZmuvNa-xQw{-R?TPtVNlREx;)^IzhRzve?z-(MO5#KQ|xGXNx~b zsKCdD*vdtUjC0^lqb8qAv&JO|& zZ9^6LKDqjpAqPI$GNND&^^uy(@pN=LO+(JZuZmIM3WQMRxkIY#*M;mc_AP{AS(Yw4 z9D{vCUAPUc8VblVd}o{MiqXquJK~1RSw>ZL57ub-#2=21-(IB~n6-Hi)_ctow+#Vl zvqD89?h5kjDT>U(%X)V_o@C5;dF0k&o$`iX4(5;dr-e);rCMLv5*o`32DLO zYK!)dkw?s?cL?Ygua8aKEN``U_}sKJSsS;J9?hNbYxi=aZ`|3^_Q)6YimbD>4~T?e z*odQ1vA0}cLPCCf{Kn$j`)TF(P_E&R|Mb4wn=8vp8qdGRx$zq1c`bZlr$l}-A+*{- z&;4F=o#@q5t0zNq{xz{5mcstJ#XCyugIHd5KXae>>xU2a`>+}c{jyYlE@0<9i?|ad zxk$fIQSWkJG*>~jx7Vza?|HJ-fbD22O;faFjKM3_kvFsPxG)HxQ^^Ir?JHH~8|HFf z>yF22pEJE_MiS2!(rLNiL%*#nZ>v13H&K?%6H>USO|6o+TE;EGbeV!!o3*p=c;E-- z$XPR+otakn@Y(DVk%GPN8i^-ry(6NVx4`%wI7u3WS+t zIKT&yUWwNW`5w~vbdA@(UH)^8mmh^CavQjX$zD3sNC<8=SlscmItp+rK=9kiq*yx(10Vy$Lcrz-5#*Mnm4g#_8^-2=7qscp|kk!jS>CXADx z0;RK6oF?sgFW9UNl;@zh%`-uDoqUSVyw@FT7`2&{cV54K7*XwMySy)XE3vS6TFGcv z)AzjnJC%iZ=}`;f#@^S|v=8ePP%=m5Y^#mbhlKD->${p4(ta339g^^0N}>} z0E9#U01t0c$R7Z}lM?_qHU$8Lz5xJ4F1Z~VB5wmkGbK6cH@=;JQk}tXKyi`RcLxB_ z@cvV9fb3l0n~3D0pdy2G437f`fGZvkn|_n%J!JGeEWUVH3YoiEz5&3^$<4>c31Z{o z)8YmRaq5*StY@<|NX_aop1GLn9dlt} z71b3X@&dIyq~JySB1UWJDj!Dz9NU8<$MDkP7ky(ydP2&9 zqYaSZ9pHt#%2b<1Sh&x}<=oZCKrB6KbiI&_))AXIFd#8kp)98fFsr!gzuAR7wd4y) zhb~wHF!0PsgZU29+H`1T8Oy&$(Rrn6CY2^cR%+=cY*r)=ukRPlvsWXv%llPtJNw{+xB`{C49&kyB)o{zl2Xx36_swV z+n*iDeON(oZsjTr+NSxWBx;7_&hyfu<5+V(*r>B64c5Z71GB6h#2~0~YcVqLB1iqP zQKnHiZXVzorn5JH$|Je7Y0}B!i5zm~0V7rUmZn9hak@X^&h6M?C`43589W#WgH){d zmq|#Lf8&?g6fm!NjjAaY&e2u|Yx(-yvziS-Npa9KkQhakUIw%_aR$C0X0#I71#9{T zHoDhmuq`S5T1s@Qf{@G>UaRc74jrWPGN#;o;;o=7k6GLq0xIbfR%7YXT*-3cA<)S! zyOeR^+xe9XY3h}t<|X|6K#22mD7Dq?S|?s)8k@@odAC{nB%5IarQbeEs%8>Z9({({ zY$!6J+3hq;n;Mk)H$nUMygj|Z=NwJ&`rC<3b4QGhV3^q_3Qj8kFs=J@!b_!}6CekD zmPyn{wgj$!n~?OxKG$~-Cy`uuD0X*P-`IEvX*SC9h`(Ju% zX^1@Sjr|%+<_6|^YkJ#q1y_d9oAHb7UTEO`yWdG!Y^jFHAqu`JT(NKoYUmZA$r7A} z`XW-b$^JZOG1?;evo>k_jn?nUPR$eaZ9>Y8&`qZYqz-%7Fp*H>q=0cB#uMJ+`ZNIHztPgOKMiwlQ{GiCB zRLV-A8|EJ8u6UF&7ml?RwgVqg7y+n~B$%W_=0h$Hi|}DHrjid>3P+^2F*L&dqK6;w zc*+$O?FV@&X@!9gMdBCs396g%phnuW*scQ%l=FS;rz^|=bqG?OqWU0z z13R+myHsE0GWjxjr4)3!q)ip!pgC0e^LoL(cIs(#M|&H6sWl4^x_rm_rolOyenhNs1M|?fFkOLk(yFVjDCD0WLJgv6xQ!# zxfgYpd?&=(qA2}e)Z7k{g1<%%`Uc2&gqOh<$X6`dd22)#gnF<-bT32XkeR>TDAOl5 zM|ydOhkh>cK%D)9P~vGrjsliP|C#|(wkncM5h+ddV5tQM9S z!^Jb?;o@H8M-n+>ESNN+dTf~DkHxYc0lUH)Hpzm}IgW$!k_lVpE`$&Opo*ihJD}hna zZA>(O?!{;+MIwo$*@t9!xkQ)y(S*r@1o66L}?{Gw=kg0MWTXAGX zb%}6X>5@He83T2EdeR)yCIe8!mfZoLMjz(fT4!szR zFzC z*;dMx(|TB;chQGak|YL>rbMEbAbQRm-4>VWQuJi!aBlz05&c5MAbzH0cB(7Md9M`= zj1(7F=|#CmO|q}Z!l^WOn@RrUKpmmQ+&C{h{PTos(dB9Hxk*^jnXkl!la+w`OB)4H zgth5b&*hg5r!=W@-B3-gv3ouYWZFPhpfMNq8yJpKMATK5C5}FnhF38i z5}R9adDW#CZ_!?~#^0nUk%x=g*9H`dI!s^8r6SJ#=-a`tEvZrHE4C(a=KD@keK2wi zOAB3{&L+J>NRiSd!%pj)B)lNWE(R4YK(l{&a)u{rB67B3Yt_E4oPWv67fq5yJE0U; zIXw(NY$*)s{8bptJBrX^8LA1Sq4_}2Ee@C`mX+Lgn&Rkf~&SYQdhXozKN8doy*DVen!OxM(l#w);xE_HYZhIv)DPK63 z4*unlQBbIoRSc&oKsAa`d@Pr-^c{{l?haMPv~AzD#MO?RMVyqp_^xfMm{!ZqL5U=# z(4gWje@+ndRIFDx=W-zPT4olG4Lu049Hzk_QkSf@3h?#qLGUTL8)KGS2y7V%}4DL zUj#g!=;;e8^@x2wgfmCd!ZD%eF+k(Z>aE46$_`;%SkA{r_-jkVF!0Ofuc1Ww#-R*H z(0h)+QF{Tmdq0D$EmWrHtuE`f?G(KzGYq}hsi_!K85i8;-*BqrQ(;6!T1kzeRB#Rx zf|<9hKd)+fgMH>%{vDe`bop( z5oiX!@&2zj#!&Z@nx{$r*VOj*_Ym(j(F7M~?B>NJ?1mw!s4%TglU6gTr$<*;or%|FpH0RJ0&aWe z@~F>OwMxruy1J~vzk6d9GR&HFt3pOWm>WLi{K1!A(7Tk)FRno;&3`ef>v5FUgLJl# z&42xH_a>qGfGUvWU8(m0we}^H0`!bIZvC;ONMV39E@dmwNW9V6q(E5IRk)S%nZ9~- zbWPAs+(BGfe6R$!8AX$3h?c=LVTN0B+<@Dh%l4}DuDZ7FY9=df{Ss1s<^KvZClkE^ zF7oqnRdSU;(;cxt6Bwv~Rsw-qE;3a2y*7oX8=Gbq-4x^2WP|cFv!xPJ=$cvDV2o3- znZraeAK?q32O&bj+XjeRwrcC(K2nh|NX{vCY6hD=U&dq$1K)UsX>rA^htB{(rv}0vJTK@?QhFbY+o~y9S}MHqlZ2@8&5yY z(HsXNh}luj0F+5PzjThnr=nD7juq|Bq)_|CA_eWwC)G$UqTCYj z=}>dWq1H^b%kN)b1=Q|m@&eB4x~G(#83<)i5`TQt7!H*SN0P=2V@a{Gt#h#W5^J*m-y3!vACAb14b1lDXTfQ>9h7(fq{t8+cbD1CO!0|Fg5d z;N7NoB2m|krO0g=HT5)8-Z1WNIiieQ{(?+@(sN_M0ZZKrDroM9qY9m8LO#iUw*s(&NAN|+=#jfFADgBk6Oia#0%l>n;JmiVo_r)=OVL>zCaL;;K zU~T*2oPvAJDXo%TGae%oj82X}LEe!rrAuB0Odx>`@eyZnPY)6&drgJu8n8*#ZCdeE z-#+dLNx87HdHpNtbUOS<>GKih{#D&kW@(QnM6FD*bQG+>R`Kwtn)}4ih!$mjCoFpy zktK4Dug|+|%8A*Kh541Do9u8bmaxl~6-o~O>E=>&rob`9nz;6NhlE6oaCZ&5p`|V_TEB3McLQ+9sh) z2nOjmoF<9GPd&=mX+J)0`7jj9FQkR zJr>ACxU+eXlUmi#6h~B5v(=}o;rQ0YPlU7&&ocQhO@~_^@j6Rekm6F)JX3HVf>cyr z=C6k_5=70`k6Hk&MsLl1jR*Y-$z_tZ)h6b0Gp%GQggP>@mO+-+{m=D;i9uA{ z68#o=>}F<2iskRKQ|)K|5T||e+V)%5r(e@OndPF-i~^nqWG!ih*X;G7zid~{bgsRo9!4z_ zgAd^hY8DeehT1Z;DDVIn}v>99p`Vpehc(-wes5)uov6+PdtRd~SfKNEYH1+;zF_`6mh;XcKO}3uoQ6*4B25q3H4Ul!=YCYRQ*9%!h{^~7 zBU}=a=A3Qw2gEoeo&hp3Z{pzFQ86;}kPQ8^d^lD8r9QTVq_dwp2}I$H$87PD{SLoR zQ?U?VZZQ&K!{wHr9Ln;sF&-9h7Vixk6no`)UQv8l6(4Si))x2H#3+LUYgDUo|L$Q)xu3)(%NDNMR?dgoi$o?Ze!2SjadT zXmu#aKT5+z;Dj2F@GJYaQR_4J9n_s4hK6>}{p(N#IRKZbru$@2Vz%N%uw0dV*k6vN z)n=8hsZzNX5q}oJT87aUFNy=D#z=o<&r1*+<*7K+DWNKZOx;kbB^&(YW(dIsfHo(lejCd;I;JbJ`QnQIc)O%F7PB z5JJRU!u>N=V5U8mSk^q_qDH~?*fNu#EuwaMOi-Ue-*Q`$H7gTl8Znq4Kr+yXkmhQU zo2!u_k_cdSj8UdK{vU|GnNiW3`MRfc z6kauO-#1n*gO1xd_9p8-j?Jx$3}XLIXsjmLa^z85%H#H0CnNsU*wh$0chB?>@_jQgxe2!4lcYQvJ@51M} zmOUKuDco-RJatrSe|7|g?bjd)wmxCopID>x&7a+j!n{_oD}{KyRAgyhI!yrsVrIa^ zD0Deej4TuI{@;ni_8#*{S0aI=V398)BMpT5nK)OW5!`2G&^+0I5?jT$r-?@{sQPFkTVuwk=8pHIj-Y)TARq#b+Jo5 zWQ*U!}i%9SHZ#m)k zVwj}aq*%emi#5aR=5PdBTL^7h{1HU-L2m9wT9cRxuPS(!H$Zn-0Q2G=wvIy6y>u{h z!#LaX&6cCm*yemAqI;vZe734?eu$vE9JTw=%ZWHZZU9ZF%JQnZE9&{WDc>U`^~zqKs{cbE{-7_*OL4G#+6tlVE%{ul>ZKl0U44^-Uu646Bpt7(&>UFVdQ&MBb@07;l-RR910 literal 0 HcmV?d00001 diff --git a/packages/desktop/resources/tray-iconTemplate@2x.png b/packages/desktop/resources/tray-iconTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e5b4106fbb019c6796114b84105028787b4598 GIT binary patch literal 426 zcmeAS@N?(olHy`uVBq!ia0vp^4j{}3Bp8~6e2Re-Q%R6tFatx`)^mS!_#Vp8REuD1p-!7n#YKdz^NlIc#s#S7P zDv)9@GB7gJH89pSGz&2@wlXlcGPcwrcJV#O3?Wow4iNry#hRnf2O(Znon;+JCa> z8V5T_-P>+ZTzyn~)0c?m6G4S<|8$#fP+fIsSq8`NV}XgLB9@<6QaQ>rCO&%4=(Wb> z_B-aOg1f|Lcc?nC>zC~^{9qLQbKdi!jM`;?Zp`k^F|pb3w_;1jZlDtxJYD@<);T3K F0RWfwi^~82 literal 0 HcmV?d00001 diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 910ff081..726e50bb 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -126,6 +126,67 @@ function saveWindowState(win: BrowserWindow): void { } } +// ─── Auto-Launch Settings ──────────────────────────────────────────────────── + +interface AutoLaunchSettings { + openAtLogin: boolean; + startMinimized: boolean; +} + +const DEFAULT_AUTO_LAUNCH: AutoLaunchSettings = { + openAtLogin: false, + startMinimized: true, +}; + +function getAutoLaunchSettingsPath(): string { + return path.join(app.getPath('userData'), 'auto-launch.json'); +} + +function loadAutoLaunchSettings(): AutoLaunchSettings { + try { + const raw = fs.readFileSync(getAutoLaunchSettingsPath(), 'utf-8'); + const parsed = JSON.parse(raw) as Partial; + return { + openAtLogin: typeof parsed.openAtLogin === 'boolean' ? parsed.openAtLogin : DEFAULT_AUTO_LAUNCH.openAtLogin, + startMinimized: typeof parsed.startMinimized === 'boolean' ? parsed.startMinimized : DEFAULT_AUTO_LAUNCH.startMinimized, + }; + } catch { + return { ...DEFAULT_AUTO_LAUNCH }; + } +} + +function saveAutoLaunchSettings(settings: AutoLaunchSettings): void { + fs.writeFileSync(getAutoLaunchSettingsPath(), JSON.stringify(settings)); +} + +function applyLoginItemSettings(openAtLogin: boolean, startMinimized: boolean): void { + if (process.platform === 'darwin') { + app.setLoginItemSettings({ + openAtLogin, + openAsHidden: startMinimized, + }); + } else if (process.platform === 'win32') { + app.setLoginItemSettings({ + openAtLogin, + args: startMinimized ? ['--hidden'] : [], + name: 'Backspace', + }); + } else { + // Linux: setLoginItemSettings creates a .desktop file in ~/.config/autostart/ + // For AppImage, the path changes on update — we pass it explicitly. + // Electron's Linux impl doesn't support `path`/`args` in types, + // but the runtime does accept them in the options object. + const opts: Record = { openAtLogin }; + if (process.env.APPIMAGE) { + opts.path = process.env.APPIMAGE; + } + if (startMinimized) { + opts.args = ['--hidden']; + } + app.setLoginItemSettings(opts as Electron.Settings); + } +} + // ─── Tray Icon ────────────────────────────────────────────────────────────── function generateFallbackTrayIcon(): Electron.NativeImage { @@ -139,9 +200,10 @@ function generateFallbackTrayIcon(): Electron.NativeImage { const idx = (y * size + x) * 4; const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2); if (dist <= r) { - canvas[idx] = 0x58; // R (blurple) + // NativeImage raw buffer uses BGRA on most platforms + canvas[idx] = 0xf2; // B (blurple #5865f2) canvas[idx + 1] = 0x65; // G - canvas[idx + 2] = 0xf2; // B + canvas[idx + 2] = 0x58; // R canvas[idx + 3] = 0xff; // A } else { canvas[idx] = 0; @@ -155,15 +217,24 @@ function generateFallbackTrayIcon(): Electron.NativeImage { } function loadTrayIcon(): Electron.NativeImage { - const iconPath = path.join(__dirname, '..', 'build', 'tray-icon.png'); + const resourcesDir = path.join(__dirname, '..', 'resources'); try { - const icon = nativeImage.createFromPath(iconPath); - if (!icon.isEmpty()) { - const resized = icon.resize({ width: 16, height: 16 }); - if (process.platform === 'darwin') { - resized.setTemplateImage(true); + if (process.platform === 'darwin') { + // macOS template image: Electron auto-resolves @2x from the base path. + // Template images adapt to light/dark menu bar automatically. + const templatePath = path.join(resourcesDir, 'tray-iconTemplate.png'); + const icon = nativeImage.createFromPath(templatePath); + if (!icon.isEmpty()) { + icon.setTemplateImage(true); + return icon; + } + } else { + // Windows/Linux: colored icon + const iconPath = path.join(resourcesDir, 'tray-icon.png'); + const icon = nativeImage.createFromPath(iconPath); + if (!icon.isEmpty()) { + return icon.resize({ width: 16, height: 16 }); } - return resized; } } catch { // Fall through to generated icon @@ -225,7 +296,14 @@ function createWindow(): void { } mainWindow.once('ready-to-show', () => { - mainWindow?.show(); + // Check if launched minimized (auto-start to tray) + const launchedHidden = + process.argv.includes('--hidden') || + (process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden); + + if (!launchedHidden) { + mainWindow?.show(); + } // Send any pending deep link that launched the app if (pendingDeepLink && mainWindow) { @@ -436,6 +514,34 @@ function registerIpcHandlers(): void { // Handled via ipcMain.once in the display media handler — this is just // a safety net to prevent unhandled-message warnings }); + + // Auto-launch settings + ipcMain.handle('get-auto-launch-settings', (): { openAtLogin: boolean; startMinimized: boolean } => { + const saved = loadAutoLaunchSettings(); + const osState = app.getLoginItemSettings(); + return { + openAtLogin: osState.openAtLogin, + startMinimized: saved.startMinimized, + }; + }); + + ipcMain.handle('set-auto-launch-settings', (_event, settings: { openAtLogin?: boolean; startMinimized?: boolean }) => { + const current = loadAutoLaunchSettings(); + const osState = app.getLoginItemSettings(); + + const newOpenAtLogin = settings.openAtLogin ?? osState.openAtLogin; + const newStartMinimized = settings.startMinimized ?? current.startMinimized; + + const updated: AutoLaunchSettings = { + openAtLogin: newOpenAtLogin, + startMinimized: newStartMinimized, + }; + + saveAutoLaunchSettings(updated); + applyLoginItemSettings(newOpenAtLogin, newStartMinimized); + + return updated; + }); } // ─── Auto-Update ──────────────────────────────────────────────────────────── @@ -663,6 +769,10 @@ if (!gotTheLock) { createTray(); initAutoUpdater(); + // Sync auto-launch settings with OS on startup (refreshes login item path for AppImage updates) + const autoLaunchSettings = loadAutoLaunchSettings(); + applyLoginItemSettings(autoLaunchSettings.openAtLogin, autoLaunchSettings.startMinimized); + // Check if the app was launched with a deep link (Windows/Linux) const launchArg = process.argv.find((arg) => arg.startsWith('backspace://')); if (launchArg) { diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index a22e652d..236a45e2 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -62,4 +62,9 @@ contextBridge.exposeInMainWorld('backspace', { getInstanceUrl: () => ipcRenderer.invoke('get-instance-url'), setInstanceUrl: (url: string) => ipcRenderer.invoke('set-instance-url', url), clearInstanceUrl: () => ipcRenderer.invoke('clear-instance-url'), + + // Auto-launch settings + getAutoLaunchSettings: () => ipcRenderer.invoke('get-auto-launch-settings'), + setAutoLaunchSettings: (settings: { openAtLogin?: boolean; startMinimized?: boolean }) => + ipcRenderer.invoke('set-auto-launch-settings', settings), }); diff --git a/packages/web/src/components/modals/settingsPanels/AccountPanel.tsx b/packages/web/src/components/modals/settingsPanels/AccountPanel.tsx index fe3c446d..e5b4e9f2 100644 --- a/packages/web/src/components/modals/settingsPanels/AccountPanel.tsx +++ b/packages/web/src/components/modals/settingsPanels/AccountPanel.tsx @@ -10,8 +10,70 @@ import { AVATAR_COLORS } from '@backspace/shared'; import type { User, UserStatus, AvatarColor } from '@backspace/shared'; import type { FederationOpResult } from '../../../utils/federationOps'; import { isElectron } from '../../../platform/platform'; +import { Toggle } from '../../ui/Toggle'; +function AutoLaunchSettings() { + const [openAtLogin, setOpenAtLogin] = useState(false); + const [startMinimized, setStartMinimized] = useState(true); + const [loading, setLoading] = useState(true); + + useEffect(() => { + window.backspace?.getAutoLaunchSettings().then((settings) => { + setOpenAtLogin(settings.openAtLogin); + setStartMinimized(settings.startMinimized); + setLoading(false); + }).catch(() => setLoading(false)); + }, []); + + const handleOpenAtLoginChange = async (enabled: boolean) => { + setOpenAtLogin(enabled); + try { + const result = await window.backspace!.setAutoLaunchSettings({ openAtLogin: enabled }); + setOpenAtLogin(result.openAtLogin); + setStartMinimized(result.startMinimized); + } catch { + setOpenAtLogin(!enabled); + } + }; + + const handleStartMinimizedChange = async (enabled: boolean) => { + setStartMinimized(enabled); + try { + const result = await window.backspace!.setAutoLaunchSettings({ startMinimized: enabled }); + setOpenAtLogin(result.openAtLogin); + setStartMinimized(result.startMinimized); + } catch { + setStartMinimized(!enabled); + } + }; + + if (loading) return null; + + return ( + <> +
+
+
Start at boot
+
+ Automatically launch Backspace when you log in +
+
+ +
+
+
+
Start minimized
+
+ Start hidden in the system tray instead of showing the window +
+
+ +
+ + ); +} + export function AccountPanel() { const user = useAuthStore((s) => s.user); const updateProfile = useAuthStore((s) => s.updateProfile); @@ -688,11 +750,15 @@ export function AccountPanel() { - {/* ── Connected Instance (Electron only) ── */} + {/* ── Desktop (Electron only) ── */} {isElectron() && (
-
Connected Instance
-
+
Desktop
+
+ + +
+
{window.location.origin}
diff --git a/packages/web/src/platform/electron.d.ts b/packages/web/src/platform/electron.d.ts index 6bfd1a4c..d8da18f4 100644 --- a/packages/web/src/platform/electron.d.ts +++ b/packages/web/src/platform/electron.d.ts @@ -42,6 +42,11 @@ interface BackspaceElectronAPI { getInstanceUrl: () => Promise; setInstanceUrl: (url: string) => Promise; clearInstanceUrl: () => Promise; + + // Auto-launch settings + getAutoLaunchSettings: () => Promise<{ openAtLogin: boolean; startMinimized: boolean }>; + setAutoLaunchSettings: (settings: { openAtLogin?: boolean; startMinimized?: boolean }) => + Promise<{ openAtLogin: boolean; startMinimized: boolean }>; } interface Window {