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 @@
-