Windows 메뉴바 제거, Windows 빌드 환경 적용
- Windows 빌드(npm run build) 실행을 위해서는 VScode를 관리자 권한으로 실행해야 함
This commit is contained in:
+3
-5
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "photo-renamer",
|
"name": "photo-renamer",
|
||||||
"version": "2.0.0",
|
"version": "1.0.0",
|
||||||
"description": "사진 파일 이름 정리 앱",
|
"description": "사진 파일 이름 정리 앱",
|
||||||
"main": "src/main/index.js",
|
"main": "src/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development electron .",
|
"start": "cross-env NODE_ENV=development electron .",
|
||||||
"debug": "cross-env NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 .",
|
"debug": "cross-env NODE_ENV=development electron --inspect=9229 --remote-debugging-port=9222 .",
|
||||||
"build": "cross-env NODE_ENV=production electron-builder --win --dir",
|
"build": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win --dir",
|
||||||
"dist": "cross-env NODE_ENV=production electron-builder --win",
|
"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",
|
"build:mac": "cross-env NODE_ENV=production electron-builder --mac --dir",
|
||||||
"dist:mac": "cross-env NODE_ENV=production electron-builder --mac"
|
"dist:mac": "cross-env NODE_ENV=production electron-builder --mac"
|
||||||
},
|
},
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"installerIcon": "assets/icon.png",
|
|
||||||
"uninstallerIcon": "assets/icon.png",
|
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true
|
"createStartMenuShortcut": true
|
||||||
},
|
},
|
||||||
|
|||||||
+41
-31
@@ -1,16 +1,16 @@
|
|||||||
const { app, BrowserWindow, screen } = require('electron');
|
const { app, BrowserWindow, screen, Menu } = require("electron");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const { registerAll } = require('./ipc');
|
const { registerAll } = require("./ipc");
|
||||||
const { readConfig, updateConfig } = require('./config');
|
const { readConfig, updateConfig } = require("./config");
|
||||||
const logger = require('./logger');
|
const logger = require("./logger");
|
||||||
|
|
||||||
app.setName('Photo Renamer');
|
app.setName("Photo Renamer");
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const windowState = getSavedWindowState();
|
const windowState = getSavedWindowState();
|
||||||
logger.log('main', 'createWindow', windowState);
|
logger.log("main", "createWindow", windowState);
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
x: windowState.bounds?.x,
|
x: windowState.bounds?.x,
|
||||||
y: windowState.bounds?.y,
|
y: windowState.bounds?.y,
|
||||||
@@ -18,33 +18,36 @@ function createWindow() {
|
|||||||
height: windowState.bounds?.height || 820,
|
height: windowState.bounds?.height || 820,
|
||||||
minWidth: 900,
|
minWidth: 900,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
titleBarStyle: 'hiddenInset',
|
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
|
||||||
backgroundColor: '#0f0f17',
|
backgroundColor: "#0f0f17",
|
||||||
icon: path.join(__dirname, '../../assets/icon.icns'),
|
icon: path.join(__dirname, "../../assets/icon.icns"),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, '../preload/index.js'),
|
preload: path.join(__dirname, "../preload/index.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once("ready-to-show", () => {
|
||||||
logger.log('main', 'ready-to-show');
|
logger.log("main", "ready-to-show");
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
if (windowState.isFullScreen) mainWindow.setFullScreen(true);
|
if (windowState.isFullScreen) mainWindow.setFullScreen(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on('close', saveWindowState);
|
mainWindow.on("close", saveWindowState);
|
||||||
|
|
||||||
mainWindow.on('enter-full-screen', () => {
|
mainWindow.on("enter-full-screen", () => {
|
||||||
mainWindow.webContents.send('fullscreen-change', true);
|
mainWindow.webContents.send("fullscreen-change", true);
|
||||||
});
|
});
|
||||||
mainWindow.on('leave-full-screen', () => {
|
mainWindow.on("leave-full-screen", () => {
|
||||||
mainWindow.webContents.send('fullscreen-change', false);
|
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() {
|
function getSavedWindowState() {
|
||||||
@@ -62,36 +65,43 @@ function isUsableBounds(bounds) {
|
|||||||
const values = [bounds.x, bounds.y, bounds.width, bounds.height];
|
const values = [bounds.x, bounds.y, bounds.width, bounds.height];
|
||||||
if (!values.every(Number.isFinite)) return false;
|
if (!values.every(Number.isFinite)) return false;
|
||||||
if (bounds.width < 900 || bounds.height < 600) 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) {
|
function rectanglesOverlap(a, b) {
|
||||||
return a.x < b.x + b.width &&
|
return (
|
||||||
a.x + a.width > b.x &&
|
a.x < b.x + b.width &&
|
||||||
a.y < b.y + b.height &&
|
a.x + a.width > b.x &&
|
||||||
a.y + a.height > b.y;
|
a.y < b.y + b.height &&
|
||||||
|
a.y + a.height > b.y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWindowState() {
|
function saveWindowState() {
|
||||||
if (!mainWindow) return;
|
if (!mainWindow) return;
|
||||||
const isFullScreen = mainWindow.isFullScreen();
|
const isFullScreen = mainWindow.isFullScreen();
|
||||||
const bounds = isFullScreen ? mainWindow.getNormalBounds() : mainWindow.getBounds();
|
const bounds = isFullScreen
|
||||||
updateConfig(cfg => {
|
? mainWindow.getNormalBounds()
|
||||||
|
: mainWindow.getBounds();
|
||||||
|
updateConfig((cfg) => {
|
||||||
cfg.windowState = { isFullScreen, bounds };
|
cfg.windowState = { isFullScreen, bounds };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
logger.log('main', 'app ready');
|
logger.log("main", "app ready");
|
||||||
|
Menu.setApplicationMenu(null);
|
||||||
createWindow();
|
createWindow();
|
||||||
registerAll(mainWindow);
|
registerAll(mainWindow);
|
||||||
logger.log('main', 'ipc registered');
|
logger.log("main", "ipc registered");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on("window-all-closed", () => {
|
||||||
if (process.platform !== 'darwin') app.quit();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ const { readConfig, updateConfig } = require('../config');
|
|||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
function register(mainWindow) {
|
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 () => {
|
ipcMain.handle('select-folder', async () => {
|
||||||
logger.log('ipc:folder', 'select-folder requested');
|
logger.log('ipc:folder', 'select-folder requested');
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
@@ -22,6 +30,19 @@ function register(mainWindow) {
|
|||||||
|
|
||||||
ipcMain.handle('get-home-dir', () => os.homedir());
|
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', () => {
|
ipcMain.handle('get-last-folder', () => {
|
||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null });
|
logger.log('ipc:folder', 'get-last-folder config', { lastFolder: cfg.lastFolder || null });
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
isProduction: env.isProduction,
|
isProduction: env.isProduction,
|
||||||
logEnabled: env.logEnabled,
|
logEnabled: env.logEnabled,
|
||||||
nodeEnv: env.nodeEnv,
|
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'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
listImages: (folder) => ipcRenderer.invoke('list-images', folder),
|
listImages: (folder) => ipcRenderer.invoke('list-images', folder),
|
||||||
readImage: (path) => ipcRenderer.invoke('read-image', path),
|
readImage: (path) => ipcRenderer.invoke('read-image', path),
|
||||||
@@ -19,6 +29,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
return (callback) => { _cb = callback; };
|
return (callback) => { _cb = callback; };
|
||||||
})(),
|
})(),
|
||||||
getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
|
getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
|
||||||
|
getDrives: () => ipcRenderer.invoke('get-drives'),
|
||||||
getLastFolder: () => ipcRenderer.invoke('get-last-folder'),
|
getLastFolder: () => ipcRenderer.invoke('get-last-folder'),
|
||||||
setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p),
|
setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p),
|
||||||
listDirectories: (p) => ipcRenderer.invoke('list-directories', p),
|
listDirectories: (p) => ipcRenderer.invoke('list-directories', p),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function createFolderController({
|
|||||||
updatePreview,
|
updatePreview,
|
||||||
}) {
|
}) {
|
||||||
let homeDir = null;
|
let homeDir = null;
|
||||||
|
let _drives = [];
|
||||||
|
|
||||||
async function openFolder() {
|
async function openFolder() {
|
||||||
log('folder', 'openFolder start', { homeDir });
|
log('folder', 'openFolder start', { homeDir });
|
||||||
@@ -28,11 +29,7 @@ export function createFolderController({
|
|||||||
|
|
||||||
log('folder', 'openFolder selected', { folder });
|
log('folder', 'openFolder selected', { folder });
|
||||||
if (!folder) return;
|
if (!folder) return;
|
||||||
if (homeDir && folder.startsWith(homeDir)) {
|
await fileExplorer.loadTree(homeDir, folder, _drives);
|
||||||
await fileExplorer.loadTree(homeDir, folder);
|
|
||||||
} else {
|
|
||||||
await fileExplorer.loadTree(folder, folder);
|
|
||||||
}
|
|
||||||
await loadFolder(folder);
|
await loadFolder(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +109,7 @@ export function createFolderController({
|
|||||||
loadFolder,
|
loadFolder,
|
||||||
openFolder,
|
openFolder,
|
||||||
setHomeDir: value => { homeDir = value; },
|
setHomeDir: value => { homeDir = value; },
|
||||||
|
setDrives: value => { _drives = value; },
|
||||||
sortFiles,
|
sortFiles,
|
||||||
updateSortButtons,
|
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_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_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_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) {
|
export function init(container, onSelect) {
|
||||||
_onSelect = onSelect;
|
_onSelect = onSelect;
|
||||||
@@ -15,29 +16,47 @@ export function init(container, onSelect) {
|
|||||||
container.classList.add('tree-root');
|
container.classList.add('tree-root');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTree(rootPath, selectedPath) {
|
export async function loadTree(rootPath, selectedPath, drives = []) {
|
||||||
_selectedPath = selectedPath;
|
_selectedPath = selectedPath;
|
||||||
const container = document.querySelector('.file-explorer');
|
const container = document.querySelector('.file-explorer');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const rootNode = createNode({
|
if (drives.length > 0) {
|
||||||
name: rootPath.split('/').pop() || rootPath,
|
for (const drive of drives) {
|
||||||
path: rootPath,
|
const driveNode = createNode({
|
||||||
hasChildren: true,
|
name: drive.replace(/\\$/, ''),
|
||||||
isRoot: true,
|
path: drive,
|
||||||
});
|
hasChildren: true,
|
||||||
container.appendChild(rootNode);
|
isRoot: true,
|
||||||
await expandNode(rootNode, selectedPath);
|
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(/[\\/]/).filter(Boolean).pop() || rootPath,
|
||||||
|
path: rootPath,
|
||||||
|
hasChildren: true,
|
||||||
|
isRoot: true,
|
||||||
|
});
|
||||||
|
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');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'tree-node';
|
wrap.className = 'tree-node';
|
||||||
wrap.dataset.path = nodePath;
|
wrap.dataset.path = nodePath;
|
||||||
wrap.dataset.expanded = 'false';
|
wrap.dataset.expanded = 'false';
|
||||||
wrap.dataset.hasChildren = hasChildren ? 'true' : 'false';
|
wrap.dataset.hasChildren = hasChildren ? 'true' : 'false';
|
||||||
wrap.dataset.isRoot = isRoot ? 'true' : 'false';
|
wrap.dataset.isRoot = isRoot ? 'true' : 'false';
|
||||||
|
wrap.dataset.isDrive = isDrive ? 'true' : 'false';
|
||||||
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'tree-row';
|
row.className = 'tree-row';
|
||||||
@@ -50,7 +69,7 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
|||||||
|
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'tree-icon';
|
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');
|
const label = document.createElement('span');
|
||||||
label.className = 'tree-label';
|
label.className = 'tree-label';
|
||||||
@@ -91,7 +110,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nodeIcon(nodeEl) {
|
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 } = {}) {
|
function selectNode(nodeEl, { notify = true } = {}) {
|
||||||
@@ -140,7 +161,7 @@ async function expandNode(nodeEl, autoExpandPath) {
|
|||||||
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
|
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
|
||||||
for (const child of childNodes) {
|
for (const child of childNodes) {
|
||||||
const cp = child.dataset.path;
|
const cp = child.dataset.path;
|
||||||
if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) {
|
if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/') || autoExpandPath.startsWith(cp + '\\')) {
|
||||||
await expandNode(child, autoExpandPath);
|
await expandNode(child, autoExpandPath);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>Photo Renamer</h1>
|
<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>
|
</header>
|
||||||
|
|
||||||
<section class="app-workspace" aria-label="사진 이름 변경 작업 영역">
|
<section class="app-workspace" aria-label="사진 이름 변경 작업 영역">
|
||||||
|
|||||||
@@ -78,19 +78,43 @@ window.api.onFullscreenChange((isFullscreen) => {
|
|||||||
document.body.classList.toggle('fullscreen', 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 () => {
|
(async () => {
|
||||||
log('startup', 'init start');
|
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.getLastFolder(),
|
||||||
window.api.getHomeDir(),
|
window.api.getHomeDir(),
|
||||||
window.api.getPreviewOn(),
|
window.api.getPreviewOn(),
|
||||||
window.api.getSidebarWidth(),
|
window.api.getSidebarWidth(),
|
||||||
window.api.getFileExplorerVisible(),
|
window.api.getFileExplorerVisible(),
|
||||||
window.api.getThumbSize(),
|
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.setHomeDir(homeDir);
|
||||||
|
folderController.setDrives(drives);
|
||||||
|
|
||||||
btnPreviewToggle.classList.toggle('active', previewOn);
|
btnPreviewToggle.classList.toggle('active', previewOn);
|
||||||
hoverPreview.setEnabled(previewOn);
|
hoverPreview.setEnabled(previewOn);
|
||||||
@@ -110,7 +134,7 @@ window.api.onFullscreenChange((isFullscreen) => {
|
|||||||
|
|
||||||
const target = lastFolder || homeDir;
|
const target = lastFolder || homeDir;
|
||||||
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
|
log('startup', 'initial folder target', { target, fromLastFolder: Boolean(lastFolder) });
|
||||||
await fileExplorer.loadTree(homeDir, target);
|
await fileExplorer.loadTree(homeDir, target, drives);
|
||||||
await folderController.loadFolder(target);
|
await folderController.loadFolder(target);
|
||||||
log('startup', 'init done');
|
log('startup', 'init done');
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -67,6 +67,40 @@ header h1 {
|
|||||||
font-size: 13px; font-weight: 600; color: var(--muted);
|
font-size: 13px; font-weight: 600; color: var(--muted);
|
||||||
letter-spacing: .08em; text-transform: uppercase;
|
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 {
|
.dir-path {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 11px; color: var(--muted); opacity: .7;
|
font-size: 11px; color: var(--muted); opacity: .7;
|
||||||
|
|||||||
Reference in New Issue
Block a user