diff --git a/css/components/search.scss b/css/components/search.scss index 06af9946..4e0c8384 100644 --- a/css/components/search.scss +++ b/css/components/search.scss @@ -262,6 +262,9 @@ } &-item__icon { + position: relative; + overflow: visible; + &.icon-folder, &--no-preview { width: 42px; @@ -271,6 +274,19 @@ background-repeat: no-repeat; background-position: center; } + + .nmc-search-favorite { + position: absolute; + top: 0; + right: -6px; + width: 18px; + height: 18px; + background-image: var(--icon-starred-yellow); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + display: block; + } } &-footer { diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php index 2182ffc6..3452d369 100644 --- a/lib/Listener/BeforeTemplateRenderedListener.php +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -97,6 +97,7 @@ public function handle(Event $event): void { \OCP\Util::addScript('nmctheme', 'nmctheme-nmcfiles', "core"); \OCP\Util::addScript("nmctheme", "nmctheme-mimetypes", "core"); \OCP\Util::addScript("nmctheme", "nmctheme-skipactions", "core"); + \OCP\Util::addScript("nmctheme", "nmctheme-searchfavorites", "core"); \OCP\Util::addScript("nmctheme", "nmctheme-filessettings", "files"); \OCP\Util::addScript("nmctheme", "nmctheme-filelistplugin", "files"); } diff --git a/src/js/searchfavorites.js b/src/js/searchfavorites.js new file mode 100644 index 00000000..7cf704df --- /dev/null +++ b/src/js/searchfavorites.js @@ -0,0 +1,111 @@ +/** + * @copyright Copyright (c) 2024 T-Systems International + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Shows a favorite star indicator on favorited files/folders in unified search results. + */ + +const LOG = '[nmc-search-favorites]' + +let favoriteIdsPromise = null + +function getRequestToken() { + return document.head.dataset.requesttoken + ?? window.OC?.requestToken + ?? '' +} + +function getUserId() { + return window.OC?.currentUser ?? '' +} + +/** + * Fetches all favorited file IDs for the current user in a single REPORT request. + * Result is cached for the page lifetime. + */ +async function loadFavoriteIds() { + if (favoriteIdsPromise) return favoriteIdsPromise + + favoriteIdsPromise = (async () => { + const userId = getUserId() + if (!userId) return new Set() + + const url = `/remote.php/dav/files/${encodeURIComponent(userId)}/` + + try { + const response = await fetch(url, { + method: 'REPORT', + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + requesttoken: getRequestToken(), + }, + body: '' + + '' + + '' + + '1' + + '', + credentials: 'same-origin', + }) + + if (!response.ok) { + console.warn(LOG, `REPORT failed: HTTP ${response.status}`) + return new Set() + } + + const text = await response.text() + const doc = new DOMParser().parseFromString(text, 'application/xml') + const ids = new Set() + + Array.from(doc.getElementsByTagNameNS('http://owncloud.org/ns', 'fileid')) + .forEach(el => ids.add(el.textContent.trim())) + + return ids + } catch (err) { + console.error(LOG, 'Error loading favorites:', err) + return new Set() + } + })() + + return favoriteIdsPromise +} + +function addFavoriteMarker(item) { + const iconEl = item.querySelector('.result-item__icon') + if (!iconEl || iconEl.querySelector('.nmc-search-favorite')) return + + const marker = document.createElement('span') + marker.className = 'nmc-search-favorite' + marker.setAttribute('aria-hidden', 'true') + iconEl.appendChild(marker) +} + +async function processResultItem(item, favoriteIds) { + item.dataset.nmcFavChecked = '1' + + const link = item.querySelector('a[href*="/index.php/f/"]') + if (!link) return + + const match = link.href.match(/\/index\.php\/f\/(\d+)/) + if (!match) return + + const fileId = match[1] + if (favoriteIds.has(fileId)) { + addFavoriteMarker(item) + } +} + +async function processUnprocessedItems() { + const unprocessed = [ + ...document.querySelectorAll('.result-item:not([data-nmc-fav-checked])'), + ] + if (unprocessed.length === 0) return + + const favoriteIds = await loadFavoriteIds() + unprocessed.forEach(item => processResultItem(item, favoriteIds)) +} + +window.addEventListener('DOMContentLoaded', () => { + const observer = new MutationObserver(processUnprocessedItems) + observer.observe(document.body, { childList: true, subtree: true }) +}) diff --git a/webpack.config.cjs b/webpack.config.cjs index 480d04ed..068fb643 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -9,6 +9,7 @@ webpackConfig.entry = { filessettings: path.join(__dirname, 'src', 'js', 'filessettings.js'), filelistplugin: path.join(__dirname, 'src', 'js', 'filelistplugin.js'), skipactions: path.join(__dirname, 'src', 'js', 'skipactions.js'), + searchfavorites: path.join(__dirname, 'src', 'js', 'searchfavorites.js'), conflictdialog: path.join(__dirname, 'src', 'js', 'conflictdialog.js'), mimetypes: path.join(__dirname, 'src', 'js', 'mimetypes.js'), nmcfooter: path.join(__dirname, 'src', 'nmcfooter.ts'),