Fusion des scans (désinstallés, doublons, ajouts manuels), rescan, ajout manuel et explorateur de fichiers pour la manette
Some checks are pending
CI / windows (push) Waiting to run
Some checks are pending
CI / windows (push) Waiting to run
This commit is contained in:
parent
bcc96d5fe5
commit
f68ed4112f
8 changed files with 179 additions and 7 deletions
|
|
@ -2,6 +2,7 @@
|
|||
import { join } from 'node:path'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { emit, handle, registerCore } from './ipc'
|
||||
import { registerLibrary } from './library'
|
||||
import { initLog, log } from './log'
|
||||
import { Store } from './store'
|
||||
import { registerThemeScheme, Themes } from './themes'
|
||||
|
|
@ -56,19 +57,22 @@ app.on('second-instance', () => {
|
|||
})
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerCore({
|
||||
const ctx = {
|
||||
store,
|
||||
dataDir,
|
||||
win: () => win,
|
||||
onSettings: (s, before) => {
|
||||
onSettings: (s: typeof store.settings, before: typeof store.settings) => {
|
||||
if (win && s.fullscreen !== before.fullscreen) win.setFullScreen(s.fullscreen)
|
||||
}
|
||||
})
|
||||
}
|
||||
registerCore(ctx)
|
||||
const library = registerLibrary(ctx)
|
||||
themes.registerProtocol()
|
||||
handle('themes:list', async () => themes.list())
|
||||
themes.watch((id) => emit(win, 'themes:changed', { id, themes: themes.list() }))
|
||||
win = createWindow()
|
||||
L.info('démarrage', app.getVersion())
|
||||
win.webContents.once('did-finish-load', () => library.rescan()) // scan au démarrage, l'interface affiche déjà la bibliothèque connue
|
||||
})
|
||||
|
||||
app.on('before-quit', () => store.flush())
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@ export function registerCore(ctx: Ctx): void {
|
|||
handle('library:update', async (id, patch) => {
|
||||
const g = store.games.get(str(id, 300, 'id'))
|
||||
if (!g) throw new Error('jeu introuvable')
|
||||
Object.assign(g, gamePatch(patch))
|
||||
const p = gamePatch(patch)
|
||||
if (p.name !== undefined && p.name !== g.name) g.nameLocked = true
|
||||
if (p.args !== undefined || p.workDir !== undefined) g.argsLocked = true
|
||||
Object.assign(g, p)
|
||||
changed()
|
||||
return g
|
||||
})
|
||||
|
|
|
|||
68
src/main/library.ts
Normal file
68
src/main/library.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/* Bibliothèque : scan, ajout manuel, suppression, explorateur de fichiers pour la manette. */
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { readdirSync, statSync } from 'node:fs'
|
||||
import { dirname, join, parse } from 'node:path'
|
||||
import type { FsFilter, FsListing, ScanReport } from '../shared/ipc'
|
||||
import { emit, handle, type Ctx } from './ipc'
|
||||
import { log } from './log'
|
||||
import { gameId, mergeScan } from './scanners/merge'
|
||||
import { runScanners } from './scanners'
|
||||
import { manualInput, str } from './validate'
|
||||
|
||||
const L = log('library')
|
||||
const EXT: Record<FsFilter, RegExp> = { exe: /\.(exe|bat|cmd|lnk|url)$/i, image: /\.(png|jpe?g|webp|gif)$/i, dir: /^$/ }
|
||||
|
||||
export function listDir(dir: string, filter: FsFilter): FsListing {
|
||||
if (!dir) {
|
||||
// racine : lecteurs disponibles (Windows) ou / ailleurs
|
||||
if (process.platform !== 'win32') return listDir('/', filter)
|
||||
const drives = 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((d) => d + ':\\').filter((d) => { try { statSync(d); return true } catch { return false } })
|
||||
return { dir: '', parent: null, entries: drives.map((d) => ({ name: d, path: d, dir: true })) }
|
||||
}
|
||||
const entries = readdirSync(dir, { withFileTypes: true })
|
||||
.filter((e) => !e.name.startsWith('.') && !e.name.startsWith('$') && (e.isDirectory() || EXT[filter].test(e.name)))
|
||||
.map((e) => ({ name: e.name, path: join(dir, e.name), dir: e.isDirectory() }))
|
||||
.sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name, 'fr', { sensitivity: 'base' }))
|
||||
const root = parse(dir).root
|
||||
return { dir, parent: dir === root ? '' : dirname(dir), entries: entries.slice(0, 2000) }
|
||||
}
|
||||
|
||||
export function registerLibrary(ctx: Ctx): { rescan: () => Promise<ScanReport> } {
|
||||
const { store } = ctx
|
||||
let running: Promise<ScanReport> | null = null
|
||||
|
||||
const rescan = () =>
|
||||
(running ??= (async () => {
|
||||
emit(ctx.win(), 'scan:state', { running: true })
|
||||
const res = await runScanners(store.settings)
|
||||
const { games, stats } = mergeScan(store.list(), res)
|
||||
store.games = new Map(games.map((g) => [g.id, g]))
|
||||
store.save()
|
||||
const report = { ...stats, failed: res.failed, total: games.length }
|
||||
L.info('scan terminé', report)
|
||||
emit(ctx.win(), 'library:changed', store.list())
|
||||
emit(ctx.win(), 'scan:state', { running: false, report })
|
||||
return report
|
||||
})().finally(() => { running = null }))
|
||||
|
||||
handle('library:rescan', rescan)
|
||||
handle('library:addManual', async (input) => {
|
||||
const m = manualInput(input)
|
||||
const g = { id: gameId({ platform: 'manual', platformId: randomUUID() }), platform: 'manual' as const, platformId: '', ...m, installDir: dirname(m.exe), installed: true, favorite: false, hidden: false, addedAt: Date.now(), playtime: 0, collections: [], media: {}, nameLocked: true, argsLocked: true }
|
||||
g.platformId = g.id.slice('manual:'.length)
|
||||
store.games.set(g.id, g)
|
||||
store.save()
|
||||
emit(ctx.win(), 'library:changed', store.list())
|
||||
return g
|
||||
})
|
||||
handle('library:remove', async (id) => {
|
||||
const g = store.games.get(str(id, 300, 'id'))
|
||||
if (!g) return
|
||||
if (g.platform === 'manual') store.games.delete(g.id)
|
||||
else g.hidden = true // un jeu détecté reviendrait au prochain scan : on le masque
|
||||
store.save()
|
||||
emit(ctx.win(), 'library:changed', store.list())
|
||||
})
|
||||
handle('fs:list', async (dir, filter) => listDir(str(dir, 1000, 'dossier'), filter in EXT ? filter : 'exe'))
|
||||
return { rescan }
|
||||
}
|
||||
|
|
@ -60,3 +60,16 @@ export function gamePatch(p: unknown): GamePatch {
|
|||
}
|
||||
return o
|
||||
}
|
||||
|
||||
export function manualInput(p: unknown): { name: string; exe: string; args?: string; workDir?: string } {
|
||||
if (!p || typeof p !== 'object') throw new InvalidInput('jeu invalide')
|
||||
const i = p as Record<string, unknown>
|
||||
const exe = str(i.exe, 260, 'exécutable')
|
||||
if (!/\.(exe|bat|cmd|lnk|url)$/i.test(exe)) throw new InvalidInput('exécutable .exe, .bat, .cmd, .lnk ou .url attendu')
|
||||
return {
|
||||
name: str(i.name ?? '', 200, 'nom').trim() || exe.split(/[\\/]/).pop()!.replace(/\.\w+$/, ''),
|
||||
exe,
|
||||
args: i.args === undefined ? undefined : str(i.args, 1000, 'arguments'),
|
||||
workDir: i.workDir === undefined || i.workDir === '' ? undefined : str(i.workDir, 260, 'dossier de travail')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue