Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c226eba8 | ||
|
|
3b74d93b74 | ||
|
|
ff54ab4ef6 | ||
|
|
a7c90b63df | ||
|
|
872962c435 | ||
|
|
b9c4627a77 | ||
|
|
eff7a288cf | ||
|
|
94d419cf01 | ||
|
|
4760d576f3 | ||
|
|
800c9f3f02 | ||
|
|
6c7def3184 | ||
|
|
962279db29 | ||
|
|
bffe286f40 | ||
|
|
abc96dd648 | ||
|
|
6142dbdb25 | ||
|
|
01040ada26 | ||
|
|
ee2d83993a | ||
|
|
0103960645 | ||
|
|
b8e30e13f2 | ||
|
|
b8554af71b | ||
|
|
e130e945d5 | ||
|
|
7c4496ae1d | ||
|
|
cfd0914b9b | ||
|
|
53a6f0abfc |
No files matched your search
@@ -1,5 +1,40 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [3.11.5](https://github.com/Masutayunikon/FanKarr/compare/v3.11.4...v3.11.5) (2026-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* le folder picker affiche tous les disques depuis le bouton homes sous windows ([7149022](https://github.com/Masutayunikon/FanKarr/commit/7149022c8701f79d194c675da87c6cb2d5793a35))
|
||||||
|
|
||||||
|
## [3.11.4](https://github.com/Masutayunikon/FanKarr/compare/v3.11.3...v3.11.4) (2026-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ajout de la mise a jour des noms toutes les 1h ([0fe5c8c](https://github.com/Masutayunikon/FanKarr/commit/0fe5c8ccb7ecdc1643b5e69de3957c39dfd8085e))
|
||||||
|
|
||||||
|
## [3.11.3](https://github.com/Masutayunikon/FanKarr/compare/v3.11.2...v3.11.3) (2026-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* dockerfile node version to 22 ([5983494](https://github.com/Masutayunikon/FanKarr/commit/598349407750c729579bd639680ebc6a8ebcc12f))
|
||||||
|
|
||||||
|
## [3.11.2](https://github.com/Masutayunikon/FanKarr/compare/v3.11.1...v3.11.2) (2026-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ajout de undici dans le package.json ([459e602](https://github.com/Masutayunikon/FanKarr/commit/459e602c1df1c610466be5015a660624b1189620))
|
||||||
|
|
||||||
|
## [3.11.1](https://github.com/Masutayunikon/FanKarr/compare/v3.11.0...v3.11.1) (2026-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* le setup plex peux maintenant etre utilisé sur un serveur local (probleme de certificat) ([6907584](https://github.com/Masutayunikon/FanKarr/commit/69075842be396b149949b158922a9dc84dbc7ccf))
|
||||||
|
|
||||||
## [3.11.0](https://github.com/Masutayunikon/FanKarr/compare/v3.10.0...v3.11.0) (2026-04-23)
|
## [3.11.0](https://github.com/Masutayunikon/FanKarr/compare/v3.10.0...v3.11.0) (2026-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-slim AS base
|
FROM node:22-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -15,7 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# ── Production ─────────────────────────────────────────────────
|
# ── Production ─────────────────────────────────────────────────
|
||||||
FROM node:20-slim
|
FROM node:22-slim
|
||||||
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -28,4 +28,4 @@ RUN mkdir -p /config
|
|||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
|
"undici": "^8.1.0",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+9
@@ -38,6 +38,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
undici:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.25
|
specifier: ^3.5.25
|
||||||
version: 3.5.29(typescript@5.9.3)
|
version: 3.5.29(typescript@5.9.3)
|
||||||
@@ -1343,6 +1346,10 @@ packages:
|
|||||||
undici-types@7.16.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
|
undici@8.1.0:
|
||||||
|
resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==}
|
||||||
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2645,6 +2652,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
|
undici@8.1.0: {}
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
unplugin-utils@0.3.1:
|
unplugin-utils@0.3.1:
|
||||||
|
|||||||
+14
-1
@@ -11,7 +11,7 @@ import transmissionDriver from './torrent-clients/transmission.js'
|
|||||||
import synologyDsDriver from './torrent-clients/synology-ds.js'
|
import synologyDsDriver from './torrent-clients/synology-ds.js'
|
||||||
import utorrentDriver from './torrent-clients/utorrent.js'
|
import utorrentDriver from './torrent-clients/utorrent.js'
|
||||||
import rtorrentDriver from './torrent-clients/rtorrent.js'
|
import rtorrentDriver from './torrent-clients/rtorrent.js'
|
||||||
import { autoOrganizeAll, scanMediaPath } from './organize.js'
|
import { autoOrganizeAll, scanMediaPath, syncFilenameChanges } from './organize.js'
|
||||||
import { logger } from './logger.js'
|
import { logger } from './logger.js'
|
||||||
import { DATA_DIR, BASE_DIR } from './config.js'
|
import { DATA_DIR, BASE_DIR } from './config.js'
|
||||||
import { readAvailable, readInfohashMap, loadEnrichedSeriesData } from './lib/github-cache.js'
|
import { readAvailable, readInfohashMap, loadEnrichedSeriesData } from './lib/github-cache.js'
|
||||||
@@ -139,4 +139,17 @@ server.listen(PORT, async () => {
|
|||||||
checkNfoUpdates().catch(err => logger.error('nfo-update', `Vérif horaire échouée : ${err instanceof Error ? err.message : err}`))
|
checkNfoUpdates().catch(err => logger.error('nfo-update', `Vérif horaire échouée : ${err instanceof Error ? err.message : err}`))
|
||||||
}, 60 * 60_000)
|
}, 60 * 60_000)
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
||||||
|
// Sync automatique des noms de fichiers toutes les heures.
|
||||||
|
// Le TTL du cache GitHub est de 1h — ce setInterval tire toujours sur des données fraîches.
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const seriesData = await loadEnrichedSeriesData()
|
||||||
|
const { renamed } = await syncFilenameChanges(seriesData, path.join(DATA_DIR, 'organized.json'))
|
||||||
|
if (renamed > 0)
|
||||||
|
logger.info('api', `Sync noms auto — ${renamed} fichier(s) renommé(s)`)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('api', `Sync noms auto échoué : ${err instanceof Error ? err.message : err}`)
|
||||||
|
}
|
||||||
|
}, 60 * 60_000)
|
||||||
})
|
})
|
||||||
+41
-12
@@ -1,4 +1,5 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
import { Agent, fetch as undiciFetch } from 'undici'
|
||||||
import { requireAuth } from '../auth.js'
|
import { requireAuth } from '../auth.js'
|
||||||
import { logger } from '../logger.js'
|
import { logger } from '../logger.js'
|
||||||
|
|
||||||
@@ -6,6 +7,16 @@ const router = Router()
|
|||||||
|
|
||||||
const PLEX_TV_API = 'https://plex.tv/api/v2'
|
const PLEX_TV_API = 'https://plex.tv/api/v2'
|
||||||
|
|
||||||
|
// ── Fetch avec bypass SSL pour les serveurs locaux ────────────────────────────
|
||||||
|
function getServerFetch(serverUrl: string) {
|
||||||
|
return (url: string, opts: any = {}) =>
|
||||||
|
undiciFetch(url, {
|
||||||
|
...opts,
|
||||||
|
dispatcher: new Agent({ connect: { rejectUnauthorized: false } }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fetch vers plex.tv (certificat valide, pas besoin de bypass) ──────────────
|
||||||
async function plexFetch(url: string, options: RequestInit = {}): Promise<any> {
|
async function plexFetch(url: string, options: RequestInit = {}): Promise<any> {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
@@ -105,6 +116,10 @@ router.post('/plex/setup', requireAuth, async (req, res) => {
|
|||||||
if (!token || !serverUrl || !libraryName || !libraryPath) {
|
if (!token || !serverUrl || !libraryName || !libraryPath) {
|
||||||
res.status(400).json({ error: 'token, serverUrl, libraryName, libraryPath requis' }); return
|
res.status(400).json({ error: 'token, serverUrl, libraryName, libraryPath requis' }); return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch avec SSL bypass pour les appels vers le serveur Plex local
|
||||||
|
const serverFetch = getServerFetch(serverUrl)
|
||||||
|
|
||||||
const headers: Record<string, string> = { 'X-Plex-Token': token, 'Accept': 'application/json' }
|
const headers: Record<string, string> = { 'X-Plex-Token': token, 'Accept': 'application/json' }
|
||||||
const TARGET_AGENT_URI = 'https://metadata.fankai.fr/plex'
|
const TARGET_AGENT_URI = 'https://metadata.fankai.fr/plex'
|
||||||
const steps: { step: string; ok: boolean; message: string }[] = []
|
const steps: { step: string; ok: boolean; message: string }[] = []
|
||||||
@@ -114,27 +129,30 @@ router.post('/plex/setup', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
// ── Étape 1 : Enregistrer le provider Fankai ─────────────
|
// ── Étape 1 : Enregistrer le provider Fankai ─────────────
|
||||||
try {
|
try {
|
||||||
const providersRes = await fetch(`${serverUrl}/media/providers/metadata`, { headers })
|
const providersRes = await serverFetch(`${serverUrl}/media/providers/metadata`, { headers })
|
||||||
if (providersRes.ok) {
|
if (providersRes.ok) {
|
||||||
const data = await providersRes.json()
|
const data = await providersRes.json() as any
|
||||||
const providers: any[] = data?.MediaContainer?.MetadataAgentProvider ?? []
|
const providers: any[] = data?.MediaContainer?.MetadataAgentProvider ?? []
|
||||||
let provider = providers.find((p: any) => p.uri === TARGET_AGENT_URI)
|
let provider = providers.find((p: any) => p.uri === TARGET_AGENT_URI)
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
const r = await fetch(`${serverUrl}/media/providers/metadata?uri=${encodeURIComponent(TARGET_AGENT_URI)}`, { method: 'POST', headers })
|
const r = await serverFetch(`${serverUrl}/media/providers/metadata?uri=${encodeURIComponent(TARGET_AGENT_URI)}`, { method: 'POST', headers })
|
||||||
if (r.ok) provider = (await r.json())?.MediaContainer?.MetadataAgentProvider?.[0]
|
if (r.ok) provider = ((await r.json()) as any)?.MediaContainer?.MetadataAgentProvider?.[0]
|
||||||
}
|
}
|
||||||
if (provider) {
|
if (provider) {
|
||||||
agentIdentifier = provider.identifier ?? agentIdentifier
|
agentIdentifier = provider.identifier ?? agentIdentifier
|
||||||
steps.push({ step: 'provider', ok: true, message: `Provider enregistré (${agentIdentifier})` })
|
steps.push({ step: 'provider', ok: true, message: `Provider enregistré (${agentIdentifier})` })
|
||||||
}
|
}
|
||||||
const groupsRes = await fetch(`${serverUrl}/media/providers/metadata/group`, { headers })
|
const groupsRes = await serverFetch(`${serverUrl}/media/providers/metadata/group`, { headers })
|
||||||
if (groupsRes.ok) {
|
if (groupsRes.ok) {
|
||||||
const gdata = await groupsRes.json()
|
const gdata = await groupsRes.json() as any
|
||||||
const groups: any[] = gdata?.MediaContainer?.MetadataAgentProviderGroup ?? []
|
const groups: any[] = gdata?.MediaContainer?.MetadataAgentProviderGroup ?? []
|
||||||
let group = groups.find((g: any) => g.primaryIdentifier === agentIdentifier)
|
let group = groups.find((g: any) => g.primaryIdentifier === agentIdentifier)
|
||||||
if (!group) {
|
if (!group) {
|
||||||
const r = await fetch(`${serverUrl}/media/providers/metadata/group?title=Fankai&primaryIdentifier=${encodeURIComponent(agentIdentifier)}`, { method: 'POST', headers })
|
const r = await serverFetch(
|
||||||
if (r.ok) group = (await r.json())?.MediaContainer?.MetadataAgentProviderGroup?.[0]
|
`${serverUrl}/media/providers/metadata/group?title=Fankai&primaryIdentifier=${encodeURIComponent(agentIdentifier)}`,
|
||||||
|
{ method: 'POST', headers },
|
||||||
|
)
|
||||||
|
if (r.ok) group = ((await r.json()) as any)?.MediaContainer?.MetadataAgentProviderGroup?.[0]
|
||||||
}
|
}
|
||||||
if (group) {
|
if (group) {
|
||||||
groupId = String(group.id ?? '')
|
groupId = String(group.id ?? '')
|
||||||
@@ -142,18 +160,29 @@ router.post('/plex/setup', requireAuth, async (req, res) => {
|
|||||||
agentSetupOk = true
|
agentSetupOk = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const status = providersRes.status
|
||||||
|
steps.push({ step: 'agent', ok: false, message: `Agent non configuré (HTTP ${status} — Plex < 1.43 ?) — setup manuel requis` })
|
||||||
|
logger.warn('plex', `Setup agent Fankai — HTTP ${status} depuis ${serverUrl}`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : 'Erreur'
|
const msg = err instanceof Error ? err.message : 'Erreur'
|
||||||
steps.push({ step: 'agent', ok: false, message: `Agent non configuré (Plex < 1.43 ?) — setup manuel requis` })
|
steps.push({ step: 'agent', ok: false, message: `Agent non configuré — setup manuel requis` })
|
||||||
logger.warn('plex', `Setup agent Fankai échoué : ${msg}`)
|
logger.warn('plex', `Setup agent Fankai échoué : ${msg}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Étape 2 : Créer la bibliothèque ──────────────────────
|
// ── Étape 2 : Créer la bibliothèque ──────────────────────
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ type: 'show', name: libraryName, agent: agentIdentifier, scanner: 'Plex TV Series', language: 'fr-FR', location: libraryPath })
|
const params = new URLSearchParams({
|
||||||
|
type : 'show',
|
||||||
|
name : libraryName,
|
||||||
|
agent : agentIdentifier,
|
||||||
|
scanner : 'Plex TV Series',
|
||||||
|
language: 'fr-FR',
|
||||||
|
location: libraryPath,
|
||||||
|
})
|
||||||
if (groupId) params.set('metadataAgentProviderGroupId', groupId)
|
if (groupId) params.set('metadataAgentProviderGroupId', groupId)
|
||||||
const libRes = await fetch(`${serverUrl}/library/sections?${params}`, { method: 'POST', headers })
|
const libRes = await serverFetch(`${serverUrl}/library/sections?${params}`, { method: 'POST', headers })
|
||||||
if (!libRes.ok) throw new Error(`HTTP ${libRes.status}: ${await libRes.text()}`)
|
if (!libRes.ok) throw new Error(`HTTP ${libRes.status}: ${await libRes.text()}`)
|
||||||
steps.push({ step: 'library', ok: true, message: `Bibliothèque "${libraryName}" créée` })
|
steps.push({ step: 'library', ok: true, message: `Bibliothèque "${libraryName}" créée` })
|
||||||
logger.info('plex', `Bibliothèque "${libraryName}" créée sur ${serverUrl}`)
|
logger.info('plex', `Bibliothèque "${libraryName}" créée sur ${serverUrl}`)
|
||||||
@@ -166,4 +195,4 @@ router.post('/plex/setup', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
+27
-2
@@ -47,11 +47,36 @@ router.get('/version', (_req, res) => {
|
|||||||
|
|
||||||
// ── Parcours fichiers ──────────────────────────────────────────
|
// ── Parcours fichiers ──────────────────────────────────────────
|
||||||
router.get('/browse', requireAuth, (req, res) => {
|
router.get('/browse', requireAuth, (req, res) => {
|
||||||
const dirPath = (req.query.path as string) || '/'
|
const isWindows = process.platform === 'win32'
|
||||||
|
const dirPath = (req.query.path as string) || '/'
|
||||||
|
|
||||||
|
// ── Racine virtuelle Windows : liste les lecteurs disponibles ──
|
||||||
|
if (isWindows && dirPath === '/') {
|
||||||
|
const drives: string[] = []
|
||||||
|
for (const letter of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') {
|
||||||
|
const d = `${letter}:\\`
|
||||||
|
try { fs.readdirSync(d); drives.push(d) } catch {}
|
||||||
|
}
|
||||||
|
res.json({ path: '/', parent: null, dirs: drives, drivesRoot: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(n => !n.startsWith('.')).sort((a, b) => a.localeCompare(b))
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(n => !n.startsWith('.')).sort((a, b) => a.localeCompare(b))
|
||||||
res.json({ path: dirPath, parent: dirPath === '/' ? null : path.dirname(dirPath), dirs })
|
|
||||||
|
// Sur Windows, remonter à '/' quand on est à la racine d'un lecteur (ex: C:\)
|
||||||
|
let parent: string | null
|
||||||
|
if (dirPath === '/') {
|
||||||
|
parent = null
|
||||||
|
} else if (isWindows && /^[A-Za-z]:\\?$/.test(dirPath)) {
|
||||||
|
parent = '/'
|
||||||
|
} else {
|
||||||
|
const up = path.dirname(dirPath)
|
||||||
|
parent = up === dirPath ? null : up // dirname de C:\ retourne C:\ → null
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ path: dirPath, parent, dirs })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('api', `Lecture dossier "${dirPath}" impossible : ${err instanceof Error ? err.message : err}`)
|
logger.warn('api', `Lecture dossier "${dirPath}" impossible : ${err instanceof Error ? err.message : err}`)
|
||||||
res.status(400).json({ error: err instanceof Error ? err.message : 'Erreur lecture dossier' })
|
res.status(400).json({ error: err instanceof Error ? err.message : 'Erreur lecture dossier' })
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<!-- Chemin éditable -->
|
<!-- Chemin éditable -->
|
||||||
<div class="px-5 py-3 border-b border-border shrink-0 flex gap-2">
|
<div class="px-5 py-3 border-b border-border shrink-0 flex gap-2">
|
||||||
<button @click="navigateTo('/')" class="btn-secondary px-2 text-muted hover:text-primary" title="Retour à la racine">
|
<button @click="navigateTo('/')" class="btn-secondary px-2 text-muted hover:text-primary" :title="drivesRoot ? 'Lecteurs' : 'Retour à la racine'">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none">
|
<svg width="14" height="14" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||||
@@ -85,12 +85,13 @@ import { ref, watch } from 'vue'
|
|||||||
const props = defineProps<{ initialPath?: string }>()
|
const props = defineProps<{ initialPath?: string }>()
|
||||||
const emit = defineEmits<{ select: [path: string]; cancel: [] }>()
|
const emit = defineEmits<{ select: [path: string]; cancel: [] }>()
|
||||||
|
|
||||||
const current = ref(props.initialPath || '/')
|
const current = ref(props.initialPath || '/')
|
||||||
const inputPath = ref(current.value)
|
const inputPath = ref(current.value)
|
||||||
const dirs = ref<string[]>([])
|
const dirs = ref<string[]>([])
|
||||||
const parent = ref<string | null>(null)
|
const parent = ref<string | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const drivesRoot = ref(false) // true quand la liste affiche les lecteurs Windows
|
||||||
|
|
||||||
// FIX : la regex /\/+$/ dans un @click inline fait planter le parser Vue
|
// FIX : la regex /\/+$/ dans un @click inline fait planter le parser Vue
|
||||||
function selectCurrent() {
|
function selectCurrent() {
|
||||||
@@ -98,8 +99,10 @@ function selectCurrent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function joinPath(base: string, name: string): string {
|
function joinPath(base: string, name: string): string {
|
||||||
|
// Lecteur Windows complet (ex: "C:\") → chemin absolu, pas de jointure
|
||||||
|
if (/^[A-Za-z]:\\$/.test(name)) return name
|
||||||
if (base === '/') return `/${name}`
|
if (base === '/') return `/${name}`
|
||||||
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
|
const cleanBase = base.replace(/[\\/]+$/, '')
|
||||||
return `${cleanBase}/${name}`
|
return `${cleanBase}/${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,10 +117,11 @@ async function navigateTo(p: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
current.value = data.path
|
current.value = data.path
|
||||||
inputPath.value = data.path
|
inputPath.value = data.path
|
||||||
parent.value = data.parent
|
parent.value = data.parent
|
||||||
dirs.value = data.dirs
|
dirs.value = data.dirs
|
||||||
|
drivesRoot.value = data.drivesRoot ?? false
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'Impossible de lire ce dossier'
|
error.value = 'Impossible de lire ce dossier'
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.11.0
|
3.11.5
|
||||||
Reference in new issue
Block a user