feat(export): wire ExportService — tray '내보내기...' menu + dialog

Tray now has 4th callback that opens directory chooser, exports all
notes via ExportService with includeMedia=true default. Dialog
message warns about raw_text plain-text + recommends private location.
Native toast on success/failure with note + media counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-04-26 10:44:38 +09:00
parent 9fdfd6610c
commit 27666178a2
2 changed files with 45 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
import electron from 'electron';
const { app, BrowserWindow, Notification } = electron;
const { app, BrowserWindow, Notification, dialog } = electron;
import '@shared/types';
import { existsSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
@@ -25,6 +25,7 @@ import {
import { createTray } from './tray.js';
import { MediaGc } from './services/MediaGc.js';
import { BackupService } from './services/BackupService.js';
import { ExportService } from './services/ExportService.js';
const HIDDEN_ARG = '--hidden';
const startedHidden = process.argv.includes(HIDDEN_ARG);
@@ -102,6 +103,8 @@ app.whenReady().then(async () => {
const gc = new MediaGc(db, store);
void gc.run().then((r) => logger.info('media.gc', { ...r } as Record<string, unknown>));
const exportSvc = new ExportService(repo, store);
const backup = new BackupService(db, join(paths.profileDir, 'backups'));
void backup.runDaily()
.then((r) => logger.info('backup.daily', { ...r } as Record<string, unknown>))
@@ -142,6 +145,40 @@ app.whenReady().then(async () => {
silent: true
}).show();
}
},
async () => {
const win = getInboxWindow();
const dialogOpts: Electron.OpenDialogOptions = {
title: '내보낼 폴더 선택',
message: '선택한 폴더에 노트를 마크다운으로 내보냅니다. 이미지가 함께 포함됩니다. raw_text 가 평문으로 보관되니 비공개 위치를 권장합니다.',
buttonLabel: '여기에 내보내기',
properties: ['openDirectory', 'createDirectory']
};
const result = win
? await dialog.showOpenDialog(win, dialogOpts)
: await dialog.showOpenDialog(dialogOpts);
if (result.canceled || result.filePaths.length === 0) return;
try {
const r = await exportSvc.export(result.filePaths[0]!, { includeMedia: true });
logger.info('export.done', {
outDir: r.outDir,
noteCount: r.noteCount,
mediaCount: r.mediaCount,
bytes: r.bytes
});
new Notification({
title: 'Inkling',
body: `내보내기 완료 — 노트 ${r.noteCount}개, 이미지 ${r.mediaCount}`,
silent: true
}).show();
} catch (e) {
logger.warn('export.failed', { reason: String(e) });
new Notification({
title: 'Inkling',
body: '내보내기를 완료하지 못했습니다.',
silent: true
}).show();
}
}
);

View File

@@ -7,13 +7,15 @@ let tray: TrayType | null = null;
function buildMenu(
showInbox: () => void,
showCapture: () => void,
runBackup: () => void
runBackup: () => void,
runExport: () => void
) {
const items: MenuItemConstructorOptions[] = [
{ label: '구출한 메모 보기', click: showInbox },
{ label: '기억 구출하기', click: showCapture },
{ type: 'separator' },
{ label: '지금 백업', click: runBackup }
{ label: '지금 백업', click: runBackup },
{ label: '내보내기...', click: runExport }
];
if (app.isPackaged) {
const { openAtLogin } = app.getLoginItemSettings();
@@ -39,12 +41,13 @@ function buildMenu(
export function createTray(
showInbox: () => void,
showCapture: () => void,
runBackup: () => void
runBackup: () => void,
runExport: () => void
): TrayType {
const icon = nativeImage.createEmpty();
tray = new Tray(icon);
tray.setToolTip('Inkling');
tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup));
tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup, runExport));
tray.on('click', showInbox);
return tray;
}