From e007e90588d900ca2cb098ee97eb5514598c640e Mon Sep 17 00:00:00 2001 From: jolavillette Date: Tue, 17 Feb 2026 20:39:04 +0100 Subject: [PATCH] fix search events, fonts and login; improve Files labels and search results --- webui-src/app/files/files_proxy.js | 11 +- webui-src/app/files/files_search.js | 111 +++++++----- webui-src/app/files/my_files.js | 109 ++++++++---- webui-src/app/main.js | 15 +- webui-src/app/rswebui.js | 128 +++++++++----- webui-src/styles.css | 264 ++++++++++++++++++++++++++-- 6 files changed, 491 insertions(+), 147 deletions(-) diff --git a/webui-src/app/files/files_proxy.js b/webui-src/app/files/files_proxy.js index 74ceeb9..a7dbca9 100644 --- a/webui-src/app/files/files_proxy.js +++ b/webui-src/app/files/files_proxy.js @@ -8,14 +8,21 @@ const fileProxyObj = futil.createProxy({}, () => { rs.events[rs.RsEventsType.FILE_TRANSFER] = { handler: (event) => { - console.log('search results : ', event); + console.warn('[RS-DEBUG] FILE_TRANSFER event received:', event); // if request item doesn't already exists in Object then create new item if (!Object.prototype.hasOwnProperty.call(fileProxyObj, event.mRequestId)) { fileProxyObj[event.mRequestId] = []; } - fileProxyObj[event.mRequestId].push(...event.mResults); + event.mResults.forEach((newRes) => { + const isAlt = fileProxyObj[event.mRequestId].some( + (oldRes) => oldRes.fHash === newRes.fHash && oldRes.fName === newRes.fName + ); + if (!isAlt) { + fileProxyObj[event.mRequestId].push(newRes); + } + }); }, }; diff --git a/webui-src/app/files/files_search.js b/webui-src/app/files/files_search.js index 0a3f9f0..c17355d 100644 --- a/webui-src/app/files/files_search.js +++ b/webui-src/app/files/files_search.js @@ -11,11 +11,12 @@ const reqObj = {}; function handleSubmit() { rs.rsJsonApiRequest('/rsFiles/turtleSearch', { matchString }) .then((res) => { + console.warn('[RS-DEBUG] turtleSearch response:', res.body); // Add prefix to obj keys so that javascript doesn't sort them reqObj['_' + res.body.retval] = matchString; currentItem = '_' + res.body.retval; }) - .catch((error) => console.log(error)); + .catch((error) => console.error('[RS-DEBUG] turtleSearch error:', error)); } const SearchBar = () => { @@ -63,58 +64,72 @@ const Layout = () => { m('.widget__body', [ m('div.file-search-container', [ m('div.file-search-container__keywords', [ - m('h5.bold', 'Keywords'), - Object.keys(reqObj).length !== 0 && + m('.keywords-header', [ + m('h5.bold', 'Keywords'), m( - 'div.keywords-container', - Object.keys(reqObj) - .reverse() - .map((item, index) => { - return m( - m.route.Link, - { - class: active === index ? 'selected' : '', - onclick: () => { - active = index; - currentItem = item; - }, - href: `/files/search/${item}`, - }, - reqObj[item] - ); - }) + 'button.red.clear-btn', + { + onclick: () => { + Object.keys(reqObj).forEach((key) => delete reqObj[key]); + Object.keys(fproxy.fileProxyObj).forEach((key) => delete fproxy.fileProxyObj[key]); + currentItem = 0; + active = 0; + }, + }, + 'Clear' ), + ]), + Object.keys(reqObj).length !== 0 && + m( + 'div.keywords-container', + Object.keys(reqObj) + .reverse() + .map((item, index) => { + return m( + m.route.Link, + { + class: active === index ? 'selected' : '', + onclick: () => { + active = index; + currentItem = item; + }, + href: `/files/search/${item}`, + }, + reqObj[item] + ); + }) + ), ]), m('div.file-search-container__results', [ - Object.keys(fproxy.fileProxyObj).length === 0 + Object.keys(fproxy.fileProxyObj).length === 0 || currentItem === 0 ? m('h5.bold', 'Results') - : m('table.results-container', [ - m( - 'thead.results-header', - m('tr', [ - m('th', 'Name'), - m('th', 'Size'), - m('th', 'Hash'), - m('th', 'Download'), - ]) - ), - m( - 'tbody.results', - fproxy.fileProxyObj[currentItem.slice(1)] - ? fproxy.fileProxyObj[currentItem.slice(1)].map((item) => - m('tr', [ - m('td.results__name', [m('i.fas.fa-file'), m('span', item.fName)]), - m('td.results__size', rs.formatBytes(item.fSize.xint64)), - m('td.results__hash', item.fHash), - m( - 'td.results__download', - m('button', { onclick: () => handleFileDownload(item) }, 'Download') - ), - ]) - ) - : 'No Results.' - ), - ]), + : m('div.results-container', [ + m( + 'div.results-header', + m('.results-row', [ + m('.results-cell.name-col', 'Name'), + m('.results-cell.size-col', 'Size'), + m('.results-cell.hash-col', 'Hash'), + m('.results-cell.action-col', 'Download'), + ]) + ), + m( + 'div.results-list', + fproxy.fileProxyObj[currentItem.slice(1)] + ? fproxy.fileProxyObj[currentItem.slice(1)].map((item) => + m('div.results-row.file-item', [ + m('.results-cell.name-col', [m('i.fas.fa-file'), m('span', item.fName)]), + m('.results-cell.size-col', rs.formatBytes((item.fSize && (item.fSize.xint64 || item.fSize.xstr64)) || 0)), + m('.results-cell.hash-col', item.fHash), + m( + '.results-cell.action-col', + m('button', { onclick: () => handleFileDownload(item) }, 'Download') + ), + ]) + ) + : 'No Results.' + ), + ]), ]), ]), ]), diff --git a/webui-src/app/files/my_files.js b/webui-src/app/files/my_files.js index d040aeb..2cc332b 100644 --- a/webui-src/app/files/my_files.js +++ b/webui-src/app/files/my_files.js @@ -3,6 +3,14 @@ const rs = require('rswebui'); const util = require('files/files_util'); const manager = require('files/files_manager'); +const translateName = (name) => { + const n = name.toLowerCase().trim(); + if (n === 'extra list' || n === '[extra list]') return 'Temporary shared files'; + // Match hex strings (IDs) or pure numeric strings + if (/^[0-9a-fA-F]{16,}$/.test(name) || /^\d+$/.test(name)) return 'My Files'; + return name; +}; + const DisplayFiles = () => { const childrenList = []; // stores children details let loaded = false; // checks whether we have loaded the children details or not. @@ -15,28 +23,34 @@ const DisplayFiles = () => { }, view: (v) => [ m('tr', [ - parStruct && Object.keys(parStruct.details.children).length + parStruct && parStruct.details.children && parStruct.details.children.length ? m( - 'td', - m('i.fas.fa-angle-right', { - class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`, - style: 'margin-top: 0.5rem', - onclick: () => { - if (!loaded) { - // if it is not already retrieved - parStruct.details.children.map(async (child) => { - const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + 'td', + m('i.fas.fa-angle-right', { + class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`, + style: 'margin-top: 0.5rem', + onclick: async () => { + if (!loaded) { + // if it is not already retrieved + const results = await Promise.all( + parStruct.details.children.map((child) => + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { handle: child.handle.xint64, flags: util.RS_FILE_HINTS_LOCAL, - }); + }) + ) + ); + results.forEach((res) => { + if (res && res.body && res.body.details) { childrenList.push(res.body.details); - loaded = true; - }); - } - parStruct.showChild = !parStruct.showChild; - }, - }) - ) + } + }); + loaded = true; + } + parStruct.showChild = !parStruct.showChild; + }, + }) + ) : m('td', ''), m( 'td', @@ -47,29 +61,49 @@ const DisplayFiles = () => { left: `calc(1.5rem*${v.attrs.replyDepth})`, }, }, - parStruct.details.name + translateName(parStruct.details.name || '') ), - m('td', rs.formatBytes(parStruct.details.size.xint64)), + m('td', rs.formatBytes((parStruct.details.size && parStruct.details.size.xint64) || 0)), ]), parStruct.showChild && - childrenList.map((child) => - m(DisplayFiles, { - // recursive call - par_directory: { details: child, showChild: false }, - replyDepth: v.attrs.replyDepth + 1, - }) - ), + childrenList.map((child) => + m(DisplayFiles, { + // recursive call + par_directory: { details: child, showChild: false }, + replyDepth: v.attrs.replyDepth + 1, + }) + ), ], }; }; const Layout = () => { - // let root_handle; - let parent; - let showShareManager = false; + let displayList = []; + let isLoading = true; + let showShareManager = false; // Retain original declaration + return { oninit: () => { - rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then((res) => (parent = res)); + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then(async (res) => { + if (res && res.body && res.body.details) { + if (res.body.details.name === 'root') { + // Skip root and fetch full details for each child (Location ID, Extra list, etc) + const results = await Promise.all( + res.body.details.children.map((child) => + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + flags: util.RS_FILE_HINTS_LOCAL, + }) + ) + ); + displayList = results.map((r) => r.body.details); + } else { + displayList = [res.body.details]; + } + } + isLoading = false; + m.redraw(); + }); }, view: () => [ m('.widget__heading', [ @@ -81,11 +115,14 @@ const Layout = () => { util.MyFilesTable, m( 'tbody', - parent && - m(DisplayFiles, { - par_directory: { details: parent.body.details, showChild: false }, - replyDepth: 0, - }) + isLoading + ? m('tr', m('td[colspan=3]', 'Loading...')) + : displayList.map((details) => + m(DisplayFiles, { + par_directory: { details, showChild: false }, + replyDepth: 0, + }) + ) ) ), m( diff --git a/webui-src/app/main.js b/webui-src/app/main.js index f842e24..268b829 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -44,7 +44,7 @@ const navbar = () => { m('.nav-menu__logo-text', [ m('h5', 'RetroShare'), m('.webui-version-box', [ - m('span.webui-version', 'v44'), + m('span.webui-version', 'v61'), m('i.fas.fa-sync-alt.refresh-icon', { onclick: () => window.location.reload(true), title: 'Force reload application', @@ -181,3 +181,16 @@ m.route(document.getElementById('main'), '/', { render: (v) => m(Layout, m(config, v.attrs)), }, }); + +// v51 architectural fix: ensure event queue starts on direct route refresh +if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) { + console.info('[RS-DEBUG] main.js: Initiating global event queue for verified session...'); + rs.logon( + { Authorization: `Basic ${btoa(`${rs.loginKey.username}:${rs.loginKey.passwd}`)}` }, + () => { }, // displayAuthError + () => { }, // displayErrorMessage + () => { + console.info('[RS-DEBUG] Global event queue successfully started.'); + } + ); +} diff --git a/webui-src/app/rswebui.js b/webui-src/app/rswebui.js index 81e8635..1b50b03 100644 --- a/webui-src/app/rswebui.js +++ b/webui-src/app/rswebui.js @@ -106,6 +106,7 @@ function rsJsonApiRequest( handleSerialize = JSON.stringify, config = null ) { + console.warn('[RS-DEBUG] rsJsonApiRequest called for path:', path, 'with config:', !!config); headers['Accept'] = 'application/json'; if (loginKey.isVerified) { if (loginKey.username && loginKey.passwd) { @@ -135,7 +136,7 @@ function rsJsonApiRequest( headers, body: data, - config, + xhr: config, }) .then((result) => { if (result.status === 200) { @@ -254,8 +255,9 @@ const eventQueue = { }, }, handler: (event) => { + console.warn('[RS-DEBUG] Event queue handler received type:', event.mType); if (!deeperIfExist(eventQueue.events, event.mType, (owner) => owner.handler(event, owner))) { - // console.info('[RS-DEBUG] unhandled event', event); + console.info('[RS-DEBUG] unhandled event', event); } }, }; @@ -340,42 +342,57 @@ function startEventQueue( displayErrorMessage = () => { }, successful = () => { } ) { - return rsJsonApiRequest( - '/rsEvents/registerEventsHandler', - {}, - (data, success) => { - if (success) { - // unused - } else if (data.status === 401) { + console.warn('[RS-DEBUG] startEventQueue starting raw XHR for:', info); + const xhr = new window.XMLHttpRequest(); + let lastIndex = 0; + xhr.open('POST', loginKey.url + '/rsEvents/registerEventsHandler', true); + + // Set headers for authentication + const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + ...loginHeader, + }; + + if (loginKey.isVerified && !headers['Authorization']) { + if (loginKey.username && loginKey.passwd) { + console.warn('[RS-DEBUG] Setting persistent Auth header for event queue'); + headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd); + } else { + console.warn('[RS-DEBUG] Missing credentials for event queue auth'); + } + } + + Object.keys(headers).forEach((key) => { + xhr.setRequestHeader(key, headers[key]); + }); + + xhr.onreadystatechange = () => { + console.warn('[RS-DEBUG] Event Queue XHR state changed:', xhr.readyState, 'status:', xhr.status); + if (xhr.readyState === 4) { + if (xhr.status === 401) { displayAuthError('Incorrect login/password.'); - } else if (data.status === 0) { - displayErrorMessage([ - 'Retroshare-jsonapi not available.', - m('br'), - 'Please fix host and/or port.', - ]); - } else { - displayErrorMessage('Login failed: HTTP ' + data.status + ' ' + data.statusText); + } else if (xhr.status === 0) { + console.error('[RS-DEBUG] Event Queue connection failed (status 0)'); } - }, - true, - loginHeader, - JSON.parse, - JSON.stringify, - (xhr, args, url) => { - let lastIndex = 0; - xhr.onprogress = (ev) => { - const currIndex = xhr.responseText.length; - if (currIndex > lastIndex) { - const parts = xhr.responseText.substring(lastIndex, currIndex); - lastIndex = currIndex; - parts - .trim() - .split('\n\n') - .filter((e) => e.startsWith('data: {')) - .map((e) => e.substr(6)) - .map(JSON.parse) - .forEach((data) => { + } + }; + + xhr.onprogress = (ev) => { + const currIndex = xhr.responseText.length; + if (currIndex > lastIndex) { + const parts = xhr.responseText.substring(lastIndex, currIndex); + lastIndex = currIndex; + console.warn('[RS-DEBUG] RAW DATA RECEIVED:', parts); + parts + .trim() + .split('\n\n') + .filter((e) => e.trim().length > 0) + .forEach((e) => { + if (e.startsWith('data: {')) { + try { + const data = JSON.parse(e.substr(6)); + console.warn('[RS-DEBUG] PARSED EVENT:', data); if (Object.prototype.hasOwnProperty.call(data, 'retval')) { console.info( '[RS-DEBUG] ' + info + ' [' + data.retval.errorCategory + '] ' + data.retval.errorMessage @@ -389,21 +406,38 @@ function startEventQueue( data.event.queueSize = currIndex; try { eventQueue.handler(data.event); - } catch (e) { - console.error('[RS-DEBUG] Error in event handler:', e, data.event); + } catch (err) { + console.error('[RS-DEBUG] Error in event handler:', err, data.event); } } - }); - if (currIndex > 1e6) { - // max 1 MB eventQueue - startEventQueue('restart queue'); - xhr.abort(); + } catch (err) { + console.error('[RS-DEBUG] JSON parse error for part:', e, err); + } + } else { + console.info('[RS-DEBUG] Ignored non-data part:', e); } - } - }; - return xhr; + }); + if (currIndex > 1e6) { + // max 1 MB eventQueue + console.warn('[RS-DEBUG] Restarting event queue (size > 1MB)'); + startEventQueue('restart queue'); + xhr.abort(); + } } - ); + }; + + xhr.onload = () => { + console.warn('[RS-DEBUG] Event Queue XHR load finished. Status:', xhr.status); + }; + + xhr.onerror = (err) => { + console.error('[RS-DEBUG] Event Queue XHR error occurred:', err); + }; + + // We need to send an eventType to registerEventsHandler + // 0 means all events + xhr.send(JSON.stringify({ eventType: 0 })); + return xhr; } function logon(loginHeader, displayAuthError, displayErrorMessage, successful) { diff --git a/webui-src/styles.css b/webui-src/styles.css index 49c628e..4ee381c 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -46,84 +46,84 @@ p { @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Bold.woff2") format("woff2"), url("./webfonts/Roboto-Bold.woff") format("woff"), url("./webfonts/Roboto-Bold.ttf") format("truetype"); + src: url("./webfonts/Roboto-Bold.woff") format("woff"), url("./webfonts/Roboto-Bold.ttf") format("truetype"); font-weight: 700; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Bold.woff2") format("woff2"), url("./webfonts/Roboto-Bold.woff") format("woff"), url("./webfonts/Roboto-Bold.ttf") format("truetype"); + src: url("./webfonts/Roboto-Bold.woff") format("woff"), url("./webfonts/Roboto-Bold.ttf") format("truetype"); font-weight: bold; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"), url("./webfonts/Roboto-BoldItalic.woff") format("woff"), url("./webfonts/Roboto-BoldItalic.ttf") format("truetype"); + src: url("./webfonts/Roboto-BoldItalic.woff") format("woff"), url("./webfonts/Roboto-BoldItalic.ttf") format("truetype"); font-weight: 700; font-style: italic } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"), url("./webfonts/Roboto-BoldItalic.woff") format("woff"), url("./webfonts/Roboto-BoldItalic.ttf") format("truetype"); + src: url("./webfonts/Roboto-BoldItalic.woff") format("woff"), url("./webfonts/Roboto-BoldItalic.ttf") format("truetype"); font-weight: bold; font-style: italic } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Medium.woff2") format("woff2"), url("./webfonts/Roboto-Medium.woff") format("woff"), url("./webfonts/Roboto-Medium.ttf") format("truetype"); + src: url("./webfonts/Roboto-Medium.woff") format("woff"), url("./webfonts/Roboto-Medium.ttf") format("truetype"); font-weight: 500; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"), url("./webfonts/Roboto-MediumItalic.woff") format("woff"), url("./webfonts/Roboto-MediumItalic.ttf") format("truetype"); + src: url("./webfonts/Roboto-MediumItalic.woff") format("woff"), url("./webfonts/Roboto-MediumItalic.ttf") format("truetype"); font-weight: 500; font-style: italic } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Regular.woff2") format("woff2"), url("./webfonts/Roboto-Regular.woff") format("woff"), url("./webfonts/Roboto-Regular.ttf") format("truetype"); + src: url("./webfonts/Roboto-Regular.woff") format("woff"), url("./webfonts/Roboto-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Regular.woff2") format("woff2"), url("./webfonts/Roboto-Regular.woff") format("woff"), url("./webfonts/Roboto-Regular.ttf") format("truetype"); + src: url("./webfonts/Roboto-Regular.woff") format("woff"), url("./webfonts/Roboto-Regular.ttf") format("truetype"); font-weight: normal; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Italic.woff2") format("woff2"), url("./webfonts/Roboto-Italic.woff") format("woff"), url("./webfonts/Roboto-Italic.ttf") format("truetype"); + src: url("./webfonts/Roboto-Italic.woff") format("woff"), url("./webfonts/Roboto-Italic.ttf") format("truetype"); font-weight: 400; font-style: italic } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Italic.woff2") format("woff2"), url("./webfonts/Roboto-Italic.woff") format("woff"), url("./webfonts/Roboto-Italic.ttf") format("truetype"); + src: url("./webfonts/Roboto-Italic.woff") format("woff"), url("./webfonts/Roboto-Italic.ttf") format("truetype"); font-weight: normal; font-style: italic } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-Light.woff2") format("woff2"), url("./webfonts/Roboto-Light.woff") format("woff"), url("./webfonts/Roboto-Light.ttf") format("truetype"); + src: url("./webfonts/Roboto-Light.woff") format("woff"), url("./webfonts/Roboto-Light.ttf") format("truetype"); font-weight: 300; font-style: normal } @font-face { font-family: Roboto; - src: url("./webfonts/Roboto-LightItalic.woff2") format("woff2"), url("./webfonts/Roboto-LightItalic.woff") format("woff"), url("./webfonts/Roboto-LightItalic.ttf") format("truetype"); + src: url("./webfonts/Roboto-LightItalic.woff") format("woff"), url("./webfonts/Roboto-LightItalic.ttf") format("truetype"); font-weight: 300; font-style: italic } @@ -8565,4 +8565,242 @@ table.boards tr.hidden { height: auto !important; z-index: 100; } -} \ No newline at end of file +} + +/* FILES MODULE RESPONSIVENESS (v45) */ +@media (max-width: 700px) { + + /* General Files Layout */ + .file-view__body-details { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + /* File Detail Grid (Downloads/Uploads) */ + .file-view__body-details-stat { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .file-view__body-details-stat span { + display: flex; + align-items: center; + } + + /* Share Manager Table -> Cards */ + .share-manager__table, + .share-manager__table thead, + .share-manager__table tbody, + .share-manager__table tr, + .share-manager__table td { + display: block; + width: 100% !important; + } + + .share-manager__table thead { + display: none; + /* Hide header on mobile */ + } + + .share-manager__table tr { + border: 1px solid #ccc; + border-radius: 8px; + margin-bottom: 1rem; + padding: 0.5rem; + background: white; + } + + .share-manager__table td { + margin-bottom: 0.5rem; + border: none !important; + padding-left: 0 !important; + } + + /* My Files / Friends Files Tables -> Cards */ + table.myfiles, + table.myfiles tr, + table.myfiles td, + table.friendsfiles, + table.friendsfiles tr, + table.friendsfiles td { + display: block; + width: 100% !important; + } + + table.myfiles th, + table.friendsfiles th { + display: none; + } + + table.myfiles tr, + table.friendsfiles tr { + border: 1px solid #ccc; + border-radius: 8px; + margin-bottom: 1rem; + padding: 0.5rem; + background: white; + } + + /* Search Container Layout */ + .file-search-container { + flex-direction: column; + } + + .file-search-container__keywords { + flex-basis: auto; + width: 100%; + border-right: none; + border-bottom: 1px solid rgba(20, 20, 27, 0.1); + padding-bottom: 1rem; + margin-bottom: 1rem; + } + + /* Search results table -> cards */ + .results-container, + .results-container thead, + .results-container tbody, + .results-container tr, + .results-container td { + display: block; + width: 100% !important; + } + + .results-container thead { + display: none; + } + + .results-container tr { + border-bottom: 1px solid #eee; + padding: 1rem 0; + } + + .results-container td { + margin-bottom: 0.5rem; + word-break: break-all; + } + + /* GLOBAL SIDEBAR TO TABS (v46) */ + .tab-content { + flex-direction: column; + } + + .sidebar { + width: 100%; + flex-direction: row; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + border-bottom: 1px solid rgba(20, 20, 27, 0.1); + background: white; + z-index: 50; + flex-shrink: 0; + } + + .sidebar a { + display: inline-block; + padding: 0.8rem 1.2rem; + border-bottom: 3px solid transparent; + } + + .sidebar .selected-sidebar-link { + border-left: none; + border-bottom: 3px solid #3ba4d7; + animation: none; + } + + /* Hide quickview headings on mobile to save space if needed */ + .sidebarquickview>h4 { + display: none; + } +} +/* Search Results Responsive Layout (v60) */ +.results-container { + display: flex; + flex-direction: column; + width: 100%; + border-radius: 8px; + overflow: hidden; + background: var(--main-bg-color); + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.results-header { + background: var(--secondary-bg-color); + font-weight: bold; +} + +.results-row { + display: flex; + border-bottom: 1px solid var(--border-color); + padding: 0.8rem; + align-items: center; +} + +.results-cell { + padding: 0 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.name-col { flex: 3; } +.size-col { flex: 1; text-align: center; } +.hash-col { flex: 4; font-family: monospace; font-size: 0.8rem; opacity: 0.7; } +.action-col { flex: 1; text-align: right; } + +.file-item:hover { + background: rgba(var(--primary-color-rgb), 0.05); +} + +/* Mobile Adjustments */ +@media screen and (max-width: 768px) { + .results-header { + display: none; /* Hide header on mobile */ + } + + .results-row { + flex-direction: column; + align-items: flex-start; + padding: 1rem; + gap: 0.5rem; + } + + .results-cell { + padding: 0; + width: 100%; + white-space: normal; + } + + .name-col { + font-weight: bold; + font-size: 1.1rem; + margin-bottom: 0.3rem; + } + + .size-col { + text-align: left; + color: var(--secondary-text-color); + font-size: 0.9rem; + } + + .hash-col { + display: none; /* Hide hash on mobile as requested */ + } + + .action-col { + width: 100%; + text-align: center; + margin-top: 0.5rem; + } + + .action-col button { + width: 100%; + padding: 0.8rem; + background: var(--primary-color); + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + } +}