Compare commits
3 Commits
aa55a42988
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c34fa580cd | |||
| fd600001a0 | |||
| d60832120e |
@@ -1,3 +1,8 @@
|
||||
### VS Code
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### IntelliJ
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "앱 실행",
|
||||
"type": "shell",
|
||||
"command": "npm start",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "앱 실행 (디버그)",
|
||||
"type": "shell",
|
||||
"command": "npm run debug",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+28
-2
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "photo-renamer",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "photo-renamer",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1"
|
||||
}
|
||||
@@ -303,6 +304,13 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -1496,6 +1504,24 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@epic-web/invariant": "^1.0.0",
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "dist/bin/cross-env.js",
|
||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
||||
+32
-6
@@ -1,22 +1,47 @@
|
||||
{
|
||||
"name": "photo-renamer",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.0",
|
||||
"description": "사진 파일 이름 정리 앱",
|
||||
"main": "src/main/index.js",
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=development electron .",
|
||||
"debug": "NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 . 2>&1 | tee debug.log",
|
||||
"build": "NODE_ENV=production electron-builder --mac --dir",
|
||||
"dist": "NODE_ENV=production electron-builder --mac"
|
||||
"start": "cross-env NODE_ENV=development electron .",
|
||||
"debug": "cross-env NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 .",
|
||||
"build": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win --dir",
|
||||
"dist": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
|
||||
"build:mac": "cross-env NODE_ENV=production electron-builder --mac --dir",
|
||||
"dist:mac": "cross-env NODE_ENV=production electron-builder --mac"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.photorenamer.app",
|
||||
"productName": "Photo Renamer",
|
||||
"win": {
|
||||
"icon": "assets/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.photography",
|
||||
"icon": "assets/icon.icns",
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["arm64", "x64"] }
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
@@ -26,6 +51,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1"
|
||||
}
|
||||
|
||||
+39
-29
@@ -1,16 +1,16 @@
|
||||
const { app, BrowserWindow, screen } = require('electron');
|
||||
const path = require('path');
|
||||
const { registerAll } = require('./ipc');
|
||||
const { readConfig, updateConfig } = require('./config');
|
||||
const logger = require('./logger');
|
||||
const { app, BrowserWindow, screen, Menu } = require("electron");
|
||||
const path = require("path");
|
||||
const { registerAll } = require("./ipc");
|
||||
const { readConfig, updateConfig } = require("./config");
|
||||
const logger = require("./logger");
|
||||
|
||||
app.setName('Photo Renamer');
|
||||
app.setName("Photo Renamer");
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
const windowState = getSavedWindowState();
|
||||
logger.log('main', 'createWindow', windowState);
|
||||
logger.log("main", "createWindow", windowState);
|
||||
mainWindow = new BrowserWindow({
|
||||
x: windowState.bounds?.x,
|
||||
y: windowState.bounds?.y,
|
||||
@@ -18,33 +18,36 @@ function createWindow() {
|
||||
height: windowState.bounds?.height || 820,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
backgroundColor: '#0f0f17',
|
||||
icon: path.join(__dirname, '../../assets/icon.icns'),
|
||||
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
|
||||
backgroundColor: "#0f0f17",
|
||||
icon: path.join(__dirname, "../../assets/icon.icns"),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
preload: path.join(__dirname, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
logger.log('main', 'ready-to-show');
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
logger.log("main", "ready-to-show");
|
||||
mainWindow.show();
|
||||
if (windowState.isFullScreen) mainWindow.setFullScreen(true);
|
||||
});
|
||||
|
||||
mainWindow.on('close', saveWindowState);
|
||||
mainWindow.on("close", saveWindowState);
|
||||
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
mainWindow.webContents.send('fullscreen-change', true);
|
||||
mainWindow.on("enter-full-screen", () => {
|
||||
mainWindow.webContents.send("fullscreen-change", true);
|
||||
});
|
||||
mainWindow.on('leave-full-screen', () => {
|
||||
mainWindow.webContents.send('fullscreen-change', false);
|
||||
mainWindow.on("leave-full-screen", () => {
|
||||
mainWindow.webContents.send("fullscreen-change", false);
|
||||
});
|
||||
|
||||
mainWindow.on("maximize", () => mainWindow.webContents.send("maximize-change", true));
|
||||
mainWindow.on("unmaximize", () => mainWindow.webContents.send("maximize-change", false));
|
||||
}
|
||||
|
||||
function getSavedWindowState() {
|
||||
@@ -62,36 +65,43 @@ function isUsableBounds(bounds) {
|
||||
const values = [bounds.x, bounds.y, bounds.width, bounds.height];
|
||||
if (!values.every(Number.isFinite)) return false;
|
||||
if (bounds.width < 900 || bounds.height < 600) return false;
|
||||
return screen.getAllDisplays().some(display => rectanglesOverlap(bounds, display.workArea));
|
||||
return screen
|
||||
.getAllDisplays()
|
||||
.some((display) => rectanglesOverlap(bounds, display.workArea));
|
||||
}
|
||||
|
||||
function rectanglesOverlap(a, b) {
|
||||
return a.x < b.x + b.width &&
|
||||
return (
|
||||
a.x < b.x + b.width &&
|
||||
a.x + a.width > b.x &&
|
||||
a.y < b.y + b.height &&
|
||||
a.y + a.height > b.y;
|
||||
a.y + a.height > b.y
|
||||
);
|
||||
}
|
||||
|
||||
function saveWindowState() {
|
||||
if (!mainWindow) return;
|
||||
const isFullScreen = mainWindow.isFullScreen();
|
||||
const bounds = isFullScreen ? mainWindow.getNormalBounds() : mainWindow.getBounds();
|
||||
updateConfig(cfg => {
|
||||
const bounds = isFullScreen
|
||||
? mainWindow.getNormalBounds()
|
||||
: mainWindow.getBounds();
|
||||
updateConfig((cfg) => {
|
||||
cfg.windowState = { isFullScreen, bounds };
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
logger.log('main', 'app ready');
|
||||
logger.log("main", "app ready");
|
||||
Menu.setApplicationMenu(null);
|
||||
createWindow();
|
||||
registerAll(mainWindow);
|
||||
logger.log('main', 'ipc registered');
|
||||
logger.log("main", "ipc registered");
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,14 @@ const { readConfig, updateConfig } = require('../config');
|
||||
const logger = require('../logger');
|
||||
|
||||
function register(mainWindow) {
|
||||
ipcMain.handle('window-minimize', () => mainWindow.minimize());
|
||||
ipcMain.handle('window-maximize', () => {
|
||||
if (mainWindow.isMaximized()) mainWindow.unmaximize();
|
||||
else mainWindow.maximize();
|
||||
});
|
||||
ipcMain.handle('window-close', () => mainWindow.close());
|
||||
ipcMain.handle('window-is-maximized', () => mainWindow.isMaximized());
|
||||
|
||||
ipcMain.handle('select-folder', async () => {
|
||||
logger.log('ipc:folder', 'select-folder requested');
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
@@ -22,6 +30,19 @@ function register(mainWindow) {
|
||||
|
||||
ipcMain.handle('get-home-dir', () => os.homedir());
|
||||
|
||||
ipcMain.handle('get-drives', () => {
|
||||
if (process.platform !== 'win32') return [];
|
||||
const drives = [];
|
||||
for (let i = 67; i <= 90; i++) { // C-Z
|
||||
const drive = `${String.fromCharCode(i)}:\\`;
|
||||
try {
|
||||
fs.accessSync(drive);
|
||||
drives.push(drive);
|
||||
} catch {}
|
||||
}
|
||||
return drives;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-last-folder', () => {
|
||||
const cfg = readConfig();
|
||||
logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null });
|
||||
|
||||
@@ -8,7 +8,17 @@ contextBridge.exposeInMainWorld('api', {
|
||||
isProduction: env.isProduction,
|
||||
logEnabled: env.logEnabled,
|
||||
nodeEnv: env.nodeEnv,
|
||||
platform: process.platform,
|
||||
}),
|
||||
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
|
||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||
onMaximizeChange: (() => {
|
||||
let _cb = null;
|
||||
ipcRenderer.on('maximize-change', (_e, v) => { if (_cb) _cb(v); });
|
||||
return (callback) => { _cb = callback; };
|
||||
})(),
|
||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||
listImages: (folder) => ipcRenderer.invoke('list-images', folder),
|
||||
readImage: (path) => ipcRenderer.invoke('read-image', path),
|
||||
@@ -19,6 +29,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
return (callback) => { _cb = callback; };
|
||||
})(),
|
||||
getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
|
||||
getDrives: () => ipcRenderer.invoke('get-drives'),
|
||||
getLastFolder: () => ipcRenderer.invoke('get-last-folder'),
|
||||
setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p),
|
||||
listDirectories: (p) => ipcRenderer.invoke('list-directories', p),
|
||||
|
||||
@@ -14,6 +14,7 @@ export function createFolderController({
|
||||
updatePreview,
|
||||
}) {
|
||||
let homeDir = null;
|
||||
let _drives = [];
|
||||
|
||||
async function openFolder() {
|
||||
log('folder', 'openFolder start', { homeDir });
|
||||
@@ -28,11 +29,7 @@ export function createFolderController({
|
||||
|
||||
log('folder', 'openFolder selected', { folder });
|
||||
if (!folder) return;
|
||||
if (homeDir && folder.startsWith(homeDir)) {
|
||||
await fileExplorer.loadTree(homeDir, folder);
|
||||
} else {
|
||||
await fileExplorer.loadTree(folder, folder);
|
||||
}
|
||||
await fileExplorer.loadTree(homeDir, folder, _drives);
|
||||
await loadFolder(folder);
|
||||
}
|
||||
|
||||
@@ -112,6 +109,7 @@ export function createFolderController({
|
||||
loadFolder,
|
||||
openFolder,
|
||||
setHomeDir: value => { homeDir = value; },
|
||||
setDrives: value => { _drives = value; },
|
||||
sortFiles,
|
||||
updateSortButtons,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ let _selectedPath = null;
|
||||
const ICON_CLOSED = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#7c6af7" fill-opacity=".75"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#9180ff" fill-opacity=".9"/></svg>`;
|
||||
const ICON_OPEN = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#c9b8ff" fill-opacity=".7"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#e0d5ff" fill-opacity=".85"/></svg>`;
|
||||
const ICON_HOME = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 1.5L1 6.5V12.5H5.5V9H8.5V12.5H13V6.5L7 1.5Z" fill="#6ac8f7" fill-opacity=".9"/></svg>`;
|
||||
const ICON_DRIVE = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="3.5" width="12" height="7" rx="1.5" fill="#7c6af7" fill-opacity=".75"/><rect x="1" y="7" width="12" height="3.5" rx="1.5" fill="#9180ff" fill-opacity=".9"/><circle cx="10.5" cy="9" r="1" fill="white" fill-opacity=".7"/></svg>`;
|
||||
|
||||
export function init(container, onSelect) {
|
||||
_onSelect = onSelect;
|
||||
@@ -15,14 +16,30 @@ export function init(container, onSelect) {
|
||||
container.classList.add('tree-root');
|
||||
}
|
||||
|
||||
export async function loadTree(rootPath, selectedPath) {
|
||||
export async function loadTree(rootPath, selectedPath, drives = []) {
|
||||
_selectedPath = selectedPath;
|
||||
const container = document.querySelector('.file-explorer');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (drives.length > 0) {
|
||||
for (const drive of drives) {
|
||||
const driveNode = createNode({
|
||||
name: drive.replace(/\\$/, ''),
|
||||
path: drive,
|
||||
hasChildren: true,
|
||||
isRoot: true,
|
||||
isDrive: true,
|
||||
});
|
||||
container.appendChild(driveNode);
|
||||
const selectedUpper = (selectedPath || '').toUpperCase();
|
||||
if (selectedUpper.startsWith(drive.toUpperCase())) {
|
||||
await expandNode(driveNode, selectedPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const rootNode = createNode({
|
||||
name: rootPath.split('/').pop() || rootPath,
|
||||
name: rootPath.split(/[\\/]/).filter(Boolean).pop() || rootPath,
|
||||
path: rootPath,
|
||||
hasChildren: true,
|
||||
isRoot: true,
|
||||
@@ -30,14 +47,16 @@ export async function loadTree(rootPath, selectedPath) {
|
||||
container.appendChild(rootNode);
|
||||
await expandNode(rootNode, selectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
||||
function createNode({ name, path: nodePath, hasChildren, isRoot = false, isDrive = false }) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'tree-node';
|
||||
wrap.dataset.path = nodePath;
|
||||
wrap.dataset.expanded = 'false';
|
||||
wrap.dataset.hasChildren = hasChildren ? 'true' : 'false';
|
||||
wrap.dataset.isRoot = isRoot ? 'true' : 'false';
|
||||
wrap.dataset.isDrive = isDrive ? 'true' : 'false';
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'tree-row';
|
||||
@@ -50,7 +69,7 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-icon';
|
||||
icon.innerHTML = isRoot ? ICON_HOME : ICON_CLOSED;
|
||||
icon.innerHTML = isDrive ? ICON_DRIVE : (isRoot ? ICON_HOME : ICON_CLOSED);
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'tree-label';
|
||||
@@ -91,7 +110,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
||||
}
|
||||
|
||||
function nodeIcon(nodeEl) {
|
||||
return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN;
|
||||
if (nodeEl.dataset.isDrive === 'true') return ICON_DRIVE;
|
||||
if (nodeEl.dataset.isRoot === 'true') return ICON_HOME;
|
||||
return ICON_OPEN;
|
||||
}
|
||||
|
||||
function selectNode(nodeEl, { notify = true } = {}) {
|
||||
@@ -140,7 +161,7 @@ async function expandNode(nodeEl, autoExpandPath) {
|
||||
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
|
||||
for (const child of childNodes) {
|
||||
const cp = child.dataset.path;
|
||||
if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) {
|
||||
if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/') || autoExpandPath.startsWith(cp + '\\')) {
|
||||
await expandNode(child, autoExpandPath);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
|
||||
<header>
|
||||
<h1>Photo Renamer</h1>
|
||||
<div class="window-controls" id="window-controls">
|
||||
<button class="wc-btn" id="btn-wc-minimize" title="최소화">
|
||||
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="wc-btn" id="btn-wc-maximize" title="최대화">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" fill="none"/></svg>
|
||||
</button>
|
||||
<button class="wc-btn wc-close" id="btn-wc-close" title="닫기">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.2"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="app-workspace" aria-label="사진 이름 변경 작업 영역">
|
||||
|
||||
@@ -78,19 +78,43 @@ window.api.onFullscreenChange((isFullscreen) => {
|
||||
document.body.classList.toggle('fullscreen', isFullscreen);
|
||||
});
|
||||
|
||||
// ── 플랫폼 / 창 컨트롤 ──────────────────────────────
|
||||
if (window.api.env.platform === 'win32') {
|
||||
document.body.classList.add('platform-win32');
|
||||
|
||||
const ICON_MAXIMIZE = `<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" fill="none"/></svg>`;
|
||||
const ICON_RESTORE = `<svg width="10" height="10" viewBox="0 0 10 10"><rect x="2" y="0.5" width="7.5" height="7.5" stroke="currentColor" fill="none"/><polyline points="0.5,2 0.5,9.5 8,9.5" stroke="currentColor" fill="none"/></svg>`;
|
||||
|
||||
const btnMaximize = document.getElementById('btn-wc-maximize');
|
||||
|
||||
function updateMaximizeIcon(isMaximized) {
|
||||
btnMaximize.innerHTML = isMaximized ? ICON_RESTORE : ICON_MAXIMIZE;
|
||||
btnMaximize.title = isMaximized ? '이전 크기로' : '최대화';
|
||||
}
|
||||
|
||||
window.api.windowIsMaximized().then(updateMaximizeIcon);
|
||||
window.api.onMaximizeChange(updateMaximizeIcon);
|
||||
|
||||
document.getElementById('btn-wc-minimize').addEventListener('click', () => window.api.windowMinimize());
|
||||
btnMaximize.addEventListener('click', () => window.api.windowMaximize());
|
||||
document.getElementById('btn-wc-close').addEventListener('click', () => window.api.windowClose());
|
||||
}
|
||||
|
||||
// ── 앱 시작 ───────────────────────────────────────
|
||||
(async () => {
|
||||
log('startup', 'init start');
|
||||
const [lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize] = await Promise.all([
|
||||
const [lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize, drives] = await Promise.all([
|
||||
window.api.getLastFolder(),
|
||||
window.api.getHomeDir(),
|
||||
window.api.getPreviewOn(),
|
||||
window.api.getSidebarWidth(),
|
||||
window.api.getFileExplorerVisible(),
|
||||
window.api.getThumbSize(),
|
||||
window.api.getDrives(),
|
||||
]);
|
||||
log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize });
|
||||
log('startup', 'settings loaded', { lastFolder, homeDir, previewOn, sidebarWidth, fileExplorerVisible, thumbSize, drives });
|
||||
folderController.setHomeDir(homeDir);
|
||||
folderController.setDrives(drives);
|
||||
|
||||
btnPreviewToggle.classList.toggle('active', previewOn);
|
||||
hoverPreview.setEnabled(previewOn);
|
||||
@@ -110,7 +134,7 @@ window.api.onFullscreenChange((isFullscreen) => {
|
||||
|
||||
const target = lastFolder || homeDir;
|
||||
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
|
||||
await fileExplorer.loadTree(homeDir, target);
|
||||
await fileExplorer.loadTree(homeDir, target, drives);
|
||||
await folderController.loadFolder(target);
|
||||
log('startup', 'init done');
|
||||
})();
|
||||
|
||||
@@ -67,6 +67,40 @@ header h1 {
|
||||
font-size: 13px; font-weight: 600; color: var(--muted);
|
||||
letter-spacing: .08em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 커스텀 창 컨트롤 버튼 (Windows) */
|
||||
.window-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 44px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
body.platform-win32 .window-controls {
|
||||
display: flex;
|
||||
}
|
||||
.wc-btn {
|
||||
width: 46px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
.wc-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: var(--fg);
|
||||
}
|
||||
.wc-close:hover {
|
||||
background: #c42b1c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dir-path {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; color: var(--muted); opacity: .7;
|
||||
|
||||
Reference in New Issue
Block a user