From e612fbec8ea885ed940f24302e66a9db2db60fa6 Mon Sep 17 00:00:00 2001 From: Ryochobi Date: Sat, 22 Aug 2026 05:52:20 +0800 Subject: [PATCH] Added some stuff --- .gitignore | 3 +- iidxwidget-app/controller/controllerReader.js | 261 +++++------------- iidxwidget-app/localization/translations.js | 46 +++ iidxwidget-app/main.js | 189 +++++++------ iidxwidget-app/package.json | 9 +- iidxwidget-app/preload.js | 8 +- iidxwidget-app/renderer/chatter/chatter.html | 8 +- iidxwidget-app/renderer/chatter/chatter.js | 5 +- iidxwidget-app/renderer/logs/logs.html | 4 +- .../renderer/settings/settings.html | 83 ++++-- iidxwidget-app/renderer/settings/settings.js | 72 ++++- iidxwidget-app/renderer/settings/style.css | 5 +- iidxwidget-app/renderer/shared/i18n.js | 33 +++ iidxwidget-app/renderer/widget/index.html | 2 +- iidxwidget-app/renderer/widget/index.js | 43 ++- iidxwidget-app/renderer/widget/style.css | 5 +- iidxwidget-app/settings.json | 18 +- iidxwidget-app/test/controllerReader.test.js | 40 +++ iidxwidget-app/test/localization.test.js | 17 ++ 19 files changed, 508 insertions(+), 343 deletions(-) create mode 100644 iidxwidget-app/localization/translations.js create mode 100644 iidxwidget-app/renderer/shared/i18n.js create mode 100644 iidxwidget-app/test/controllerReader.test.js create mode 100644 iidxwidget-app/test/localization.test.js diff --git a/.gitignore b/.gitignore index 3a95d6f..0003cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /iidxwidget-app/node_modules -/iidxwidget-app/dist \ No newline at end of file +/iidxwidget-app/dist +/iidxwidget-app/dist-* diff --git a/iidxwidget-app/controller/controllerReader.js b/iidxwidget-app/controller/controllerReader.js index 7964cfe..1a99fd5 100644 --- a/iidxwidget-app/controller/controllerReader.js +++ b/iidxwidget-app/controller/controllerReader.js @@ -1,202 +1,73 @@ -const HID = require('node-hid'); -let lastButtonByte = 0; -let isLR2Active = false; -let lr2DetectEnabled = false; -let lr2PatternCount = 0; -let normalPatternCount = 0; -let currentDiscRaw = 0; -let lastLR2Direction = 'neutral'; -const LR2_ACTIVATE_THRESHOLD = 120; -const LR2_DEACTIVATE_THRESHOLD = 3; - -function startControllerReader(mode, onDataCallback, options = {}) { - // ๐Ÿ” ์ƒํƒœ ์ดˆ๊ธฐํ™” - isLR2Active = false; - lr2PatternCount = 0; - normalPatternCount = 0; - currentDiscRaw = 0; - lastLR2Direction = 'neutral'; - lr2DetectEnabled = !!options.lr2ModeEnabled; - const devices = HID.devices(); - - const deviceInfo = devices.find(d => { - if (mode === 'PHOENIXWAN') { - return d.vendorId === 0x1CCF && d.productId === 0x8048 && d.interface === 1; - } else if (mode === 'FPS EMP Gen2') { - return d.vendorId === 0x1CCF && d.productId === 0x8048 && d.interface === 0 && d.usagePage === 1; - } - return false; - }); - - if (!deviceInfo) { - console.error(`โŒ ${mode} ์žฅ์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); - return null; - } - - console.log(`๐ŸŽฎ ${mode} ์—ฐ๊ฒฐ ์‹œ๋„:`, deviceInfo); - - let device; - try { - device = new HID.HID(deviceInfo.path); - console.log(`๐ŸŸข ${mode} ์—ฐ๊ฒฐ ์„ฑ๊ณต`); - } catch (e) { - console.error(`โŒ ${mode} ์žฅ์น˜ ์—ด๊ธฐ ์‹คํŒจ:`, e); - return null; - } - - - device.on('data', buffer => { - try { - if (!device || typeof device.read !== 'function') return; - - const xRaw = buffer[0]; - if (lr2DetectEnabled) detectLR2Mode(buffer, onDataCallback); - const parsed = parseControllerData(buffer); - - if (isLR2Active) { - const filtered = parsed.filter(event => !(event.type === 'axis' && event.axis === 'X')); - - let direction = 'neutral'; - if (xRaw === 0x80) direction = '+'; - else if (xRaw === 0x7F) direction = '-'; - - if (direction !== 'neutral' && direction !== lastLR2Direction) { - lastLR2Direction = direction; - - if (direction === '+') { - currentDiscRaw = (currentDiscRaw + 5) % 256; - } else if (direction === '-') { - currentDiscRaw = (currentDiscRaw - 5 + 256) % 256; - } - - filtered.push({ - type: 'axis', - axis: 'X', - direction, - discRaw: currentDiscRaw, - timestamp: Date.now() - }); - - onDataCallback(filtered); - } else if (direction === 'neutral' && lastLR2Direction !== 'neutral') { - lastLR2Direction = 'neutral'; - filtered.push({ - type: 'axis', - axis: 'X', - direction: 'neutral', - discRaw: currentDiscRaw, - timestamp: Date.now() - }); - onDataCallback(filtered); - } else { - // ๋ฒ„ํŠผ๋งŒ ๋ˆŒ๋ฆฐ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ - if (filtered.length > 0) { - onDataCallback(filtered); - } - } - return; - } - - // โœ… ์ผ๋ฐ˜ ๋ชจ๋“œ์ผ ๊ฒฝ์šฐ parsed ์›๋ณธ์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜ - if (parsed.length > 0) onDataCallback(parsed); - - } catch (err) { - if (err?.message?.includes('Object has been destroyed')) { - console.warn('โš ๏ธ HID device destroyed, ignoring further data events.'); - } else { - console.error('โŒ Error in device.on("data") handler:', err); - } - } -}); - - - device.on('error', err => { - console.error('โŒ ๋””๋ฐ”์ด์Šค ์—๋Ÿฌ:', err); - }); - - return { - close: () => { - try { - device.removeAllListeners('data'); - device.removeAllListeners('error'); - device.close(); - console.log('๐Ÿ›‘ HID ์žฅ์น˜ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ซํž˜'); - } catch (e) { - console.error('โŒ HID ๋‹ซ๊ธฐ ์‹คํŒจ:', e); - } - } - }; - +let hidModule; +function getHID() { if (!hidModule) hidModule = require('node-hid'); return hidModule; } +const isPhoenix = d => d.vendorId === 0x1CCF && d.productId === 0x8048 && d.interface === 1; +const isFps = d => d.vendorId === 0x1CCF && d.productId === 0x8048 && d.interface === 0 && d.usagePage === 1; +function findExactDedicatedDevice(devices, profile) { + return devices.find(d => d.path && (profile === 'PHOENIXWAN' ? isPhoenix(d) : profile === 'FPS EMP Gen2' ? isFps(d) : false)); } - -function detectLR2Mode(buffer, logCallback) { - const xRaw = buffer[0]; - const isStatic = [0x80, 0x7F, 0x00].includes(xRaw); - - if (isStatic) { - if (lr2PatternCount === 0) lr2FirstStaticTime = Date.now(); - lr2PatternCount++; - normalPatternCount = 0; - - const duration = Date.now() - lr2FirstStaticTime; - - if (!isLR2Active && lr2PatternCount >= LR2_ACTIVATE_THRESHOLD && duration < 500) { - isLR2Active = true; - console.log('๐Ÿ”ต LR2 ๋ชจ๋“œ ํ™œ์„ฑํ™”๋จ'); - if (typeof logCallback === 'function') { - logCallback([{ type: 'log', message: '๐Ÿ”ต LR2 ๋ชจ๋“œ ํ™œ์„ฑํ™”๋จ', timestamp: Date.now() }]); - } - } - } else { - lr2PatternCount = 0; - normalPatternCount++; - lr2FirstStaticTime = null; - - if (isLR2Active && normalPatternCount >= LR2_DEACTIVATE_THRESHOLD) { - isLR2Active = false; - console.log('โšช LR2 ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”๋จ'); - if (typeof logCallback === 'function') { - logCallback([{ type: 'log', message: 'โšช LR2 ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”๋จ', timestamp: Date.now() }]); - } - } - } +function findAutoController(devices) { + const usable = devices.filter(d => d.path); + const phoenix = usable.find(isPhoenix); if (phoenix) return { device: phoenix, parser: 'PHOENIXWAN' }; + const fps = usable.find(isFps); if (fps) return { device: fps, parser: 'FPS_EMP' }; + const terms = /phoenixwan|fps|emp|infinitas|inf&bms|iidx|beatmania|yuancon|gamo2/i; + const named = usable.find(d => terms.test(`${d.product || ''} ${d.manufacturer || ''}`) && !(d.usagePage === 1 && (d.usage === 2 || d.usage === 6))); + if (named) return { device: named, parser: 'GENERIC' }; + const generic = usable.find(d => d.usagePage === 1 && (d.usage === 4 || d.usage === 5)); + return generic ? { device: generic, parser: 'GENERIC' } : null; } - -function parseControllerData(buffer) { - const events = []; - const buttonByte = buffer[2]; - const now = Date.now(); - - for (let i = 0; i < 7; i++) { - const mask = 1 << i; - const wasPressed = (lastButtonByte & mask) !== 0; - const isPressed = (buttonByte & mask) !== 0; - - if (wasPressed !== isPressed) { - events.push({ - type: 'button', - button: `button ${i + 1}`, - pressed: isPressed, - timestamp: now - }); +function parseGenericControllerData(buffer, mapping = {}, state = { previousButtons: 0 }) { + if (!buffer?.length) return []; + if (!Number.isInteger(state.currentDiscRaw)) state.currentDiscRaw = 128; + const offset = buffer[0] !== 0 ? 1 : 0; + let buttons = 0; + for (let i = 0; i < Math.min(4, buffer.length - offset); i++) buttons = (buttons | ((buffer[offset + i] || 0) << (i * 8))) >>> 0; + const changed = (buttons ^ (state.previousButtons >>> 0)) >>> 0, events = [], timestamp = Date.now(); + for (let i = 0; i < 32; i++) { + const mask = (1 << i) >>> 0; + if (!(changed & mask)) continue; + const physicalButton = i + 1, pressed = !!(buttons & mask); + events.push({ type: 'physical-button', physicalButton, pressed, timestamp }); + const logical = Object.keys(mapping).find(key => Number(mapping[key]) === physicalButton); + if (/^[1-7]$/.test(logical)) events.push({ type: 'button', button: `button ${logical}`, physicalButton, pressed, timestamp }); + else if (pressed && (logical === 'SCup' || logical === 'SCdown')) { + state.currentDiscRaw = (state.currentDiscRaw + (logical === 'SCup' ? 2 : -2) + 256) % 256; + events.push({ type: 'axis', axis: 'X', direction: logical === 'SCup' ? '+' : '-', discRaw: state.currentDiscRaw, physicalButton, timestamp }); } } - - lastButtonByte = buttonByte; - - const xRaw = buffer[0]; - const direction = xRaw < 100 ? '-' : xRaw > 150 ? '+' : 'neutral'; - - events.push({ - type: 'axis', - axis: 'X', - direction, - discRaw: xRaw, - timestamp: now - }); - + state.previousButtons = buttons; return events; } - -module.exports = { startControllerReader }; +function createDedicatedParserState() { return { lastButtonByte: 0, isLR2Active: false, lr2DetectEnabled: false, lr2PatternCount: 0, normalPatternCount: 0, lr2FirstStaticTime: null, currentDiscRaw: 0, lastLR2Direction: 'neutral' }; } +function parseControllerData(buffer, state) { + const events = [], buttonByte = buffer[2], timestamp = Date.now(); + for (let i = 0; i < 7; i++) { const mask = 1 << i, pressed = !!(buttonByte & mask); if (!!(state.lastButtonByte & mask) !== pressed) events.push({ type: 'button', button: `button ${i + 1}`, pressed, timestamp }); } + state.lastButtonByte = buttonByte; + const xRaw = buffer[0]; events.push({ type: 'axis', axis: 'X', direction: xRaw < 100 ? '-' : xRaw > 150 ? '+' : 'neutral', discRaw: xRaw, timestamp }); + return events; +} +function detectLR2Mode(buffer, callback, state) { + const isStatic = [0x80, 0x7F, 0x00].includes(buffer[0]); + if (isStatic) { if (!state.lr2PatternCount) state.lr2FirstStaticTime = Date.now(); state.lr2PatternCount++; state.normalPatternCount = 0; if (!state.isLR2Active && state.lr2PatternCount >= 120 && Date.now() - state.lr2FirstStaticTime < 500) { state.isLR2Active = true; callback?.([{ type: 'log', message: '๐Ÿ”ต LR2 ๋ชจ๋“œ ํ™œ์„ฑํ™”๋จ', timestamp: Date.now() }]); } } + else { state.lr2PatternCount = 0; state.normalPatternCount++; state.lr2FirstStaticTime = null; if (state.isLR2Active && state.normalPatternCount >= 3) { state.isLR2Active = false; callback?.([{ type: 'log', message: 'โšช LR2 ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”๋จ', timestamp: Date.now() }]); } } +} +function handleDedicatedData(buffer, callback, state) { + const xRaw = buffer[0]; if (state.lr2DetectEnabled) detectLR2Mode(buffer, callback, state); + const parsed = parseControllerData(buffer, state); if (!state.isLR2Active) { if (parsed.length) callback(parsed); return; } + const filtered = parsed.filter(e => !(e.type === 'axis' && e.axis === 'X')); let direction = xRaw === 0x80 ? '+' : xRaw === 0x7F ? '-' : 'neutral'; + if (direction !== 'neutral' && direction !== state.lastLR2Direction) { state.lastLR2Direction = direction; state.currentDiscRaw = (state.currentDiscRaw + (direction === '+' ? 5 : -5) + 256) % 256; filtered.push({ type: 'axis', axis: 'X', direction, discRaw: state.currentDiscRaw, timestamp: Date.now() }); } + else if (direction === 'neutral' && state.lastLR2Direction !== 'neutral') { state.lastLR2Direction = 'neutral'; filtered.push({ type: 'axis', axis: 'X', direction, discRaw: state.currentDiscRaw, timestamp: Date.now() }); } + if (filtered.length) callback(filtered); +} +function openReader(selection, callback, options) { + const logger = options.logger || ((level, code, details) => console[level]?.(code, details || '')); let device; + try { const HID = getHID(); device = new HID.HID(selection.device.path); logger('log', 'connected', { profile: options.profile }); } catch (error) { logger('error', 'openFailed', { profile: options.profile, error }); return null; } + const dedicatedState = createDedicatedParserState(); dedicatedState.lr2DetectEnabled = selection.parser !== 'GENERIC' && !!options.lr2ModeEnabled; + const genericState = { previousButtons: 0, currentDiscRaw: 128 }; + device.on('data', buffer => { try { if (selection.parser === 'GENERIC') { const events = parseGenericControllerData(buffer, options.genericMapping, genericState); if (events.length) callback(events); } else handleDedicatedData(buffer, callback, dedicatedState); } catch (error) { logger('error', 'dataError', { error }); } }); + device.on('error', error => logger('error', 'deviceError', { error })); + return { parser: selection.parser, close() { try { device.removeAllListeners(); device.close(); } catch (error) { logger('error', 'closeFailed', { error }); } } }; +} +function startControllerReader(profile, callback, options = {}) { const device = findExactDedicatedDevice(getHID().devices(), profile); if (!device) { options.logger?.('error', 'notFound', { profile }); return null; } return openReader({ device, parser: profile === 'PHOENIXWAN' ? 'PHOENIXWAN' : 'FPS_EMP' }, callback, { ...options, profile }); } +function startAutoControllerReader(callback, options = {}) { const selection = findAutoController(getHID().devices()); if (!selection) { options.logger?.('error', 'notFound', { profile: 'AUTO' }); return null; } return openReader(selection, callback, { ...options, profile: 'AUTO' }); } +module.exports = { startControllerReader, startAutoControllerReader, findExactDedicatedDevice, findAutoController, parseGenericControllerData, parseControllerData, createDedicatedParserState }; diff --git a/iidxwidget-app/localization/translations.js b/iidxwidget-app/localization/translations.js new file mode 100644 index 0000000..879c366 --- /dev/null +++ b/iidxwidget-app/localization/translations.js @@ -0,0 +1,46 @@ +const translations = { + ko: { + menu: { main: '๋ฉ”๋‰ด', language: 'Language', settings: '์„ค์ •', logs: '๋กœ๊ทธ', uploadCount: 'ํƒ€๊ฑด ๊ธฐ๋ก ์„œ๋ฒ„๋กœ ์ „์†ก', chatter: '์ฑ„ํ„ฐ๋ง ๊ฐ์ง€', about: '์ •๋ณด', contributors: '๊ธฐ์—ฌ์ž', checkUpdates: '์—…๋ฐ์ดํŠธ ํ™•์ธ', restart: '์žฌ์‹œ์ž‘', quit: '๋๋‚ด๊ธฐ' }, + common: { ok: 'ํ™•์ธ', yes: '์˜ˆ', no: '์•„๋‹ˆ์˜ค', later: '๋‹ค์Œ์— ํ•˜๊ธฐ', error: '์˜ค๋ฅ˜', notice: '์•Œ๋ฆผ' }, + about: { message: 'IIDXwidget v{version}\n๊ฐœ๋ฐœ์ž: Sadang\nhttps://github.com/Coldlapse/IIDXwidget', contributors: '๊ธฐ์—ฌ์ž : rhombus9, ๋ฉ˜ํƒˆ๋ฐ”์‚ฌ์‚ญ' }, + upload: { noToken: 'API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.', noData: '์ „์†กํ•  ํƒ€๊ฑด ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.', confirmTitle: 'ํƒ€๊ฑด ๊ธฐ๋ก ์ „์†ก ํ™•์ธ', confirm: 'ํ˜„์žฌ ํƒ€๊ฑด ์ˆ˜ {count}ํšŒ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.\nOBS์˜ ์ˆ˜์น˜๋Š” ๊ทธ๋Œ€๋กœ ๋‚จ๊ณ , ์•ฑ ํ™”๋ฉด์˜ ํƒ€๊ฑด ์ˆ˜์น˜๋Š” 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?', failed: '์ „์†ก ์‹คํŒจ', invalidToken: '์ž˜๋ชป๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.', success: '์ „์†ก ์„ฑ๊ณต', successMessage: '์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์ผ์ผ ์ด ํƒ€๊ฑด ์ˆ˜: {count})', error: '์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {message}' }, + update: { availableTitle: '์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ', available: '์ƒˆ ๋ฒ„์ „ {version} ์ด(๊ฐ€) ์žˆ์Šต๋‹ˆ๋‹ค!\n\n๋ณ€๊ฒฝ์‚ฌํ•ญ:\n{notes}', update: '์—…๋ฐ์ดํŠธ', skip: '์ด๋ฒˆ ๋ฒ„์ „ ์Šคํ‚ต', current: 'ํ˜„์žฌ ์ตœ์‹  ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.', errorTitle: '์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜', error: '์—…๋ฐ์ดํŠธ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:\n{message}', readyTitle: '์—…๋ฐ์ดํŠธ ์ค€๋น„ ์™„๋ฃŒ', ready: '์—…๋ฐ์ดํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n์ง€๊ธˆ ์žฌ์‹œ์ž‘ํ•˜๊ณ  ์„ค์น˜ํ• ๊นŒ์š”?', restartNow: '์ง€๊ธˆ ์žฌ์‹œ์ž‘', later: '๋‚˜์ค‘์—' }, + controller: { notFound: '{profile} ์žฅ์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', connecting: '{profile} ์—ฐ๊ฒฐ ์‹œ๋„', connected: '{profile} ์—ฐ๊ฒฐ ์„ฑ๊ณต', openFailed: '{profile} ์žฅ์น˜ ์—ด๊ธฐ ์‹คํŒจ', deviceError: '๋””๋ฐ”์ด์Šค ์˜ค๋ฅ˜', closed: 'HID ์žฅ์น˜ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ซํž˜', closeFailed: 'HID ๋‹ซ๊ธฐ ์‹คํŒจ', dataError: '์ปจํŠธ๋กค๋Ÿฌ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜' }, + settings: { title: '์„ค์ •', app: '์•ฑ ๊ธฐ๋ณธ ์„ค์ •', autoLaunch: 'Windows ์‹œ์ž‘ ์‹œ ์ž๋™ ์‹คํ–‰', promo: '์œ„์ ฏ ํ™๋ณด ๋ฐ•์Šค ํ‘œ์‹œ', controllerProfile: '์ปจํŠธ๋กค๋Ÿฌ ํ”„๋กœํ•„', auto: '์ž๋™ ๊ฐ์ง€', keyboard: 'ํ‚ค๋ณด๋“œ', apiToken: 'beatmania.app ํƒ€๊ฑด ๊ธฐ๋ก ํ† ํฐ', apiPlaceholder: '์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ ์ž…๋ ฅ', lr2: 'LR2 ๋ชจ๋“œ ๊ฐ์ง€ (์ฃผ์ž‘์ฝ˜ ์ „์šฉ)', keyboardMapping: 'ํ‚ค๋ณด๋“œ ๋งคํ•‘', genericMapping: 'AUTO ์ผ๋ฐ˜ ์ปจํŠธ๋กค๋Ÿฌ ๋งคํ•‘', genericHelp: 'Auto-detect๋ฅผ ์ €์žฅํ•œ ๋’ค ์„ค์ •์„ ๋‹ค์‹œ ์—ด๊ณ , ๊ฐ ํ•„๋“œ๋ฅผ ํด๋ฆญํ•œ ๋‹ค์Œ ์›ํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์„ธ์š”. ๋งˆ์ง€๋ง‰์— ์ €์žฅ์„ ๋ˆ„๋ฅด์„ธ์š”.', logicalKey: 'IIDX ํ‚ค {key}', turntableClockwise: 'ํ„ดํ…Œ์ด๋ธ” ์‹œ๊ณ„ ๋ฐฉํ–ฅ', turntableCounterclockwise: 'ํ„ดํ…Œ์ด๋ธ” ๋ฐ˜์‹œ๊ณ„ ๋ฐฉํ–ฅ', listening: '{key}: ๋ฒ„ํŠผ ์ž…๋ ฅ ๋Œ€๊ธฐ ์ค‘...', mapped: '{key}์— ๋ฌผ๋ฆฌ ๋ฒ„ํŠผ {button} ๋งคํ•‘ ์™„๋ฃŒ', serverPort: '์„œ๋ฒ„ ํฌํŠธ', websocketPort: '์›น์†Œ์ผ“ ํฌํŠธ', infoPosition: '์„ธ์…˜ ์ •๋ณด ํ‘œ์‹œ ์œ„์น˜', top: '์ƒ๋‹จ', bottom: 'ํ•˜๋‹จ', none: '์—†์Œ', buttonLayout: '๋ฒ„ํŠผ ๋ ˆ์ด์•„์›ƒ', globalMA: '์ „์ฒด Release ์ˆ˜์ง‘ ํ‘œ๋ณธ ๊ฐฏ์ˆ˜ (์ˆ˜์น˜๊ฐ€ ๋†’์„์ˆ˜๋ก ๋ณ€ํ™”์— ๋‘”๊ฐ)', perButtonMA: '๋ฒ„ํŠผ๋ณ„ Release ์ˆ˜์ง‘ ํ‘œ๋ณธ ๊ฐฏ์ˆ˜ (์ˆ˜์น˜๊ฐ€ ๋†’์„์ˆ˜๋ก ๋ณ€ํ™”์— ๋‘”๊ฐ)', appearance: '์œ„์ ฏ ์™ธ๊ด€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•', discImage: '์Šคํฌ๋ž˜์น˜ ์ปค์Šคํ…€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ', delete: '์‚ญ์ œ', background: '๋ฐฐ๊ฒฝ/์Šคํฌ๋ž˜์น˜ ์ƒ‰์ƒ', accent: '๋ฒ„ํŠผ/์Šคํฌ๋ž˜์น˜ ์ž…๋ ฅ ์ „ ์ƒ‰์ƒ', active: '๋ฒ„ํŠผ/์Šคํฌ๋ž˜์น˜ ์ž…๋ ฅ ์‹œ ์ƒ‰์ƒ', font: 'ํฐํŠธ/์Šคํฌ๋ž˜์น˜ ๊ฐ€๋กœ์„  ์ƒ‰์ƒ', save: '์ €์žฅ', cancel: '์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์ข…๋ฃŒ', invalidPort: 'โ— ํฌํŠธ ๋ฒˆํ˜ธ๋Š” 1024 ~ 65535 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.', saved: '์ €์žฅ ์™„๋ฃŒ! OBS์˜ ๋ธŒ๋ผ์šฐ์ € ์†Œ์Šค ์†์„ฑ์—์„œ "ํ˜„์žฌ ํŽ˜์ด์ง€์˜ ์บ์‹œ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ" ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”!' }, + chatter: { title: '์ฑ„ํ„ฐ๋ง ๊ฐ์ง€๊ธฐ', waiting: '๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๋Œ€๊ธฐ ์ค‘...', none: '์•„์ง ๊ฐ์ง€๋œ ์ฑ„ํ„ฐ๋ง ์—†์Œ', count: '๋ฒ„ํŠผ {button} : {count} ํšŒ' }, logs: { title: '๋กœ๊ทธ ๋ณด๊ธฐ' } + }, + en: { + menu: { main: 'Menu', language: 'Language', settings: 'Settings', logs: 'Logs', uploadCount: 'Upload play count', chatter: 'Chatter detector', about: 'About', contributors: 'Contributors', checkUpdates: 'Check for updates', restart: 'Restart', quit: 'Quit' }, + common: { ok: 'OK', yes: 'Yes', no: 'No', later: 'Later', error: 'Error', notice: 'Notice' }, + about: { message: 'IIDXwidget v{version}\nDeveloper: Sadang\nhttps://github.com/Coldlapse/IIDXwidget', contributors: 'Contributors: rhombus9, ๋ฉ˜ํƒˆ๋ฐ”์‚ฌ์‚ญ' }, + upload: { noToken: 'API token is not configured.', noData: 'There is no play count to upload.', confirmTitle: 'Confirm play-count upload', confirm: 'Upload the current count of {count} to the server?\nThe OBS count remains unchanged and the app count will reset to 0.', failed: 'Upload failed', invalidToken: 'The token is invalid.', success: 'Upload successful', successMessage: 'Complete. (Daily total: {count})', error: 'An error occurred: {message}' }, + update: { availableTitle: 'Update available', available: 'Version {version} is available!\n\nChanges:\n{notes}', update: 'Update', skip: 'Skip this version', current: 'You are using the latest version.', errorTitle: 'Update error', error: 'An error occurred while checking for updates:\n{message}', readyTitle: 'Update ready', ready: 'The update has downloaded.\nRestart and install now?', restartNow: 'Restart now', later: 'Later' }, + controller: { notFound: 'Could not find the {profile} device.', connecting: 'Connecting to {profile}', connected: '{profile} connected', openFailed: 'Failed to open the {profile} device', deviceError: 'Device error', closed: 'HID device closed safely', closeFailed: 'Failed to close HID device', dataError: 'Controller data error' }, + settings: { title: 'Settings', app: 'Application settings', autoLaunch: 'Launch automatically with Windows', promo: 'Show widget promotion box', controllerProfile: 'Controller Profile', auto: 'Auto-detect', keyboard: 'Keyboard', apiToken: 'beatmania.app play-count token', apiPlaceholder: 'Enter the token issued by the website', lr2: 'Detect LR2 mode (dedicated controller only)', keyboardMapping: 'Keyboard mapping', genericMapping: 'AUTO generic controller mapping', genericHelp: 'Save Auto-detect, reopen Settings, click each field, and press the desired controller button. Press Save when finished.', logicalKey: 'IIDX key {key}', turntableClockwise: 'Turntable clockwise', turntableCounterclockwise: 'Turntable counterclockwise', listening: '{key}: listening for a button...', mapped: 'Mapped physical button {button} to {key}', serverPort: 'Server port', websocketPort: 'WebSocket port', infoPosition: 'Session information position', top: 'Top', bottom: 'Bottom', none: 'None', buttonLayout: 'Button layout', globalMA: 'Global release sample count (higher values react more slowly)', perButtonMA: 'Per-button release sample count (higher values react more slowly)', appearance: 'Widget appearance', discImage: 'Upload custom turntable image', delete: 'Delete', background: 'Background/turntable color', accent: 'Inactive button/turntable color', active: 'Active button/turntable color', font: 'Font/turntable line color', save: 'Save', cancel: 'Close without saving', invalidPort: 'โ— Port numbers must be between 1024 and 65535.', saved: 'Saved! In OBS browser source properties, click โ€œRefresh cache of current pageโ€.' }, + chatter: { title: 'Chatter detector', waiting: 'Waiting for data...', none: 'No chatter detected yet', count: 'Button {button}: {count}' }, logs: { title: 'Logs' } + } +}; +Object.assign(translations.ko.settings, { + containerBackground: '์œ„์ ฏ ์ปจํ…Œ์ด๋„ˆ ๋ฐฐ๊ฒฝ์ƒ‰', + transparentContainer: '์œ„์ ฏ ์ปจํ…Œ์ด๋„ˆ ๋ฐฐ๊ฒฝ ํˆฌ๋ช…ํ•˜๊ฒŒ', + background: '์Šคํฌ๋ž˜์น˜ ๋ฐฐ๊ฒฝ์ƒ‰' +}); +Object.assign(translations.en.settings, { + containerBackground: 'Widget container background', + transparentContainer: 'Transparent container background', + background: 'Turntable background' +}); +translations.ko.readme = { + title: 'OBS ์„ค์ • ์•ˆ๋‚ด', + obsSetup: 'OBS ์„ค์ • ๋ฐฉ๋ฒ•', + obsInstructions: '1. IIDXwidget์„ ์‹คํ–‰ํ•œ ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜์„ธ์š”.\n2. OBS์—์„œ ์†Œ์Šค โ†’ + โ†’ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์„ ํƒํ•˜์„ธ์š”.\n3. URL์— http://127.0.0.1:{serverPort}/widget ์„ ์ž…๋ ฅํ•˜์„ธ์š”.\n4. ๋„ˆ๋น„ 1000, ๋†’์ด 800์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.\n5. ํ™•์ธ์„ ๋ˆ„๋ฅด์„ธ์š”.\n6. ํ™”๋ฉด์ด ๊ฐฑ์‹ ๋˜์ง€ ์•Š์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ € ์†Œ์Šค ์†์„ฑ์—์„œ โ€œํ˜„์žฌ ํŽ˜์ด์ง€์˜ ์บ์‹œ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจโ€์„ ๋ˆ„๋ฅด์„ธ์š”.\n\nWebSocket์€ ws://127.0.0.1:{webSocketPort} ์— ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ๋˜๋ฏ€๋กœ OBS์—์„œ ๋ณ„๋„๋กœ ์„ค์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' +}; +translations.en.readme = { + title: 'OBS Setup Guide', + obsSetup: 'How to set up OBS', + obsInstructions: '1. Keep IIDXwidget running.\n2. In OBS, select Sources โ†’ + โ†’ Browser.\n3. Enter http://127.0.0.1:{serverPort}/widget as the URL.\n4. A width of 1000 and height of 800 is recommended.\n5. Click OK.\n6. If the display does not update, open the Browser Source properties and click โ€œRefresh cache of current page.โ€\n\nThe widget connects automatically to ws://127.0.0.1:{webSocketPort}; no separate WebSocket configuration is needed in OBS.' +}; +function normalizeLanguage(language) { return language === 'en' ? 'en' : 'ko'; } +function getNested(object, key) { return key.split('.').reduce((value, part) => value && value[part], object); } +function translate(language, key, replacements = {}) { const value = getNested(translations[normalizeLanguage(language)], key) ?? getNested(translations.ko, key) ?? key; return String(value).replace(/\{(\w+)\}/g, (_, name) => replacements[name] ?? `{${name}}`); } +module.exports = { translations, normalizeLanguage, translate }; diff --git a/iidxwidget-app/main.js b/iidxwidget-app/main.js index 6422924..b9b16df 100644 --- a/iidxwidget-app/main.js +++ b/iidxwidget-app/main.js @@ -8,8 +8,9 @@ const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); const { startServer, stopServer } = require('./server'); const { startWebSocketServer, stopWebSocketServer, broadcastControllerData } = require('./wsServer'); -const { startControllerReader } = require('./controller/controllerReader'); +const { startControllerReader, startAutoControllerReader } = require('./controller/controllerReader'); const { startGlobalKeyboardReader } = require('./controller/keyboardReader'); +const { normalizeLanguage, translate } = require('./localization/translations'); let mainWindow; @@ -20,10 +21,11 @@ let webSocketInstance; let chatterWindow = null; const defaultSettings = { + language: 'ko', apiToken: "", serverPort: 8080, webSocketPort: 5678, - controllerProfile: 'PHOENIXWAN', + controllerProfile: 'AUTO', lr2ModeEnabled: false, autoLaunch: false, keyMapping: { @@ -37,16 +39,19 @@ const defaultSettings = { "5": "KeyJ", "6": "KeyK", "7": "KeyL" - } + }, + GENERIC: { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, SCup: 8, SCdown: 9 } }, widget: { infoPosition: "bottom", buttonLayout: "1P", discImagePath: null, showPromoBox: false, + transparentContainer: false, GlobalReleaseMALength: 200, PerButtonMALength: 200, colors: { + containerBackground: "#000000", background: "#000000", accent: "#444444", fontColor: "#cccccc", @@ -140,9 +145,10 @@ function createLogsWindow() { // โœ… ํƒ€๊ฑด ๊ธฐ๋ก ์ „์†ก ํ•จ์ˆ˜ async function sendTypingCount() { + const t = (key, replacements) => translate(settings.language, key, replacements); const token = settings.apiToken; if (!token) { - dialog.showMessageBox({ type: 'error', title: '์˜ค๋ฅ˜', message: 'API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.' }); + dialog.showMessageBox({ type: 'error', title: t('common.error'), message: t('upload.noToken') }); return; } @@ -154,17 +160,16 @@ async function sendTypingCount() { // 2. ์œ„์ ฏ์œผ๋กœ๋ถ€ํ„ฐ ์นด์šดํŠธ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋ฐ›๋„๋ก ๋ฆฌ์Šค๋„ˆ ์„ค์ • ipcMain.once('session-count', async (event, count) => { if (count === 0) { - dialog.showMessageBox({ type: 'info', title: '์•Œ๋ฆผ', message: '์ „์†กํ•  ํƒ€๊ฑด ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.' }); + dialog.showMessageBox({ type: 'info', title: t('common.notice'), message: t('upload.noData') }); return; } // 3. ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „์†ก ์—ฌ๋ถ€ ํ™•์ธ const result = dialog.showMessageBoxSync(mainWindow, { type: 'question', - buttons: ['์˜ˆ', '์•„๋‹ˆ์˜ค'], + buttons: [t('common.yes'), t('common.no')], defaultId: 0, cancelId: 1, - title: 'ํƒ€๊ฑด ๊ธฐ๋ก ์ „์†ก ํ™•์ธ', - message: `ํ˜„์žฌ ํƒ€๊ฑด ์ˆ˜ ${count}ํšŒ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.\nOBS์˜ ์ˆ˜์น˜๋Š” ๊ทธ๋Œ€๋กœ ๋‚จ๊ณ , ์•ฑ ํ™”๋ฉด์˜ ํƒ€๊ฑด ์ˆ˜์น˜๋Š” 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?` + title: t('upload.confirmTitle'), message: t('upload.confirm', { count }) }); if (result === 1) return; // '์•„๋‹ˆ์˜ค' ์„ ํƒ @@ -182,14 +187,14 @@ async function sendTypingCount() { }); if (response.status === 401) { - dialog.showMessageBox({ type: 'error', title: '์ „์†ก ์‹คํŒจ', message: '์ž˜๋ชป๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.' }); + dialog.showMessageBox({ type: 'error', title: t('upload.failed'), message: t('upload.invalidToken') }); return; } if (!response.ok) throw new Error(`์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜: ${response.statusText}`); const data = await response.json(); if (data.status === 'success') { - dialog.showMessageBox({ type: 'info', title: '์ „์†ก ์„ฑ๊ณต', message: `์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์ผ์ผ ์ด ํƒ€๊ฑด ์ˆ˜: ${data.daily_total})` }); + dialog.showMessageBox({ type: 'info', title: t('upload.success'), message: t('upload.successMessage', { count: data.daily_total }) }); // 5. ์„ฑ๊ณต ์‹œ ์•ฑ์˜ ์œ„์ ฏ(mainWindow)์—๋งŒ ์ดˆ๊ธฐํ™” ๋ช…๋ น ์ „์†ก if (mainWindow) { mainWindow.webContents.send('reset-session-count'); @@ -199,52 +204,78 @@ async function sendTypingCount() { } } catch (error) { console.error('โŒ API ์ „์†ก ์˜ค๋ฅ˜:', error); - dialog.showMessageBox({ type: 'error', title: '์ „์†ก ์‹คํŒจ', message: `์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}` }); + dialog.showMessageBox({ type: 'error', title: t('upload.failed'), message: t('upload.error', { message: error.message }) }); } }); } function createStatusMenu() { + const language = normalizeLanguage(settings.language); + const t = (key, replacements) => translate(language, key, replacements); const menu = Menu.buildFromTemplate([ { - label: '๋ฉ”๋‰ด', + label: t('menu.main'), submenu: [ - { label: '์„ค์ •', click: createSettingsWindow }, - { label: '๋กœ๊ทธ', click: createLogsWindow }, + { label: t('menu.settings'), click: createSettingsWindow }, + { label: t('menu.logs'), click: createLogsWindow }, { type: 'separator' }, - { label: 'ํƒ€๊ฑด ๊ธฐ๋ก ์„œ๋ฒ„๋กœ ์ „์†ก', click: sendTypingCount}, - { label: '์ฑ„ํ„ฐ๋ง ๊ฐ์ง€', click: createChatterWindow }, + { label: t('menu.uploadCount'), click: sendTypingCount}, + { label: t('menu.chatter'), click: createChatterWindow }, { type: 'separator' }, - { label: '์ •๋ณด', click: () => { + { label: t('menu.about'), click: () => { const { dialog } = require('electron'); dialog.showMessageBox({ type: 'info', - title: '์ •๋ณด', - message: `IIDXwidget v${appVersion}\n๊ฐœ๋ฐœ์ž: Sadang\nhttps://github.com/Coldlapse/IIDXwidget`, - buttons: ['ํ™•์ธ'] + title: t('menu.about'), message: t('about.message', { version: appVersion }), buttons: [t('common.ok')] }); } }, - { label: '๊ธฐ์—ฌ์ž', click: () => { + { label: t('menu.contributors'), click: () => { const { dialog } = require('electron'); dialog.showMessageBox({ type: 'info', - title: '๊ธฐ์—ฌ์ž', - message: '๊ธฐ์—ฌ์ž : rhombus9, ๋ฉ˜ํƒˆ๋ฐ”์‚ฌ์‚ญ', - buttons: ['ํ™•์ธ'] + title: t('menu.contributors'), message: t('about.contributors'), buttons: [t('common.ok')] }); } }, { type: 'separator' }, - { label: '์—…๋ฐ์ดํŠธ ํ™•์ธ', click: () => manualUpdateCheck() }, - { label: '์žฌ์‹œ์ž‘', click: restartApp }, - { label: '๋๋‚ด๊ธฐ', click: () => app.quit() } + { label: t('menu.checkUpdates'), click: () => manualUpdateCheck() }, + { label: t('menu.restart'), click: restartApp }, + { label: t('menu.quit'), click: () => app.quit() } + ] + }, + { + label: t('menu.language'), submenu: [ + { label: 'ํ•œ๊ตญ์–ด', type: 'radio', checked: language === 'ko', click: () => setLanguage('ko') }, + { label: 'English', type: 'radio', checked: language === 'en', click: () => setLanguage('en') } ] + }, + { + label: 'README', + submenu: [{ + label: t('readme.obsSetup'), + click: () => dialog.showMessageBox({ + type: 'info', + title: t('readme.title'), + message: t('readme.obsInstructions', { + serverPort: settings.serverPort || 8080, + webSocketPort: settings.webSocketPort || 5678 + }), + buttons: [t('common.ok')] + }) + }] } ]); Menu.setApplicationMenu(menu); } +function setLanguage(language) { + settings.language = normalizeLanguage(language); + fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); + createStatusMenu(); + BrowserWindow.getAllWindows().forEach(win => { if (!win.isDestroyed()) win.webContents.send('language-changed', settings.language); }); +} + function restartApp() { console.log('๐Ÿ”„ Restarting app...'); stopServer(); @@ -264,31 +295,7 @@ function restartApp() { serverInstance = startServer(settings.serverPort, userImageDir); webSocketInstance = startWebSocketServer(settings.webSocketPort); - if (settings.controllerProfile === 'PHOENIXWAN' || settings.controllerProfile === 'FPS EMP Gen2') { - const { startControllerReader } = require('./controller/controllerReader'); - controllerInstance = startControllerReader(settings.controllerProfile, data => { - if (mainWindow) mainWindow.webContents.send('controller-data', data); - broadcastControllerData(data); - }, { lr2ModeEnabled: settings.lr2ModeEnabled }); - - currentHIDDevice = controllerInstance; // โœ… ์ด๊ฑฐ ์ถ”๊ฐ€! - } else if (settings.controllerProfile === 'KB') { - const defaultMap = { - SCup: "ShiftLeft", - SCdown: "ControlLeft", - "1": "KeyS", - "2": "KeyD", - "3": "KeyF", - "4": "Space", - "5": "KeyJ", - "6": "KeyK", - "7": "KeyL" - }; - const map = Object.assign({}, defaultMap, settings.keyMapping?.KB || {}); - keyboardInstance = startGlobalKeyboardReader(map, data => { - if (mainWindow) mainWindow.webContents.send('controller-data', [data]); - }); - } + startConfiguredController(); if (mainWindow) mainWindow.reload(); }, 300); @@ -306,6 +313,7 @@ console.log = (...args) => { // ๐Ÿ“ก IPC ipcMain.handle('get-websocket-port', () => settings.webSocketPort || 5678); +ipcMain.handle('get-language', () => normalizeLanguage(settings.language)); ipcMain.handle('request-log-buffer', () => logBuffer); ipcMain.handle('get-app-version', () => { return app.getVersion(); @@ -323,15 +331,10 @@ ipcMain.handle('load-settings', () => { ipcMain.handle('save-settings', async (event, newSettings) => { try { + newSettings.language = normalizeLanguage(newSettings.language ?? settings.language); fs.writeFileSync(SETTINGS_FILE, JSON.stringify(newSettings, null, 2)); settings = newSettings; - if (settings.controllerProfile === 'KB') { - startKBMode(); - } else { - startPHOENIXWANMode(settings.controllerProfile, settings.lr2ModeEnabled); // โœ… ์ˆ˜์ • - } - app.setLoginItemSettings({ openAtLogin: newSettings.autoLaunch, path: app.getPath('exe') @@ -423,10 +426,42 @@ function startPHOENIXWANMode(profile = 'PHOENIXWAN', lr2DetectEnabled = false) { } console.log(`๐ŸŽฎ Starting controller reader for profile: ${profile}`); - currentHIDDevice = startControllerReader(profile, (data) => { - if (mainWindow) mainWindow.webContents.send('controller-data', data); - broadcastControllerData(data); - }, { lr2ModeEnabled: lr2DetectEnabled }); + currentHIDDevice = startControllerReader(profile, dispatchControllerData, { lr2ModeEnabled: lr2DetectEnabled, logger: controllerLogger }); +} + +function startAutoControllerMode() { + stopInputReaders(); + currentHIDDevice = startAutoControllerReader(dispatchControllerData, { + genericMapping: settings.keyMapping?.GENERIC || {}, lr2ModeEnabled: settings.lr2ModeEnabled, logger: controllerLogger + }); +} + +function stopInputReaders() { + if (currentHIDDevice?.close) { try { currentHIDDevice.close(); } catch (e) {} currentHIDDevice = null; } + if (currentKBReader?.stop) { try { currentKBReader.stop(); } catch (e) {} currentKBReader = null; } +} + +function dispatchControllerData(data) { + const widgetData = data.filter(event => event.type !== 'physical-button'); + if (settingsWindow && !settingsWindow.isDestroyed()) settingsWindow.webContents.send('controller-data', data); + if (mainWindow && !mainWindow.isDestroyed() && widgetData.length) mainWindow.webContents.send('controller-data', widgetData); + if (chatterWindow && !chatterWindow.isDestroyed() && widgetData.length) chatterWindow.webContents.send('controller-data', widgetData); + if (widgetData.length) broadcastControllerData(widgetData); +} + +function controllerLogger(level, code, details = {}) { + const message = translate(settings.language, `controller.${code}`, details); + console[level]?.(`${level === 'error' ? 'โŒ' : '๐ŸŽฎ'} ${message}`, details.error || ''); +} + +function startConfiguredController() { + switch (settings.controllerProfile) { + case 'KB': startKBMode(); break; + case 'AUTO': startAutoControllerMode(); break; + case 'PHOENIXWAN': + case 'FPS EMP Gen2': startPHOENIXWANMode(settings.controllerProfile, settings.lr2ModeEnabled); break; + default: startAutoControllerMode(); break; + } } function startKBMode() { @@ -462,6 +497,7 @@ function startKBMode() { } function manualUpdateCheck() { + const t = (key, replacements) => translate(settings.language, key, replacements); autoUpdater.autoDownload = false; autoUpdater.once('checking-for-update', () => { @@ -475,13 +511,13 @@ function manualUpdateCheck() { .replace(/<[^>]+>/g, '') // HTML ํƒœ๊ทธ ์ œ๊ฑฐ .trim(); - const message = `์ƒˆ ๋ฒ„์ „ ${info.version} ์ด(๊ฐ€) ์žˆ์Šต๋‹ˆ๋‹ค!\n\n๋ณ€๊ฒฝ์‚ฌํ•ญ:\n${plainReleaseNotes}`; + const message = t('update.available', { version: info.version, notes: plainReleaseNotes }); const result = dialog.showMessageBoxSync({ type: 'info', - title: '์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ', + title: t('update.availableTitle'), message: message, - buttons: ['์—…๋ฐ์ดํŠธ', '๋‹ค์Œ์— ํ•˜๊ธฐ'], + buttons: [t('update.update'), t('common.later')], cancelId: 1, defaultId: 0, }); @@ -495,8 +531,7 @@ function manualUpdateCheck() { console.log('โœ… ํ˜„์žฌ ์ตœ์‹  ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.'); dialog.showMessageBox({ type: 'info', - title: '์—…๋ฐ์ดํŠธ ํ™•์ธ', - message: 'ํ˜„์žฌ ์ตœ์‹  ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.' + title: t('menu.checkUpdates'), message: t('update.current') }); }); @@ -504,8 +539,7 @@ function manualUpdateCheck() { console.error('โŒ ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜:', err); dialog.showMessageBox({ type: 'error', - title: '์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜', - message: `์—…๋ฐ์ดํŠธ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:\n${err.message}` + title: t('update.errorTitle'), message: t('update.error', { message: err.message }) }); }); @@ -549,6 +583,7 @@ ipcMain.handle('request-chatter-summary', () => { function checkForUpdateWithUI() { + const t = (key, replacements) => translate(settings.language, key, replacements); autoUpdater.autoDownload = false; autoUpdater.on('update-available', (info) => { @@ -564,13 +599,13 @@ function checkForUpdateWithUI() { .replace(/<[^>]+>/g, '') // HTML ํƒœ๊ทธ ์ œ๊ฑฐ .trim(); - const message = `์ƒˆ ๋ฒ„์ „ ${info.version} ์ด(๊ฐ€) ์žˆ์Šต๋‹ˆ๋‹ค!\n\n๋ณ€๊ฒฝ์‚ฌํ•ญ:\n${plainReleaseNotes}`; + const message = t('update.available', { version: info.version, notes: plainReleaseNotes }); const result = dialog.showMessageBoxSync({ type: 'info', - title: '์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ', + title: t('update.availableTitle'), message: message, - buttons: ['์—…๋ฐ์ดํŠธ', '๋‹ค์Œ์— ํ•˜๊ธฐ', '์ด๋ฒˆ ๋ฒ„์ „ ์Šคํ‚ต'], + buttons: [t('update.update'), t('common.later'), t('update.skip')], cancelId: 1, defaultId: 0, }); @@ -586,9 +621,7 @@ function checkForUpdateWithUI() { autoUpdater.on('update-downloaded', () => { const confirm = dialog.showMessageBoxSync({ type: 'question', - title: '์—…๋ฐ์ดํŠธ ์ค€๋น„ ์™„๋ฃŒ', - message: '์—…๋ฐ์ดํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n์ง€๊ธˆ ์žฌ์‹œ์ž‘ํ•˜๊ณ  ์„ค์น˜ํ• ๊นŒ์š”?', - buttons: ['์ง€๊ธˆ ์žฌ์‹œ์ž‘', '๋‚˜์ค‘์—'], + title: t('update.readyTitle'), message: t('update.ready'), buttons: [t('update.restartNow'), t('update.later')], defaultId: 0, cancelId: 1 }); @@ -643,11 +676,7 @@ app.whenReady().then(() => { serverInstance = startServer(settings.serverPort, userImageDir); webSocketInstance = startWebSocketServer(settings.webSocketPort); - if (settings.controllerProfile === 'KB') { - startKBMode(); - } else { - startPHOENIXWANMode(settings.controllerProfile, settings.lr2ModeEnabled); - } + startConfiguredController(); createMainWindow(); createStatusMenu(); diff --git a/iidxwidget-app/package.json b/iidxwidget-app/package.json index c20ed40..2436293 100644 --- a/iidxwidget-app/package.json +++ b/iidxwidget-app/package.json @@ -7,14 +7,19 @@ "main": "main.js", "scripts": { "start": "electron .", - "build": "electron-builder" + "build": "electron-builder", + "test": "node test/controllerReader.test.js && node test/localization.test.js" }, "build": { "appId": "app.beatmania.iidxwidget", "productName": "IIDXwidget", "artifactName": "IIDXwidget-Setup-${version}.${ext}", "files": [ - "**/*" + "**/*", + "!dist{,/**}", + "!dist-*/**", + "!dist-rebuild{,/**}", + "!dist-fixed{,/**}" ], "directories": { "buildResources": "build" diff --git a/iidxwidget-app/preload.js b/iidxwidget-app/preload.js index 0a7738e..75ff28c 100644 --- a/iidxwidget-app/preload.js +++ b/iidxwidget-app/preload.js @@ -6,6 +6,12 @@ contextBridge.exposeInMainWorld('electronAPI', { startKeyboardReader: () => ipcRenderer.send('start-keyboard-reader'), getWebSocketPort: () => ipcRenderer.invoke('get-websocket-port'), onControllerData: (callback) => ipcRenderer.on('controller-data', (event, data) => callback(data)), + getLanguage: () => ipcRenderer.invoke('get-language'), + onLanguageChanged: callback => { + const listener = (_, language) => callback(language); + ipcRenderer.on('language-changed', listener); + return () => ipcRenderer.removeListener('language-changed', listener); + }, onNewLog: (callback) => ipcRenderer.on('new-log', (event, message) => callback(message)), requestLogBuffer: () => ipcRenderer.invoke('request-log-buffer'), loadSettings: () => ipcRenderer.invoke('load-settings'), @@ -24,4 +30,4 @@ contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('iidxapi', { getAppVersion: () => ipcRenderer.invoke('get-app-version') -}); \ No newline at end of file +}); diff --git a/iidxwidget-app/renderer/chatter/chatter.html b/iidxwidget-app/renderer/chatter/chatter.html index f265684..e96971c 100644 --- a/iidxwidget-app/renderer/chatter/chatter.html +++ b/iidxwidget-app/renderer/chatter/chatter.html @@ -2,7 +2,7 @@ - ์ฑ„ํ„ฐ๋ง ๊ฐ์ง€๊ธฐ + ์ฑ„ํ„ฐ๋ง ๊ฐ์ง€๊ธฐ -

์ฑ„ํ„ฐ๋ง ๊ฐ์ง€๊ธฐ

-
๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๋Œ€๊ธฐ ์ค‘...
- +

์ฑ„ํ„ฐ๋ง ๊ฐ์ง€๊ธฐ

+
๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๋Œ€๊ธฐ ์ค‘...
+ diff --git a/iidxwidget-app/renderer/chatter/chatter.js b/iidxwidget-app/renderer/chatter/chatter.js index 54f8ef4..9098f63 100644 --- a/iidxwidget-app/renderer/chatter/chatter.js +++ b/iidxwidget-app/renderer/chatter/chatter.js @@ -5,9 +5,9 @@ const chatterCounts = {}; function updateUI() { let output = ''; Object.keys(chatterCounts).forEach(btn => { - output += `๋ฒ„ํŠผ ${btn} : ${chatterCounts[btn]} ํšŒ\n`; + output += `${window.i18n.t('chatter.count', { button: btn, count: chatterCounts[btn] })}\n`; }); - logEl.textContent = output || '์•„์ง ๊ฐ์ง€๋œ ์ฑ„ํ„ฐ๋ง ์—†์Œ'; + logEl.textContent = output || window.i18n.t('chatter.none'); } // ์š”์•ฝ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ํ•จ์ˆ˜ @@ -26,3 +26,4 @@ fetchSummary(); // ์ดํ›„ 1์ดˆ๋งˆ๋‹ค ๊ฐฑ์‹  setInterval(fetchSummary, 1000); +document.addEventListener('i18n-changed', updateUI); diff --git a/iidxwidget-app/renderer/logs/logs.html b/iidxwidget-app/renderer/logs/logs.html index 778d3cf..74f7ff4 100644 --- a/iidxwidget-app/renderer/logs/logs.html +++ b/iidxwidget-app/renderer/logs/logs.html @@ -2,7 +2,7 @@ - ๋กœ๊ทธ ๋ณด๊ธฐ + ๋กœ๊ทธ ๋ณด๊ธฐ