Bonjour,
J'utilise Nextcloud pour mon activité professionnelle, via un navigateur. Je suis assez embêté par l'absence d'une fonction qui me paraît essentielle lors d'une recherche de fichier : ouvrir le dossier. Simplement.
L'objectif est d'ouvrir le dossier du résultat de la recherche, au lieu d'ouvrir le fichier lui-même. Cela m'est nécessaire pour trois cas d'usages :
1. Je dois consulter / copier / manipuler plusieurs fichiers qui sont dans le même dossier
2. Je dois ajouter un fichier / dossier dans le dossier contenant le fichier, mais je ne sais pas où est dans l'arborescence
3. Je ne me souviens plus du nom du fichier que je cherche, mais je me souviens d'un autre fichier qui est dans le même dossier.
Comme je n'ai rien trouvé de pertinent sur le web, j'ai demandé à Claude :
J'utilise nextcloud à partir d'un navigateur web. Lorsque je cherche un fichier, je souhaite pouvoir visualiser le dossier dans lequel il est stocké. Sur le même modèle que "Ouvrir l'emplacement de l'élément" que j'ai avec nautilus.
Voici sa réponse :
Malheureusement, la recherche native de Nextcloud (via la barre de recherche en haut) n'offre pas de bouton "ouvrir le dossier parent" dans les résultats. Voici les options selon ton niveau de confort :
Option 1 — Extension navigateur : Nextcloud Cospend / scripts utilisateur (Greasemonkey/Tampermonkey)
Tu peux installer un userscript via Tampermonkey qui ajoute un lien "Ouvrir le dossier" sur chaque résultat de recherche. C'est la solution la plus proche de "Ouvrir l'emplacement" de Nautilus.
Cherche sur Greasy Fork : Nextcloud Files open folder.Option 2 — Utiliser l'application Files + le chemin dans l'URL
Quand un résultat de recherche apparaît, tu peux cliquer droit → Copier le lien sur le fichier, puis modifier l'URL manuellement :URL d'un fichier : …/apps/files/?dir=/MonDossier/SousDossier&scrollto=monfichier.pdf
Le paramètre dir= contient exactement le chemin du dossier parent — il suffit de le garder et de supprimer &scrollto=…C'est manuel mais ça fonctionne immédiatement sans rien installer.
Je tiens à préciser que ne suis pas développeur et que je n'ai absolument aucune connaissance dans ce domaine. La réponse de Claude m'a donc surpris, car je ne suis pas du tout le public cible à ce type de proposition. De plus, je n'ai jamais demandé d'aide à Claude à ce sujet, ce qui aurait pu favoriser une telle réponse. Néanmoins, j'ai installé un des gestionnaires de script dans Firefox, copier/coller le résultat, suivi les instructions présentées et… recommencé quelques fois, cinq exactement (coucou la console du navigateur F12 !)
Maintenant cela fonctionne et je suis tout content. Je vais pouvoir passer moins de temps dans mes recherches sur Nextcloud.
L'objectif de ce journal n'est pas tant de publier le résultat que tout le monde peut reproduire facilement, mais pour faire part de possibilités que je n'envisageai pas du tout jusqu'à présent. Cela ouvre de nouvelles perspectives.
Je publie le résultat ici, puisque cela pourra éventuellement aider d'autres personnes.
// ==UserScript==
// @name Nextcloud – Ouvrir le dossier parent
// @version 1.0.5
// @description Ajoute un bouton "Ouvrir le dossier" sur chaque résultat de recherche Nextcloud 32
// @author Claude / Anthropic
// @match *://*/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
function isNextcloud() {
return !!(
document.querySelector('#unified-search') ||
document.querySelector('meta[name="apple-itunes-app"][content*="1125420102"]')
);
}
if (!isNextcloud()) return;
const origin = window.location.origin;
// -----------------------------------------------------------------------
// Styles
// -----------------------------------------------------------------------
const style = document.createElement('style');
style.textContent = `
.nc-folder-btn {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 8px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-family: inherit;
color: var(--color-primary-element, #0082c9);
background: var(--color-primary-element-light, rgba(0,130,201,0.1));
border: 1px solid var(--color-primary-element, #0082c9);
cursor: pointer;
white-space: nowrap;
vertical-align: middle;
opacity: 0.9;
transition: opacity 0.15s;
}
.nc-folder-btn:hover { opacity: 1; }
.nc-folder-btn svg { flex-shrink: 0; }
.nc-folder-btn--loading { opacity: 0.5; cursor: wait; }
`;
document.head.appendChild(style);
const FOLDER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function getRequestToken() {
return document.head.dataset.requesttoken || '';
}
function getUser() {
return document.head.dataset.user || '';
}
// Encode chaque segment du chemin séparément pour préserver les "/"
function encodePath(path) {
return path.split('/').map(segment => encodeURIComponent(segment)).join('/');
}
// -----------------------------------------------------------------------
// Cache et résolution du chemin via WebDAV SEARCH
// -----------------------------------------------------------------------
const pathCache = {};
async function getFilePath(fileId) {
if (pathCache[fileId] !== undefined) return pathCache[fileId];
const user = getUser();
const body = `<?xml version="1.0"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select><d:prop><d:displayname/><oc:fileid/></d:prop></d:select>
<d:from><d:scope><d:href>/files/${user}</d:href><d:depth>infinity</d:depth></d:scope></d:from>
<d:where><d:eq><d:prop><oc:fileid/></d:prop><d:literal>${fileId}</d:literal></d:eq></d:where>
</d:basicsearch>
</d:searchrequest>`;
try {
const resp = await fetch(`${origin}/remote.php/dav`, {
method: 'SEARCH',
headers: {
'Content-Type': 'application/xml',
'Depth': '0',
'requesttoken': getRequestToken(),
},
body,
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const xml = await resp.text();
const doc = new DOMParser().parseFromString(xml, 'application/xml');
const href = doc.querySelector('response > href')?.textContent?.trim();
if (!href) throw new Error('href vide');
// href est retourné encodé par le serveur
// On décode complètement, puis on re-encodera segment par segment au moment de construire l'URL
const prefix = `/remote.php/dav/files/${user}/`;
// Décoder le href entier pour obtenir le chemin lisible
let decoded;
try {
decoded = decodeURIComponent(href);
} catch(e) {
decoded = href; // fallback si déjà décodé ou mal encodé
}
const decodedPrefix = `/remote.php/dav/files/${user}/`;
const relativePath = decoded.startsWith(decodedPrefix)
? decoded.slice(decodedPrefix.length)
: decoded;
// Déterminer le dossier parent (tout sauf le dernier segment)
const lastSlash = relativePath.lastIndexOf('/');
const dir = lastSlash > 0 ? '/' + relativePath.substring(0, lastSlash) : '/';
pathCache[fileId] = dir;
return dir;
} catch (e) {
console.warn('[NC-folder] SEARCH échoué :', e);
pathCache[fileId] = null;
return null;
}
}
// -----------------------------------------------------------------------
// Construire l'URL du dossier avec encodage correct segment par segment
// -----------------------------------------------------------------------
function buildFolderUrl(dir) {
// dir est un chemin décodé
// On encode chaque segment séparément
const encodedDir = encodePath(dir);
return `${origin}/apps/files/?dir=${encodedDir}`;
}
// -----------------------------------------------------------------------
// Traiter une ligne de résultat
// -----------------------------------------------------------------------
function processRow(row) {
if (row.dataset.ncFolderDone) return;
row.dataset.ncFolderDone = '1';
const fileId = row.dataset.cyFilesListRowFileid;
if (!fileId) return;
const nameCell = row.querySelector('[data-cy-files-list-row-name]');
if (!nameCell) return;
const btn = document.createElement('button');
btn.className = 'nc-folder-btn nc-folder-btn--loading';
btn.innerHTML = FOLDER_ICON + ' <span>…</span>';
btn.title = 'Chargement du chemin…';
btn.type = 'button';
nameCell.appendChild(btn);
getFilePath(fileId).then(dir => {
if (!dir) { btn.remove(); return; }
btn.classList.remove('nc-folder-btn--loading');
btn.innerHTML = FOLDER_ICON + ' <span>Ouvrir le dossier</span>';
btn.title = `Aller dans : ${dir}`;
btn.addEventListener('click', e => {
e.stopPropagation();
window.location.href = buildFolderUrl(dir);
});
});
}
// -----------------------------------------------------------------------
// Détecter si une recherche est active
// -----------------------------------------------------------------------
function isSearchActive() {
const input = document.querySelector('input[type="search"]');
return input && input.value.trim().length > 0;
}
// -----------------------------------------------------------------------
// Scanner les lignes
// -----------------------------------------------------------------------
function scan() {
if (!isSearchActive()) return;
document.querySelectorAll(
'tr[data-cy-files-list-row-fileid]:not([data-nc-folder-done])'
).forEach(processRow);
}
// -----------------------------------------------------------------------
// Observer le DOM
// -----------------------------------------------------------------------
let debounce;
const observer = new MutationObserver(() => {
clearTimeout(debounce);
debounce = setTimeout(scan, 200);
});
observer.observe(document.body, { childList: true, subtree: true });
scan();
})();
# Ce journal est inutile
Posté par Laurent Pointecouteau (site web personnel, Mastodon) . Évalué à 2 (+3/-3).
J'ai cliqué sur "Ce journal est inutile", car il se résume à un copié-collé d'une réponse Claude. Je suis d'avis que de tels journaux ne méritent pas d'être rédigés.
[^] # Re: Ce journal est inutile
Posté par Axone . Évalué à 5 (+4/-1).
J'ai trouvé au contraire ce partage d'expérience de résolution d'une problématique intéressant.
Envoyer un commentaire
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.