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.
This commit is contained in:
@@ -9,7 +9,7 @@ files:
|
|||||||
- "!node_modules"
|
- "!node_modules"
|
||||||
publish:
|
publish:
|
||||||
- provider: generic
|
- provider: generic
|
||||||
url: "${BACKSPACE_UPDATE_URL}"
|
url: "${env.BACKSPACE_UPDATE_URL}"
|
||||||
useMultipleRangeRequest: false
|
useMultipleRangeRequest: false
|
||||||
protocols:
|
protocols:
|
||||||
- name: Backspace
|
- name: Backspace
|
||||||
@@ -24,6 +24,7 @@ mac:
|
|||||||
- dmg
|
- dmg
|
||||||
- zip
|
- zip
|
||||||
win:
|
win:
|
||||||
|
icon: ./build/icon.ico
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
linux:
|
linux:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 349 B |
Binary file not shown.
|
After Width: | Height: | Size: 426 B |
@@ -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<AutoLaunchSettings>;
|
||||||
|
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<string, unknown> = { openAtLogin };
|
||||||
|
if (process.env.APPIMAGE) {
|
||||||
|
opts.path = process.env.APPIMAGE;
|
||||||
|
}
|
||||||
|
if (startMinimized) {
|
||||||
|
opts.args = ['--hidden'];
|
||||||
|
}
|
||||||
|
app.setLoginItemSettings(opts as Electron.Settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Tray Icon ──────────────────────────────────────────────────────────────
|
// ─── Tray Icon ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function generateFallbackTrayIcon(): Electron.NativeImage {
|
function generateFallbackTrayIcon(): Electron.NativeImage {
|
||||||
@@ -139,9 +200,10 @@ function generateFallbackTrayIcon(): Electron.NativeImage {
|
|||||||
const idx = (y * size + x) * 4;
|
const idx = (y * size + x) * 4;
|
||||||
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
|
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
|
||||||
if (dist <= r) {
|
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 + 1] = 0x65; // G
|
||||||
canvas[idx + 2] = 0xf2; // B
|
canvas[idx + 2] = 0x58; // R
|
||||||
canvas[idx + 3] = 0xff; // A
|
canvas[idx + 3] = 0xff; // A
|
||||||
} else {
|
} else {
|
||||||
canvas[idx] = 0;
|
canvas[idx] = 0;
|
||||||
@@ -155,15 +217,24 @@ function generateFallbackTrayIcon(): Electron.NativeImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadTrayIcon(): Electron.NativeImage {
|
function loadTrayIcon(): Electron.NativeImage {
|
||||||
const iconPath = path.join(__dirname, '..', 'build', 'tray-icon.png');
|
const resourcesDir = path.join(__dirname, '..', 'resources');
|
||||||
try {
|
try {
|
||||||
|
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);
|
const icon = nativeImage.createFromPath(iconPath);
|
||||||
if (!icon.isEmpty()) {
|
if (!icon.isEmpty()) {
|
||||||
const resized = icon.resize({ width: 16, height: 16 });
|
return icon.resize({ width: 16, height: 16 });
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
resized.setTemplateImage(true);
|
|
||||||
}
|
}
|
||||||
return resized;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to generated icon
|
// Fall through to generated icon
|
||||||
@@ -225,7 +296,14 @@ function createWindow(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-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();
|
mainWindow?.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Send any pending deep link that launched the app
|
// Send any pending deep link that launched the app
|
||||||
if (pendingDeepLink && mainWindow) {
|
if (pendingDeepLink && mainWindow) {
|
||||||
@@ -436,6 +514,34 @@ function registerIpcHandlers(): void {
|
|||||||
// Handled via ipcMain.once in the display media handler — this is just
|
// Handled via ipcMain.once in the display media handler — this is just
|
||||||
// a safety net to prevent unhandled-message warnings
|
// 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 ────────────────────────────────────────────────────────────
|
// ─── Auto-Update ────────────────────────────────────────────────────────────
|
||||||
@@ -663,6 +769,10 @@ if (!gotTheLock) {
|
|||||||
createTray();
|
createTray();
|
||||||
initAutoUpdater();
|
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)
|
// Check if the app was launched with a deep link (Windows/Linux)
|
||||||
const launchArg = process.argv.find((arg) => arg.startsWith('backspace://'));
|
const launchArg = process.argv.find((arg) => arg.startsWith('backspace://'));
|
||||||
if (launchArg) {
|
if (launchArg) {
|
||||||
|
|||||||
@@ -62,4 +62,9 @@ contextBridge.exposeInMainWorld('backspace', {
|
|||||||
getInstanceUrl: () => ipcRenderer.invoke('get-instance-url'),
|
getInstanceUrl: () => ipcRenderer.invoke('get-instance-url'),
|
||||||
setInstanceUrl: (url: string) => ipcRenderer.invoke('set-instance-url', url),
|
setInstanceUrl: (url: string) => ipcRenderer.invoke('set-instance-url', url),
|
||||||
clearInstanceUrl: () => ipcRenderer.invoke('clear-instance-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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,70 @@ import { AVATAR_COLORS } from '@backspace/shared';
|
|||||||
import type { User, UserStatus, AvatarColor } from '@backspace/shared';
|
import type { User, UserStatus, AvatarColor } from '@backspace/shared';
|
||||||
import type { FederationOpResult } from '../../../utils/federationOps';
|
import type { FederationOpResult } from '../../../utils/federationOps';
|
||||||
import { isElectron } from '../../../platform/platform';
|
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 (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between py-1">
|
||||||
|
<div className="flex-1 mr-4">
|
||||||
|
<div className="text-sm text-txt-primary">Start at boot</div>
|
||||||
|
<div className="text-xs text-txt-tertiary mt-0.5">
|
||||||
|
Automatically launch Backspace when you log in
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toggle enabled={openAtLogin} onChange={handleOpenAtLoginChange} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1">
|
||||||
|
<div className="flex-1 mr-4">
|
||||||
|
<div className={`text-sm ${openAtLogin ? 'text-txt-primary' : 'text-txt-tertiary'}`}>Start minimized</div>
|
||||||
|
<div className="text-xs text-txt-tertiary mt-0.5">
|
||||||
|
Start hidden in the system tray instead of showing the window
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toggle enabled={startMinimized} onChange={handleStartMinimizedChange} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AccountPanel() {
|
export function AccountPanel() {
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const updateProfile = useAuthStore((s) => s.updateProfile);
|
const updateProfile = useAuthStore((s) => s.updateProfile);
|
||||||
@@ -688,11 +750,15 @@ export function AccountPanel() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Connected Instance (Electron only) ── */}
|
{/* ── Desktop (Electron only) ── */}
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">Connected Instance</div>
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">Desktop</div>
|
||||||
<div className="rounded-lg bg-white/[0.03] border border-white/[0.04] p-3.5">
|
<div className="rounded-lg bg-white/[0.03] border border-white/[0.04] p-3.5 space-y-3">
|
||||||
|
<AutoLaunchSettings />
|
||||||
|
|
||||||
|
<div className="border-t border-white/[0.04]" />
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-txt-primary font-medium">{window.location.origin}</div>
|
<div className="text-sm text-txt-primary font-medium">{window.location.origin}</div>
|
||||||
|
|||||||
+5
@@ -42,6 +42,11 @@ interface BackspaceElectronAPI {
|
|||||||
getInstanceUrl: () => Promise<string | null>;
|
getInstanceUrl: () => Promise<string | null>;
|
||||||
setInstanceUrl: (url: string) => Promise<void>;
|
setInstanceUrl: (url: string) => Promise<void>;
|
||||||
clearInstanceUrl: () => Promise<void>;
|
clearInstanceUrl: () => Promise<void>;
|
||||||
|
|
||||||
|
// Auto-launch settings
|
||||||
|
getAutoLaunchSettings: () => Promise<{ openAtLogin: boolean; startMinimized: boolean }>;
|
||||||
|
setAutoLaunchSettings: (settings: { openAtLogin?: boolean; startMinimized?: boolean }) =>
|
||||||
|
Promise<{ openAtLogin: boolean; startMinimized: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
Reference in New Issue
Block a user