FIX: local files in desktop app
This commit is contained in:
parent
a9f3acb289
commit
d05a0ea9c0
3 changed files with 220 additions and 25 deletions
104
js/app.js
104
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 || '';
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue