build: generate desktop application release artifacts and update project configuration
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Vendored
+98
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user