diff --git a/js/app.js b/js/app.js index 5af1126..b9a41ef 100644 --- a/js/app.js +++ b/js/app.js @@ -374,10 +374,17 @@ document.addEventListener('DOMContentLoaded', async () => { const ua = navigator.userAgent; const isChromeOrEdge = (ua.indexOf('Chrome') > -1 || ua.indexOf('Edg') > -1) && !/Mobile|Android/.test(ua); const hasFileSystemApi = 'showDirectoryPicker' in window; + const isNeutralino = + window.NL_MODE || + window.location.search.includes('mode=neutralino') || + window.location.search.includes('nl_port='); - if (!isChromeOrEdge || !hasFileSystemApi) { + if (!isNeutralino && (!isChromeOrEdge || !hasFileSystemApi)) { selectLocalBtn.style.display = 'none'; browserWarning.style.display = 'block'; + } else if (isNeutralino) { + selectLocalBtn.style.display = 'flex'; + browserWarning.style.display = 'none'; } } @@ -1965,10 +1972,22 @@ document.addEventListener('DOMContentLoaded', async () => { if (e.target.closest('#select-local-folder-btn') || e.target.closest('#change-local-folder-btn')) { const isChange = e.target.closest('#change-local-folder-btn') !== null; try { - const handle = await window.showDirectoryPicker({ - id: 'music-folder', - mode: 'read', - }); + const isNeutralino = + window.Neutralino && (window.NL_MODE || window.location.search.includes('mode=neutralino')); + let handle; + let path; + + if (isNeutralino) { + path = await window.Neutralino.os.showFolderDialog('Select Music Folder'); + if (!path) return; + // Mock a handle object for UI compatibility + handle = { name: path.split(/[/\\]/).pop() || path, isNeutralino: true, path }; + } else { + handle = await window.showDirectoryPicker({ + id: 'music-folder', + mode: 'read', + }); + } await db.saveSetting('local_folder_handle', handle); if (isChange) { @@ -1985,32 +2004,67 @@ document.addEventListener('DOMContentLoaded', async () => { const tracks = []; let idCounter = 0; + const { readTrackMetadata } = await loadMetadataModule(); - async function scanDirectory(dirHandle) { - for await (const entry of dirHandle.values()) { - if (entry.kind === 'file') { - const name = entry.name.toLowerCase(); - if ( - name.endsWith('.flac') || - name.endsWith('.mp3') || - name.endsWith('.m4a') || - name.endsWith('.wav') || - name.endsWith('.ogg') - ) { - const file = await entry.getFile(); - const { readTrackMetadata } = await loadMetadataModule(); - const metadata = await readTrackMetadata(file); - metadata.id = `local-${idCounter++}-${file.name}`; - tracks.push(metadata); + if (isNeutralino) { + async function scanDirectoryNeu(dirPath) { + const entries = await window.Neutralino.filesystem.readDirectory(dirPath); + for (const entry of entries) { + if (entry.entry === '.' || entry.entry === '..') continue; + const fullPath = `${dirPath}/${entry.entry}`; + if (entry.type === 'FILE') { + const name = entry.entry.toLowerCase(); + if ( + name.endsWith('.flac') || + name.endsWith('.mp3') || + name.endsWith('.m4a') || + name.endsWith('.wav') || + name.endsWith('.ogg') + ) { + try { + const buffer = await window.Neutralino.filesystem.readBinaryFile(fullPath); + const stats = await window.Neutralino.filesystem.getStats(fullPath); + const file = new File([buffer], entry.entry, { + lastModified: stats.mtime, + }); + const metadata = await readTrackMetadata(file); + metadata.id = `local-${idCounter++}-${entry.entry}`; + tracks.push(metadata); + } catch (e) { + console.error('Failed to read file:', fullPath, e); + } + } + } else if (entry.type === 'DIRECTORY') { + await scanDirectoryNeu(fullPath); } - } else if (entry.kind === 'directory') { - await scanDirectory(entry); } } + await scanDirectoryNeu(path); + } else { + async function scanDirectory(dirHandle) { + for await (const entry of dirHandle.values()) { + if (entry.kind === 'file') { + const name = entry.name.toLowerCase(); + if ( + name.endsWith('.flac') || + name.endsWith('.mp3') || + name.endsWith('.m4a') || + name.endsWith('.wav') || + name.endsWith('.ogg') + ) { + const file = await entry.getFile(); + const metadata = await readTrackMetadata(file); + metadata.id = `local-${idCounter++}-${file.name}`; + tracks.push(metadata); + } + } else if (entry.kind === 'directory') { + await scanDirectory(entry); + } + } + } + await scanDirectory(handle); } - await scanDirectory(handle); - tracks.sort((a, b) => { const artistA = a.artist.name || ''; const artistB = b.artist.name || ''; diff --git a/js/desktop/neutralino-bridge.js b/js/desktop/neutralino-bridge.js index c4ad4d4..92eb066 100644 --- a/js/desktop/neutralino-bridge.js +++ b/js/desktop/neutralino-bridge.js @@ -84,9 +84,68 @@ export const os = { window.parent.postMessage({ type: 'NL_OS_SHOW_SAVE_DIALOG', id, title, options }, '*'); }); }, + showFolderDialog: async (title, options) => { + if (!isNeutralino) return; + return new Promise((resolve) => { + const id = Math.random().toString(36).substring(7); + const handler = (event) => { + if (event.data?.type === 'NL_RESPONSE' && event.data.id === id) { + window.removeEventListener('message', handler); + resolve(event.data.result); + } + }; + window.addEventListener('message', handler); + window.parent.postMessage({ type: 'NL_OS_SHOW_FOLDER_DIALOG', id, title, options }, '*'); + }); + }, }; export const filesystem = { + readBinaryFile: async (path) => { + if (!isNeutralino) return; + return new Promise((resolve, reject) => { + const id = Math.random().toString(36).substring(7); + const handler = (event) => { + if (event.data?.type === 'NL_RESPONSE' && event.data.id === id) { + window.removeEventListener('message', handler); + if (event.data.error) reject(event.data.error); + else resolve(event.data.result); + } + }; + window.addEventListener('message', handler); + window.parent.postMessage({ type: 'NL_FS_READ_BINARY', id, path }, '*'); + }); + }, + readDirectory: async (path) => { + if (!isNeutralino) return; + return new Promise((resolve, reject) => { + const id = Math.random().toString(36).substring(7); + const handler = (event) => { + if (event.data?.type === 'NL_RESPONSE' && event.data.id === id) { + window.removeEventListener('message', handler); + if (event.data.error) reject(event.data.error); + else resolve(event.data.result); + } + }; + window.addEventListener('message', handler); + window.parent.postMessage({ type: 'NL_FS_READ_DIR', id, path }, '*'); + }); + }, + getStats: async (path) => { + if (!isNeutralino) return; + return new Promise((resolve, reject) => { + const id = Math.random().toString(36).substring(7); + const handler = (event) => { + if (event.data?.type === 'NL_RESPONSE' && event.data.id === id) { + window.removeEventListener('message', handler); + if (event.data.error) reject(event.data.error); + else resolve(event.data.result); + } + }; + window.addEventListener('message', handler); + window.parent.postMessage({ type: 'NL_FS_STATS', id, path }, '*'); + }); + }, writeBinaryFile: async (path, buffer) => { if (!isNeutralino) return; return new Promise((resolve, reject) => { diff --git a/public/neutralino_loader.html b/public/neutralino_loader.html index e91542c..0b7c3a7 100644 --- a/public/neutralino_loader.html +++ b/public/neutralino_loader.html @@ -228,6 +228,88 @@ } break; + case 'NL_OS_SHOW_FOLDER_DIALOG': + try { + const result = await Neutralino.os.showFolderDialog(event.data.title, event.data.options); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, result }, + '*' + ); + } + } catch (e) { + console.error('[Shell] Show Folder Dialog failed:', e); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, error: e }, + '*' + ); + } + } + break; + + case 'NL_FS_READ_BINARY': + try { + const result = await Neutralino.filesystem.readBinaryFile(event.data.path); + if (iframe && iframe.contentWindow) { + // result is ArrayBuffer, should be transferable + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, result }, + '*', + [result] + ); + } + } catch (e) { + console.error('[Shell] Read Binary File failed:', e); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, error: e }, + '*' + ); + } + } + break; + + case 'NL_FS_READ_DIR': + try { + const result = await Neutralino.filesystem.readDirectory(event.data.path); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, result }, + '*' + ); + } + } catch (e) { + console.error('[Shell] Read Directory failed:', e); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, error: e }, + '*' + ); + } + } + break; + + case 'NL_FS_STATS': + try { + const result = await Neutralino.filesystem.getStats(event.data.path); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, result }, + '*' + ); + } + } catch (e) { + console.error('[Shell] Get Stats failed:', e); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { type: 'NL_RESPONSE', id: event.data.id, error: e }, + '*' + ); + } + } + break; + case 'NL_FS_WRITE_BINARY': try { // buffer comes as ArrayBuffer in event.data.buffer (if transferred) or event.data.buffer