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 ua = navigator.userAgent;
|
||||||
const isChromeOrEdge = (ua.indexOf('Chrome') > -1 || ua.indexOf('Edg') > -1) && !/Mobile|Android/.test(ua);
|
const isChromeOrEdge = (ua.indexOf('Chrome') > -1 || ua.indexOf('Edg') > -1) && !/Mobile|Android/.test(ua);
|
||||||
const hasFileSystemApi = 'showDirectoryPicker' in window;
|
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';
|
selectLocalBtn.style.display = 'none';
|
||||||
browserWarning.style.display = 'block';
|
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')) {
|
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;
|
const isChange = e.target.closest('#change-local-folder-btn') !== null;
|
||||||
try {
|
try {
|
||||||
const handle = await window.showDirectoryPicker({
|
const isNeutralino =
|
||||||
id: 'music-folder',
|
window.Neutralino && (window.NL_MODE || window.location.search.includes('mode=neutralino'));
|
||||||
mode: 'read',
|
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);
|
await db.saveSetting('local_folder_handle', handle);
|
||||||
if (isChange) {
|
if (isChange) {
|
||||||
|
|
@ -1985,32 +2004,67 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const tracks = [];
|
const tracks = [];
|
||||||
let idCounter = 0;
|
let idCounter = 0;
|
||||||
|
const { readTrackMetadata } = await loadMetadataModule();
|
||||||
|
|
||||||
async function scanDirectory(dirHandle) {
|
if (isNeutralino) {
|
||||||
for await (const entry of dirHandle.values()) {
|
async function scanDirectoryNeu(dirPath) {
|
||||||
if (entry.kind === 'file') {
|
const entries = await window.Neutralino.filesystem.readDirectory(dirPath);
|
||||||
const name = entry.name.toLowerCase();
|
for (const entry of entries) {
|
||||||
if (
|
if (entry.entry === '.' || entry.entry === '..') continue;
|
||||||
name.endsWith('.flac') ||
|
const fullPath = `${dirPath}/${entry.entry}`;
|
||||||
name.endsWith('.mp3') ||
|
if (entry.type === 'FILE') {
|
||||||
name.endsWith('.m4a') ||
|
const name = entry.entry.toLowerCase();
|
||||||
name.endsWith('.wav') ||
|
if (
|
||||||
name.endsWith('.ogg')
|
name.endsWith('.flac') ||
|
||||||
) {
|
name.endsWith('.mp3') ||
|
||||||
const file = await entry.getFile();
|
name.endsWith('.m4a') ||
|
||||||
const { readTrackMetadata } = await loadMetadataModule();
|
name.endsWith('.wav') ||
|
||||||
const metadata = await readTrackMetadata(file);
|
name.endsWith('.ogg')
|
||||||
metadata.id = `local-${idCounter++}-${file.name}`;
|
) {
|
||||||
tracks.push(metadata);
|
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) => {
|
tracks.sort((a, b) => {
|
||||||
const artistA = a.artist.name || '';
|
const artistA = a.artist.name || '';
|
||||||
const artistB = b.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 }, '*');
|
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 = {
|
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) => {
|
writeBinaryFile: async (path, buffer) => {
|
||||||
if (!isNeutralino) return;
|
if (!isNeutralino) return;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,88 @@
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'NL_FS_WRITE_BINARY':
|
||||||
try {
|
try {
|
||||||
// buffer comes as ArrayBuffer in event.data.buffer (if transferred) or event.data.buffer
|
// buffer comes as ArrayBuffer in event.data.buffer (if transferred) or event.data.buffer
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue