// Configuration globale let config = { baseUrl: "", currentPath: "", }; // Initialisation de l'application document.addEventListener("DOMContentLoaded", function () { initializeApp(); setupEventListeners(); }); function initializeApp() { // Tentative de configuration automatique depuis l'URL courante autoConfigureFromURL(); // Affichage du formulaire de développement showDevForm(); } function setupEventListeners() { // Formulaire de développement const urlForm = document.getElementById("url-form"); if (urlForm) { urlForm.addEventListener("submit", handleUrlFormSubmit); } // Gestion de la navigation arrière/avant du navigateur window.addEventListener("popstate", handlePopState); // Gestionnaires pour la prévisualisation const closePreview = document.getElementById("close-preview"); if (closePreview) { closePreview.addEventListener("click", hidePreview); } const downloadFile = document.getElementById("download-file"); if (downloadFile) { downloadFile.addEventListener("click", downloadCurrentFile); } } function handlePopState(event) { console.log("Navigation arrière/avant détectée:", event.state); if (event.state && event.state.baseUrl) { // Restaurer la configuration config.baseUrl = event.state.baseUrl; config.currentPath = event.state.path || ""; updateConfigDisplay(); // Charger le contenu sans ajouter à l'historique (pour éviter les boucles) loadBucketContents(config.currentPath, false); } } function autoConfigureFromURL() { const currentUrl = window.location.href; const url = new URL(currentUrl); // Configuration simplifiée : on prend juste le base URL sans le bucket config.baseUrl = `${url.protocol}//${url.host}${url.pathname.replace("/index.html", "")}`; // Vérifier s'il y a un fragment d'URL pour définir le chemin initial if (url.hash) { const hashPath = url.hash.replace("#/", ""); config.currentPath = hashPath; } updateConfigDisplay(); console.log("Configuration automatique:", config); // Charger automatiquement le contenu si on a une config valide if (config.baseUrl) { loadBucketContents(config.currentPath, false); } } function handleUrlFormSubmit(event) { event.preventDefault(); const urlInput = document.getElementById("minio-url"); if (!urlInput) return; const minioUrl = urlInput.value.trim(); if (!minioUrl) { showError("Veuillez saisir une URL MinIO"); return; } parseMinioURL(minioUrl); } function parseMinioURL(url) { try { const urlObj = new URL(url); // Configuration simplifiée : on prend l'URL complète comme base config.baseUrl = url.replace(/\/$/, ""); // Supprimer le slash final si présent config.currentPath = ""; updateConfigDisplay(); // Ajouter l'état initial à l'historique const initialState = { path: "", baseUrl: config.baseUrl }; history.replaceState(initialState, "", window.location.href); loadBucketContents(); } catch (error) { showError("URL invalide: " + error.message); console.error("Erreur de parsing URL:", error); } } function updateConfigDisplay() { const configInfo = document.getElementById("config-info"); if (configInfo) { configInfo.innerHTML = ` Configuration actuelle:
URL de base: ${config.baseUrl}
Chemin actuel: /${config.currentPath} `; } } function showDevForm() { const devForm = document.getElementById("dev-form"); if (devForm) { devForm.style.display = "block"; } } function showError(message) { const errorSection = document.getElementById("error-section"); const errorMessage = document.getElementById("error-message"); if (errorMessage) { errorMessage.textContent = message; } if (errorSection) { errorSection.style.display = "block"; // Masquer l'erreur après 5 secondes setTimeout(() => { errorSection.style.display = "none"; }, 5000); } } function showLoading(show = true) { const loading = document.getElementById("loading"); const table = document.getElementById("files-table"); if (loading) { loading.style.display = show ? "block" : "none"; } if (table) { table.style.display = show ? "none" : "table"; } } function updateCurrentPathDisplay() { const pathDisplay = document.getElementById("current-path"); if (!pathDisplay) return; // Créer la structure de navigation breadcrumb let breadcrumbHTML = ''; // Toujours commencer par la racine if (config.currentPath) { // Si on n'est pas à la racine, rendre "/" cliquable breadcrumbHTML += '/ '; } else { // Si on est à la racine, afficher "/" sans lien breadcrumbHTML += '/'; } // Si on a un chemin, découper et créer les segments if (config.currentPath) { const segments = config.currentPath.split('/').filter(segment => segment.length > 0); for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const segmentPath = segments.slice(0, i + 1).join('/'); // Le dernier segment (actuel) n'est pas cliquable if (i === segments.length - 1) { breadcrumbHTML += segment; } else { // Les segments précédents sont cliquables breadcrumbHTML += `${segment} / `; } } } pathDisplay.innerHTML = breadcrumbHTML; // Ajouter les event listeners aux liens de navigation const pathLinks = pathDisplay.querySelectorAll('.path-segment'); pathLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetPath = link.getAttribute('data-path'); loadBucketContents(targetPath, true); }); }); } async function loadBucketContents(path = "", pushToHistory = true) { console.log( "Debug loadBucketContents - path received:", JSON.stringify(path), ); if (!config.baseUrl) { showError("Configuration URL manquante"); return; } showLoading(true); config.currentPath = path; updateCurrentPathDisplay(); // Ajouter à l'historique du navigateur si demandé if (pushToHistory) { const state = { path: path, baseUrl: config.baseUrl }; const url = path ? `#/${path}` : "#/"; history.pushState(state, "", url); } try { // Construction de l'URL API avec delimiter let apiUrl = `${config.baseUrl}?list-type=2&delimiter=/`; // Construire le prefix pour l'API MinIO if (path) { // Pour un sous-dossier const cleanPath = path.startsWith("/") ? path.substring(1) : path; const fullPrefix = cleanPath + "/"; apiUrl += `&prefix=${encodeURIComponent(fullPrefix)}`; } console.log("Appel API MinIO:", apiUrl); const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`Erreur HTTP: ${response.status}`); } const xmlText = await response.text(); console.log("Réponse XML reçue:", xmlText); const contents = parseMinioXML(xmlText); displayContents(contents); } catch (error) { console.error("Erreur lors du chargement:", error); showError("Erreur de chargement: " + error.message); } finally { showLoading(false); } } function parseMinioXML(xmlText) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const contents = { folders: [], files: [], }; // Déterminer le préfixe attendu basé sur la navigation actuelle let expectedPrefix = ""; if (config.currentPath) { expectedPrefix = config.currentPath + "/"; } // Parse CommonPrefixes (dossiers) const prefixes = xmlDoc.getElementsByTagName("CommonPrefixes"); for (let prefix of prefixes) { const prefixElement = prefix.getElementsByTagName("Prefix")[0]; if (prefixElement) { const fullPrefix = prefixElement.textContent; // Extraire le nom du dossier (dernière partie avant le /) const folderName = fullPrefix.replace(/\/$/, "").split("/").pop(); if (folderName) { // Construire le chemin pour la navigation let folderPath; if (config.currentPath) { folderPath = config.currentPath + "/" + folderName; } else { folderPath = folderName; } console.log( "Debug parsing - fullPrefix:", fullPrefix, "folderName:", folderName, "currentPath:", config.currentPath, "folderPath:", folderPath, ); contents.folders.push({ name: folderName, path: folderPath, }); } } } // Parse Contents (fichiers) const objects = xmlDoc.getElementsByTagName("Contents"); for (let obj of objects) { const keyElement = obj.getElementsByTagName("Key")[0]; const sizeElement = obj.getElementsByTagName("Size")[0]; const modifiedElement = obj.getElementsByTagName("LastModified")[0]; if (keyElement) { const fullKey = keyElement.textContent; const fileName = fullKey.split("/").pop(); // Vérifier que le fichier correspond au niveau de navigation actuel let shouldInclude = false; if (config.currentPath) { // Dans un sous-dossier : vérifier que la clé commence par le bon préfixe const currentPrefix = config.currentPath + "/"; if (fullKey.startsWith(currentPrefix)) { // S'assurer qu'on n'affiche que les fichiers directs (pas dans des sous-sous-dossiers) const relativePath = fullKey.replace(currentPrefix, ""); if (!relativePath.includes("/")) { shouldInclude = true; } } } else { // À la racine : afficher tous les fichiers qui ne sont pas dans des dossiers if (!fullKey.includes("/")) { shouldInclude = true; } } // Exclure les fichiers de l'application if ( shouldInclude && fileName && !["index.html", "style.css", "script.js"].includes(fileName) ) { contents.files.push({ name: fileName, size: sizeElement ? parseInt(sizeElement.textContent) : 0, modified: modifiedElement ? new Date(modifiedElement.textContent) : new Date(), fullKey: fullKey, }); } } } return contents; } function displayContents(contents) { const tbody = document.getElementById("files-tbody"); if (!tbody) return; tbody.innerHTML = ""; // Bouton parent directory si on n'est pas à la racine if (config.currentPath) { const parentPath = config.currentPath.split("/").slice(0, -1).join("/"); const parentRow = createParentRow(parentPath); tbody.appendChild(parentRow); } // Afficher les dossiers contents.folders.forEach((folder) => { const row = createFolderRow(folder); tbody.appendChild(row); }); // Afficher les fichiers contents.files.forEach((file) => { const row = createFileRow(file); tbody.appendChild(row); }); // Afficher le tableau const filesTable = document.getElementById("files-table"); if (filesTable) { filesTable.style.display = "table"; } // Chercher et ouvrir automatiquement index.rst s'il existe autoOpenIndexRst(contents.files); } function createParentRow(parentPath) { const row = document.createElement("tr"); row.className = "clickable-row"; row.innerHTML = ` .. - - - `; // Rendre toute la ligne cliquable row.addEventListener("click", (e) => { // Éviter le double clic si on clique sur le lien if (e.target.tagName !== 'A') { e.preventDefault(); loadBucketContents(parentPath, true); } }); const link = row.querySelector(".folder-link"); link.addEventListener("click", (e) => { e.preventDefault(); loadBucketContents(parentPath, true); }); return row; } function createFolderRow(folder) { const row = document.createElement("tr"); row.className = "clickable-row"; row.innerHTML = ` ${folder.name}/ - - - `; // Rendre toute la ligne cliquable row.addEventListener("click", (e) => { // Éviter le double clic si on clique sur le lien if (e.target.tagName !== 'A') { e.preventDefault(); loadBucketContents(folder.path, true); } }); const link = row.querySelector(".folder-link"); link.addEventListener("click", (e) => { e.preventDefault(); loadBucketContents(folder.path, true); }); return row; } function createFileRow(file) { const row = document.createElement("tr"); row.className = "clickable-row"; const fileUrl = `${config.baseUrl}/${file.fullKey}`; const fileIcon = getFileIcon(file.name); row.innerHTML = ` ${fileIcon} ${file.name} ${formatFileSize(file.size)} ${formatDate(file.modified)} `; // Rendre toute la ligne cliquable row.addEventListener("click", (e) => { // Éviter le clic sur les boutons d'action et les liens if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'A') { e.preventDefault(); showPreview(fileUrl, file.name); } }); const fileLink = row.querySelector(".file-link"); fileLink.addEventListener("click", (e) => { e.preventDefault(); showPreview(fileUrl, file.name); }); const copyBtn = row.querySelector(".copy-btn"); copyBtn.addEventListener("click", (e) => { e.stopPropagation(); // Empêcher la propagation vers le clic sur la ligne copyToClipboard(fileUrl); }); const openBtn = row.querySelector(".open-btn"); openBtn.addEventListener("click", (e) => { e.stopPropagation(); // Empêcher la propagation vers le clic sur la ligne openInNewTab(fileUrl); }); return row; } function formatFileSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } function formatDate(date) { return date.toLocaleString("fr-FR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log("Lien copié:", text); // Feedback visuel temporaire const btn = event.target; const originalText = btn.textContent; btn.textContent = "Copié !"; setTimeout(() => { btn.textContent = originalText; }, 1000); } catch (error) { console.error("Erreur copie clipboard:", error); // Fallback pour les navigateurs plus anciens const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); } } function openInNewTab(fileUrl) { window.open(fileUrl, '_blank', 'noopener,noreferrer'); } // Variables globales pour la prévisualisation let currentPreviewFile = { url: "", name: "", }; function detectFileType(fileName) { const extension = fileName.toLowerCase().split(".").pop(); const fileTypes = { images: ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"], text: ["txt", "json", "xml", "csv", "log"], code: [ "js", "css", "html", "htm", "py", "java", "cpp", "c", "php", "rb", "go", "rs", "ts", "jsx", "tsx", "vue", "svelte", "tex", "latex", ], markdown: ["md", "markdown"], rst: ["rst", "rest"], pdf: ["pdf"], video: ["mp4", "avi", "mov", "wmv", "flv", "webm", "mkv"], audio: ["mp3", "wav", "ogg", "aac", "flac", "m4a"], archive: ["zip", "rar", "7z", "tar", "gz", "bz2"], }; for (const [type, extensions] of Object.entries(fileTypes)) { if (extensions.includes(extension)) { return type; } } return "unknown"; } function getFileIcon(fileName) { const extension = fileName.toLowerCase().split(".").pop(); const fileType = detectFileType(fileName); // Icônes spécifiques par extension const specificIcons = { pdf: "📕", zip: "📦", rar: "📦", "7z": "📦", tar: "📦", gz: "📦", bz2: "📦", mp4: "🎬", avi: "🎬", mov: "🎬", wmv: "🎬", flv: "🎬", webm: "🎬", mkv: "🎬", mp3: "🎵", wav: "🎵", ogg: "🎵", aac: "🎵", flac: "🎵", m4a: "🎵", jpg: "🖼️", jpeg: "🖼️", png: "🖼️", gif: "🖼️", bmp: "🖼️", svg: "🖼️", webp: "🖼️", txt: "📝", log: "📝", json: "⚙️", xml: "⚙️", csv: "📊", js: "🟨", ts: "🔷", jsx: "⚛️", tsx: "⚛️", py: "🐍", java: "☕", php: "🐘", rb: "💎", go: "🐹", rs: "🦀", html: "🌐", htm: "🌐", css: "🎨", vue: "💚", svelte: "🧡", md: "📖", markdown: "📖", rst: "📖", tex: "📄", latex: "📄", c: "⚡", cpp: "⚡", }; // Retourner l'icône spécifique si elle existe if (specificIcons[extension]) { return specificIcons[extension]; } // Sinon, retourner une icône par type const typeIcons = { images: "🖼️", text: "📝", code: "💻", markdown: "📖", rst: "📖", pdf: "📕", video: "🎬", audio: "🎵", archive: "📦", unknown: "📄" }; return typeIcons[fileType] || "📄"; } function showPreview(fileUrl, fileName) { currentPreviewFile = { url: fileUrl, name: fileName }; const previewSection = document.getElementById("preview-section"); const previewTitle = document.getElementById("preview-title"); const previewContent = document.getElementById("preview-content"); if (!previewSection || !previewTitle || !previewContent) { console.error("Éléments de prévisualisation non trouvés"); return; } previewTitle.textContent = `Prévisualisation: ${fileName}`; previewContent.innerHTML = '
Chargement...
'; previewSection.style.display = "flex"; const fileType = detectFileType(fileName); switch (fileType) { case "images": showImagePreview(fileUrl, previewContent); break; case "text": showTextPreview(fileUrl, previewContent); break; case "code": showCodePreview(fileUrl, fileName, previewContent); break; case "markdown": showMarkdownPreview(fileUrl, previewContent); break; case "rst": showRstPreview(fileUrl, previewContent); break; case "pdf": showPdfPreview(fileUrl, previewContent); break; case "video": showVideoPreview(fileUrl, previewContent); break; case "audio": showAudioPreview(fileUrl, previewContent); break; default: showUnsupportedPreview(fileName, previewContent); } } function showImagePreview(fileUrl, container) { const img = document.createElement("img"); img.src = fileUrl; img.alt = "Prévisualisation image"; img.onload = () => { container.innerHTML = ""; container.appendChild(img); }; img.onerror = () => { container.innerHTML = '
Erreur: Impossible de charger l\'image
'; }; } function showTextPreview(fileUrl, container) { fetch(fileUrl) .then((response) => { if (!response.ok) { throw new Error(`Erreur HTTP: ${response.status}`); } return response.text(); }) .then((text) => { const pre = document.createElement("pre"); pre.textContent = text; container.innerHTML = ""; container.appendChild(pre); }) .catch((error) => { console.error("Erreur chargement texte:", error); container.innerHTML = '
Erreur: Impossible de charger le fichier texte
'; }); } function detectLanguageFromExtension(fileName) { const extension = fileName.toLowerCase().split(".").pop(); const languageMap = { js: "javascript", jsx: "jsx", ts: "typescript", tsx: "tsx", py: "python", java: "java", c: "c", cpp: "cpp", cxx: "cpp", cc: "cpp", php: "php", rb: "ruby", go: "go", rs: "rust", html: "html", htm: "html", css: "css", scss: "scss", sass: "sass", less: "less", json: "json", xml: "xml", vue: "vue", svelte: "svelte", sh: "bash", bash: "bash", zsh: "bash", fish: "bash", ps1: "powershell", sql: "sql", r: "r", matlab: "matlab", m: "matlab", swift: "swift", kt: "kotlin", scala: "scala", clj: "clojure", hs: "haskell", lua: "lua", pl: "perl", tex: "latex", latex: "latex", md: "markdown", markdown: "markdown", yml: "yaml", yaml: "yaml", toml: "toml", ini: "ini", conf: "bash", config: "bash", }; return languageMap[extension] || "text"; } function showCodePreview(fileUrl, fileName, container) { fetch(fileUrl) .then((response) => { if (!response.ok) { throw new Error(`Erreur HTTP: ${response.status}`); } return response.text(); }) .then((text) => { const language = detectLanguageFromExtension(fileName); const pre = document.createElement("pre"); const code = document.createElement("code"); if (language !== "text") { code.className = `language-${language}`; } code.textContent = text; pre.appendChild(code); container.innerHTML = ""; container.appendChild(pre); // Appliquer la coloration syntaxique si Prism est disponible if (window.Prism && language !== "text") { try { Prism.highlightElement(code); } catch (error) { console.warn("Erreur coloration syntaxique:", error); } } }) .catch((error) => { console.error("Erreur chargement code:", error); container.innerHTML = '
Erreur: Impossible de charger le fichier
'; }); } function showPdfPreview(fileUrl, container) { const iframe = document.createElement("iframe"); iframe.src = fileUrl; iframe.style.width = "100%"; iframe.style.height = "100%"; container.innerHTML = ""; container.appendChild(iframe); } function showVideoPreview(fileUrl, container) { const video = document.createElement("video"); video.src = fileUrl; video.controls = true; video.style.maxWidth = "100%"; container.innerHTML = ""; container.appendChild(video); } function showAudioPreview(fileUrl, container) { const audio = document.createElement("audio"); audio.src = fileUrl; audio.controls = true; audio.style.width = "100%"; container.innerHTML = ""; container.appendChild(audio); } function showUnsupportedPreview(fileName, container) { const extension = fileName.toLowerCase().split(".").pop(); container.innerHTML = `

Prévisualisation non disponible pour ce type de fichier (.${extension})

Utilisez le bouton "Télécharger" ci-dessous pour obtenir le fichier.

`; } function hidePreview() { const previewSection = document.getElementById("preview-section"); if (previewSection) { previewSection.style.display = "none"; } currentPreviewFile = { url: "", name: "" }; } function downloadCurrentFile() { if (currentPreviewFile.url) { const link = document.createElement("a"); link.href = currentPreviewFile.url; link.download = currentPreviewFile.name; link.target = "_blank"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } // Fonction pour ouvrir automatiquement index.rst s'il existe function autoOpenIndexRst(files) { // Chercher un fichier index.rst dans la liste const indexRstFile = files.find( (file) => file.name.toLowerCase() === "index.rst", ); if (indexRstFile) { // Construire l'URL complète du fichier const fileUrl = `${config.baseUrl}/${indexRstFile.fullKey}`; // Ouvrir automatiquement la prévisualisation console.log("Auto-ouverture de index.rst trouvé:", indexRstFile.name); showPreview(fileUrl, indexRstFile.name); } else { // Pas d'index.rst trouvé, fermer le preview s'il est ouvert console.log("Aucun index.rst trouvé, fermeture du preview"); hidePreview(); } } // Fonction simple de parsing Markdown vers HTML function parseMarkdown(text) { let html = text; // Headers html = html.replace(/^### (.*$)/gim, "

$1

"); html = html.replace(/^## (.*$)/gim, "

$1

"); html = html.replace(/^# (.*$)/gim, "

$1

"); // Bold html = html.replace(/\*\*(.*?)\*\*/g, "$1"); html = html.replace(/__(.*?)__/g, "$1"); // Italic html = html.replace(/\*(.*?)\*/g, "$1"); html = html.replace(/_(.*?)_/g, "$1"); // Code inline html = html.replace(/`(.*?)`/g, "$1"); // Code blocks html = html.replace(/```([\s\S]*?)```/g, "
$1
"); // Links html = html.replace( /\[([^\]]+)\]\(([^)]+)\)/g, '$1', ); // Images html = html.replace( /!\[([^\]]*)\]\(([^)]+)\)/g, '$1', ); // Lists html = html.replace(/^\* (.*$)/gim, "
  • $1
  • "); html = html.replace(/^- (.*$)/gim, "
  • $1
  • "); html = html.replace(/^\d+\. (.*$)/gim, "
  • $1
  • "); // Wrap consecutive
  • items in