build: generate desktop application release artifacts and update project configuration

This commit is contained in:
Firman Ramdhani
2026-04-05 22:33:08 +07:00
parent 9b7e027024
commit a13feb1c51
20 changed files with 3183 additions and 31 deletions
@@ -0,0 +1,35 @@
// 1. Import hooks yang baru saja dibuat Opus
import { Button } from '@repo/ui/components';
import { useElectronPrinter } from '../../hooks/use-electron-printer';
import { useElectronUpdater } from '../../hooks/use-electron-updater';
export default function App() {
// 2. Panggil hooks-nya
const { printers, refreshPrinters } = useElectronPrinter();
const { status } = useElectronUpdater();
return (
<div style={{ padding: '20px', border: '2px solid blue', margin: '20px' }}>
<h2>πŸ§ͺ Test Integrasi Electron</h2>
<p><strong>Status Auto-Update:</strong> {status}</p>
<Button variant="filled" color="brand" onClick={refreshPrinters}>
Refresh Printer
</Button>
<h3>πŸ–¨οΈ Daftar Printer di Komputer Ini:</h3>
<ul>
{printers.length === 0 ? (
<li>Mencari printer... (Atau tidak ada printer terdeteksi)</li>
) : (
printers.map((printer, index) => (
<li key={index}>
{printer.name} {printer.isDefault ? '(Default)' : ''}
</li>
))
)}
</ul>
</div>
);
}
@@ -19,6 +19,7 @@ import {
Badge,
Divider,
} from '@repo/ui/components';
import PrinterList from './printer-list'
interface ShowcaseViewProps {
colorScheme: ColorSchemeType;
@@ -204,6 +205,9 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
</Table.Tbody>
</Table>
</Card>
<Card withBorder shadow="sm" radius="md" p="md">
<PrinterList />
</Card>
</Stack>
</Container>
);
+109
View File
@@ -0,0 +1,109 @@
import { useState, useCallback } from 'react';
// ─── Types ──────────────────────────────────────────────────────
export interface UseElectronPrinterReturn {
/** List of available printers (populated after calling `refreshPrinters`) */
printers: ElectronPrinterInfo[];
/** Whether a printer operation is in progress */
loading: boolean;
/** Last error message, if any */
error: string | null;
/** Whether the app is running inside Electron */
isElectron: boolean;
/** Fetch the current list of available printers */
refreshPrinters: () => Promise<ElectronPrinterInfo[]>;
/** Print with the given options. Returns success/failure. */
print: (options?: ElectronPrintOptions) => Promise<ElectronPrintResult>;
}
/**
* React hook for Electron printer integration.
*
* Provides methods to list available printers and trigger print jobs
* via the secure `window.electronAPI` bridge.
*
* Safe to use in both Electron and browser environments.
*
* @example
* ```tsx
* function PrintButton() {
* const { printers, refreshPrinters, print, loading } = useElectronPrinter();
*
* useEffect(() => { refreshPrinters(); }, []);
*
* const handlePrint = async () => {
* const result = await print({ silent: true, deviceName: printers[0]?.name });
* if (!result.success) alert(`Print failed: ${result.failureReason}`);
* };
*
* return (
* <button onClick={handlePrint} disabled={loading || printers.length === 0}>
* Print
* </button>
* );
* }
* ```
*/
export function useElectronPrinter(): UseElectronPrinterReturn {
const [printers, setPrinters] = useState<ElectronPrinterInfo[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
const refreshPrinters = useCallback(async (): Promise<ElectronPrinterInfo[]> => {
if (!window.electronAPI) {
setError('Not running in Electron');
return [];
}
setLoading(true);
setError(null);
try {
const result = await window.electronAPI.getPrinters();
setPrinters(result);
return result;
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to get printers';
setError(message);
return [];
} finally {
setLoading(false);
}
}, []);
const print = useCallback(
async (options?: ElectronPrintOptions): Promise<ElectronPrintResult> => {
if (!window.electronAPI) {
return { success: false, failureReason: 'Not running in Electron' };
}
setLoading(true);
setError(null);
try {
const result = await window.electronAPI.print(options);
if (!result.success && result.failureReason) {
setError(result.failureReason);
}
return result;
} catch (err) {
const message = err instanceof Error ? err.message : 'Print failed';
setError(message);
return { success: false, failureReason: message };
} finally {
setLoading(false);
}
},
[],
);
return {
printers,
loading,
error,
isElectron,
refreshPrinters,
print,
};
}
+139
View File
@@ -0,0 +1,139 @@
import { useState, useEffect, useCallback } from 'react';
// ─── Types ──────────────────────────────────────────────────────
export type UpdateStatus =
| 'idle'
| 'checking'
| 'available'
| 'not-available'
| 'downloading'
| 'ready'
| 'error';
export interface UseElectronUpdaterReturn {
/** Current status of the auto-updater lifecycle */
status: UpdateStatus;
/** Download progress percentage (0–100) */
progress: number;
/** Download speed in bytes per second */
bytesPerSecond: number;
/** Information about the available/downloaded update */
updateInfo: ElectronUpdateInfo | null;
/** Error message if the updater encountered an issue */
errorMessage: string | null;
/** Whether the app is running inside Electron */
isElectron: boolean;
/** Trigger a manual update check */
checkForUpdates: () => void;
/** Quit the app and install the downloaded update */
installUpdate: () => void;
}
/**
* React hook for the Electron auto-updater.
*
* Subscribes to all update lifecycle events via `window.electronAPI`
* and provides reactive state for building an update notification UI.
*
* Safe to use in both Electron and browser environments β€” all
* Electron-specific calls are gated behind `window.electronAPI` checks.
*
* @example
* ```tsx
* function UpdateBanner() {
* const { status, progress, updateInfo, checkForUpdates, installUpdate } = useElectronUpdater();
*
* if (status === 'available') {
* return <div>Update {updateInfo?.version} available! Downloading...</div>;
* }
* if (status === 'downloading') {
* return <div>Downloading... {progress.toFixed(0)}%</div>;
* }
* if (status === 'ready') {
* return <button onClick={installUpdate}>Restart to update</button>;
* }
* return <button onClick={checkForUpdates}>Check for updates</button>;
* }
* ```
*/
export function useElectronUpdater(): UseElectronUpdaterReturn {
const [status, setStatus] = useState<UpdateStatus>('idle');
const [progress, setProgress] = useState(0);
const [bytesPerSecond, setBytesPerSecond] = useState(0);
const [updateInfo, setUpdateInfo] = useState<ElectronUpdateInfo | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
useEffect(() => {
if (!window.electronAPI) return;
const api = window.electronAPI;
const unsubChecking = api.onUpdateChecking(() => {
setStatus('checking');
setErrorMessage(null);
});
const unsubAvailable = api.onUpdateAvailable((info) => {
setStatus('available');
setUpdateInfo(info);
});
const unsubNotAvailable = api.onUpdateNotAvailable((info) => {
setStatus('not-available');
setUpdateInfo(info);
});
const unsubProgress = api.onDownloadProgress((progressInfo) => {
setStatus('downloading');
setProgress(progressInfo.percent);
setBytesPerSecond(progressInfo.bytesPerSecond);
});
const unsubDownloaded = api.onUpdateDownloaded((info) => {
setStatus('ready');
setProgress(100);
setUpdateInfo(info);
});
const unsubError = api.onUpdateError((error) => {
setStatus('error');
setErrorMessage(error);
});
// Cleanup all listeners on unmount
return () => {
unsubChecking();
unsubAvailable();
unsubNotAvailable();
unsubProgress();
unsubDownloaded();
unsubError();
};
}, []);
const checkForUpdates = useCallback(() => {
if (!window.electronAPI) return;
setStatus('checking');
setErrorMessage(null);
window.electronAPI.checkForUpdates();
}, []);
const installUpdate = useCallback(() => {
if (!window.electronAPI) return;
window.electronAPI.installUpdate();
}, []);
return {
status,
progress,
bytesPerSecond,
updateInfo,
errorMessage,
isElectron,
checkForUpdates,
installUpdate,
};
}
+98
View File
@@ -0,0 +1,98 @@
/**
* Type declarations for the Electron preload API.
*
* When running inside Electron, `window.electronAPI` is defined.
* When running in a regular browser, it is `undefined`.
*
* Usage:
* if (window.electronAPI) {
* const printers = await window.electronAPI.getPrinters();
* }
*/
// ─── Printer types ──────────────────────────────────────────────
interface ElectronPrinterInfo {
name: string;
displayName: string;
description: string;
status: number;
isDefault: boolean;
options?: Record<string, string>;
}
interface ElectronPrintOptions {
silent?: boolean;
printBackground?: boolean;
deviceName?: string;
color?: boolean;
margins?: {
marginType?: 'default' | 'none' | 'printableArea' | 'custom';
top?: number;
bottom?: number;
left?: number;
right?: number;
};
landscape?: boolean;
scaleFactor?: number;
pagesPerSheet?: number;
collate?: boolean;
copies?: number;
pageRanges?: Array<{ from: number; to: number }>;
duplexMode?: 'simplex' | 'shortEdge' | 'longEdge';
header?: string;
footer?: string;
}
interface ElectronPrintResult {
success: boolean;
failureReason?: string;
}
// ─── Auto-Update types ──────────────────────────────────────────
interface ElectronUpdateInfo {
version: string;
releaseDate: string;
releaseName?: string | null;
releaseNotes?: string | null;
}
interface ElectronProgressInfo {
total: number;
delta: number;
transferred: number;
percent: number;
bytesPerSecond: number;
}
// ─── ElectronAPI interface ──────────────────────────────────────
interface ElectronAPI {
// Printing
getPrinters: () => Promise<ElectronPrinterInfo[]>;
print: (options?: ElectronPrintOptions) => Promise<ElectronPrintResult>;
// Auto-Update: Commands
checkForUpdates: () => void;
installUpdate: () => void;
// Auto-Update: Event Subscriptions
// Each returns an unsubscribe function.
onUpdateChecking: (callback: () => void) => () => void;
onUpdateAvailable: (callback: (info: ElectronUpdateInfo) => void) => () => void;
onUpdateNotAvailable: (callback: (info: ElectronUpdateInfo) => void) => () => void;
onDownloadProgress: (callback: (progress: ElectronProgressInfo) => void) => () => void;
onUpdateDownloaded: (callback: (info: ElectronUpdateInfo) => void) => () => void;
onUpdateError: (callback: (error: string) => void) => () => void;
}
// ─── Augment the global Window interface ────────────────────────
interface Window {
/**
* Available only when running inside Electron.
* Always check `if (window.electronAPI)` before use.
*/
electronAPI?: ElectronAPI;
}