Files
rclone-ui/public/browse.html
T
FTCHDandGitHub 7a62659323 v3
can’t innovate my ass 

Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
2025-12-07 18:42:44 +01:00

624 lines
18 KiB
HTML

<!--
This file is needed in order to add dark mode to the default Rclone browser.
It's not pretty, but at the same time Adobe's .PSD implementation exists.
-->
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<style>
html {
overscroll-behavior: none;
}
/* Header positioning */
header {
position: sticky;
top: 0;
z-index: 100;
padding: 10px;
background-color: #f2f2f2;
}
/* Meta div positioning */
.meta {
position: sticky;
top: 63px; /* Adjust based on header height + padding */
z-index: 99;
/* padding: 10px; */
background-color: #f2f2f2;
}
.loading-spinner {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.nav-spinner {
position: fixed;
bottom: 20px;
right: 20px;
width: 30px;
height: 30px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
display: none;
z-index: 9999;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark !important;
}
html {
background-color: #1d1d1d !important;
min-height: 100% !important;
}
body {
background-color: #1d1d1d !important;
color: #e0e0e0 !important ;
min-height: 100vh !important;
margin: 0 !important;
}
/* Header styles */
header {
background-color: #242424 !important;
}
header h1 a {
color: #e0e0e0 !important;
}
/* Table styles */
table {
background-color: #242424 !important;
border-color: #404040 !important;
}
tr {
border-color: #404040 !important;
}
tr:hover {
background-color: #2a2a2a !important;
}
th {
background-color: #2c2c2c !important;
color: #e0e0e0 !important;
}
td {
border-color: #404040 !important;
}
/* Links */
a {
color: #66b3ff !important;
letter-spacing: 0.05px !important;
}
a:hover {
color: #99ccff !important;
}
a:not([class]) {
color: #e8e8e8 !important;
}
a[href^="."] {
color: #808080 !important;
}
/* Item name font size */
span.name {
font-size: 16px !important;
}
/* Filter input */
#filter {
background-color: #2c2c2c !important;
color: #e0e0e0 !important;
border: 1px solid #404040 !important;
padding: 5px 10px !important;
height: 28px !important;
width: 100% !important;
max-width: -webkit-fill-available !important;
border-radius: 10px !important;
font-size: 16px !important;
text-transform: capitalize !important;
}
#filter:focus {
outline: 1px solid #66b3ff !important;
border-color: #66b3ff !important;
}
/* SVG icons */
svg {
filter: invert(0.8) !important;
}
/* Go up link */
.goup {
color: #e0e0e0 !important;
}
/* Time display */
time {
color: #b0b0b0 !important;
}
/* Sort icons */
.icon.sort {
opacity: 0.8 !important;
}
/* Meta section */
.meta {
border-color: #404040 !important;
background-color: #242424 !important;
}
/* Ensure text remains readable */
td[data-order="-1"],
.hideable {
color: #b0b0b0 !important;
}
.download-link {
background-color: #2e7d32 !important;
color: #ffffff !important;
}
.download-link:hover {
background-color: #1b5e20 !important;
}
thead {
background-color: #242424 !important;
}
}
/* Download button styles */
.download-cell {
width: 30px;
text-align: center;
}
.download-link {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
text-decoration: none;
background-color: #4CAF50;
color: white !important;
font-size: 0.8em;
margin-left: 8px;
vertical-align: middle;
}
.download-link:hover {
background-color: #45a049;
}
/* Adjust the name span to align with download button */
.name {
display: inline-flex;
align-items: center;
gap: 8px;
}
/* Table header positioning */
thead {
position: sticky;
top: 109px; /* header + meta */
z-index: 98;
background-color: #f2f2f2;
}
/* Ensure table layout works with sticky header */
table {
border-collapse: separate;
border-spacing: 0;
}
</style>
<script>
window.filter = function() {
const searchString = document.getElementById('filter').value.trim().toLowerCase();
document.querySelectorAll('tr.file').forEach(function(el) {
if (!searchString) {
el.style.display = '';
return;
}
const label = el.querySelector('.name').textContent.trim().toLowerCase();
if (label.indexOf(searchString) !== -1) {
el.style.display = '';
} else {
el.style.display = 'none';
}
});
}
</script>
<script>
// Initialize everything once __TAURI__ is available
function initializeApp() {
// Create a persistent spinner element outside the body
const navSpinner = document.createElement('div');
navSpinner.id = 'nav-spinner';
navSpinner.className = 'nav-spinner';
function toggleNavSpinner(show) {
if (!document.getElementById('nav-spinner')) {
document.body.appendChild(navSpinner);
}
navSpinner.style.display = show ? 'block' : 'none';
}
function isRootUrl(url) {
try {
const parsedUrl = new URL(url);
// Remove trailing slash for consistent comparison
const path = parsedUrl.pathname.replace(/\/$/, '');
// Count segments (excluding empty strings from leading/trailing slashes)
const segments = path.split('/').filter(Boolean);
return segments.length <= 1;
} catch (e) {
console.error('Error parsing URL:', e);
return false;
}
}
function hideGoUpIfRoot(url) {
const goUpRow = document.querySelector('tr:has(span.goup)');
if (goUpRow) {
goUpRow.style.display = isRootUrl(url) ? 'none' : '';
}
}
async function fetchAndInjectContent(url, auth, isInitialLoad = false) {
try {
// Only show navigation spinner for non-initial loads
if (!isInitialLoad) {
toggleNavSpinner(true);
}
let newUrl = url
console.log('newUrl', newUrl)
const fetch = window.__TAURI__.http.fetch
// Fetch the webpage content
const options = {
method: 'GET',
};
if (auth) {
options.headers = {
'Authorization': 'Basic ' + auth
};
}
const response = await fetch(newUrl, options);
const html = await response.text();
console.log('fetched html for', newUrl)
// Create a temporary DOM parser
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Modify the title if it exists
// const title = doc.querySelector('title');
// if (title) {
// title.textContent = 'Modified: ' + title.textContent;
// // Update current page title
// document.title = title.textContent;
// }
// Breadcrumbs are stored as a list of <a> inside an <h1>
// Disable the first link in the breadcrumbs, the one that says /
const header = doc.querySelector('header');
if (header) {
const h1 = header.querySelector('h1');
if (h1) {
const links = h1.querySelectorAll('a');
if (links.length > 0) {
links[0].style.pointerEvents = 'none';
}
}
}
// Remove existing injected styles
const existingStyles = document.querySelectorAll('style[data-injected="true"]');
existingStyles.forEach(style => style.remove());
// Copy over all style elements from the fetched document
const newStyles = doc.getElementsByTagName('style');
Array.from(newStyles).forEach(style => {
const clonedStyle = style.cloneNode(true);
clonedStyle.setAttribute('data-injected', 'true');
document.head.appendChild(clonedStyle);
});
// Store the spinner temporarily
const spinnerParent = navSpinner.parentElement;
if (spinnerParent) {
spinnerParent.removeChild(navSpinner);
}
// Replace only the body content
// regex for a hrefs with a content of /, like <a>/</a>
document.body.innerHTML = doc.body.innerHTML;
// Hide "Go up" row if at root
hideGoUpIfRoot(url);
// Re-add the spinner
document.body.appendChild(navSpinner);
// Update the URL in the address bar without reloading
const params = new URLSearchParams();
params.set('url', url);
if (auth) params.set('auth', auth);
window.history.pushState({}, '', `?${params.toString()}`);
// Re-add our event handlers
setupEventHandlers();
} catch (error) {
console.error('Error fetching webpage:', error);
document.body.innerHTML = `<div>Error loading webpage: ${error.message}</div>`;
// Make sure spinner is still available in error case
document.body.appendChild(navSpinner);
} finally {
toggleNavSpinner(false);
}
}
function handleLinkClick(event) {
const link = event.target.closest('a');
if (!link) return;
console.log('[handleLinkClick] link', link)
const href = link.getAttribute('href');
if (!href || href === '') {
event.preventDefault();
return;
}
// Handle navigation for "Go up"
const isGoUp = link.querySelector('span.goup') !== null;
if (isGoUp) {
console.log('[handleLinkClick] isGoUp')
event.preventDefault();
let currentUrl = new URLSearchParams(window.location.search).get('url');
currentUrl = currentUrl.replace(/\?.*$/, '') // remove params before going up
if (currentUrl.endsWith('/')) {
currentUrl = currentUrl.slice(0, -1);
}
currentUrl = currentUrl.split('/');
currentUrl.pop();
const fullUrl = currentUrl.join('/') + '/';
const urlParams = new URLSearchParams(window.location.search);
const auth = urlParams.get('auth');
fetchAndInjectContent(fullUrl, auth);
return;
}
console.log('[handleLinkClick] not isGoUp')
// Get the row and check if it's a file
const isFileOrFolder = Boolean(link.closest('tr.file'));
if (isFileOrFolder) {
const row = link.closest('tr')
if (!row) return;
console.log('[handleLinkClick] row', row)
const isFolder = row.querySelector('svg use').getAttribute('xlink:href') === '#folder';
if (!isFolder) {
// Handle file download
event.preventDefault();
const save = window.__TAURI__.dialog.save;
const writeFile = window.__TAURI__.fs.writeFile;
const http = window.__TAURI__.http;
// Get the full URL for the download
const href = link.getAttribute('href');
const currentUrl = new URLSearchParams(window.location.search).get('url');
const baseUrl = new URL(currentUrl).href;
const downloadUrl = new URL(href, baseUrl).href;
// Get the filename from the URL
const filename = href.split('/').pop();
save({
title: 'Save File',
defaultPath: filename
}).then(async (result) => {
if (result) {
try {
// Show the loading spinner while downloading
toggleNavSpinner(true);
console.log('downloading', downloadUrl);
const options = {
method: 'GET',
responseType: 2 // ResponseType.Binary
};
const urlParams = new URLSearchParams(window.location.search);
const auth = urlParams.get('auth');
if (auth) {
options.headers = {
'Authorization': 'Basic ' + auth
};
}
// Fetch the file contents
const response = await http.fetch(downloadUrl, options);
const data = await response.arrayBuffer();
// Write the file contents
await writeFile(result, data);
} catch (err) {
console.error('Failed to download file:', err);
} finally {
// Hide the loading spinner when done
toggleNavSpinner(false);
}
}
});
return;
}
// continue with normal handling for folders
}
console.log('starting to handle link click for', href)
// Handle different types of URLs
let fullUrl;
if (href.startsWith('http')) {
fullUrl = href;
} else if (href.startsWith('//')) {
fullUrl = 'https:' + href;
} else if (href.startsWith('/')) {
// Get the base URL from the current URL parameter
const currentUrl = new URLSearchParams(window.location.search).get('url');
const baseUrl = new URL(currentUrl).origin;
fullUrl = baseUrl + href;
} else if (href.startsWith('?')) {
const currentUrl = new URLSearchParams(window.location.search).get('url');
const baseUrl = new URL(currentUrl).href;
const existingParams = new URLSearchParams(baseUrl.split('?')[1]);
const newParams = new URLSearchParams(href.split('?')[1]);
let modifiedHref = href
if (existingParams.has('order') && newParams.has('order')) {
if (existingParams.get('order') === 'asc') {
modifiedHref = modifiedHref.replace('order=asc', 'order=desc')
} else {
modifiedHref = modifiedHref.replace('order=desc', 'order=asc')
}
}
fullUrl = baseUrl.replace(/\?.*$/, '') + modifiedHref
} else {
// Relative URL
const currentUrl = new URLSearchParams(window.location.search).get('url');
const baseUrl = new URL(currentUrl).href;
fullUrl = new URL(href, baseUrl).href;
}
// Check if the target URL is the same as the current one
const currentPageUrl = new URLSearchParams(window.location.search).get('url');
console.log('fullUrl', fullUrl)
console.log('currentPageUrl', currentPageUrl)
if (fullUrl === currentPageUrl) {
event.preventDefault();
return;
}
// Prevent default navigation
event.preventDefault();
// Fetch and inject the new content
const urlParams = new URLSearchParams(window.location.search);
const auth = urlParams.get('auth');
fetchAndInjectContent(fullUrl, auth);
}
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
function setupEventHandlers() {
// Handle all click events at the document level
document.addEventListener('click', handleLinkClick);
// Prevent form submissions and potentially handle them manually
document.addEventListener('submit', (event) => {
event.preventDefault();
});
}
// Initial setup
const urlParams = new URLSearchParams(window.location.search);
const url = urlParams.get('url');
const auth = urlParams.get('auth');
if (url) {
// Pass true to indicate this is the initial load
fetchAndInjectContent(url, auth, true);
} else {
document.body.innerHTML = '<div>Error: No URL provided.</div>';
}
// Set up initial event handlers
setupEventHandlers();
// Handle browser back/forward buttons
window.addEventListener('popstate', () => {
const params = new URLSearchParams(window.location.search);
const newUrl = params.get('url');
const auth = params.get('auth');
if (newUrl) {
fetchAndInjectContent(newUrl, auth);
}
});
}
// Function to check for __TAURI__ availability
function checkTauriAvailable() {
if ('__TAURI__' in window && 'http' in window.__TAURI__ && 'dialog' in window.__TAURI__ && 'fs' in window.__TAURI__) {
initializeApp();
} else {
setTimeout(checkTauriAvailable, 50);
}
}
// Start checking for __TAURI__ when DOM is ready
document.addEventListener('DOMContentLoaded', checkTauriAvailable);
</script>
</head>
<body>
<div class="loading-spinner"></div>
</body>
</html>