feat: implement full breadcrumb path navigation in Files tab

- Add breadcrumbPath array to app state for tracking folder hierarchy
- Rewrite updateBreadcrumb() to render full path: Home > folder > subfolder
- Each breadcrumb segment is clickable to navigate back to that level
- Current folder shown in bold (non-clickable), parent folders as links
- Reset breadcrumb path on tab switch, home navigation, and initial load
- Update navigateFolder/selectFolder to push to breadcrumb path
- Enhanced breadcrumb CSS with hover effects and dark mode support
This commit is contained in:
Diocrafts
2026-02-21 00:19:04 +01:00
parent a1a3bd1b2b
commit 269a5fe940
52 changed files with 6809 additions and 6683 deletions
+6 -3
View File
@@ -205,17 +205,20 @@ async function resolveHomeFolder() {
app.userHomeFolderId = home.id;
app.userHomeFolderName = home.name;
app.currentPath = home.id;
window.ui.updateBreadcrumb(home.name);
app.breadcrumbPath = [];
window.ui.updateBreadcrumb();
console.log(`Home folder resolved: ${home.name} (${home.id})`);
} else {
console.warn('No root folders found for user');
app.currentPath = '';
window.ui.updateBreadcrumb('');
app.breadcrumbPath = [];
window.ui.updateBreadcrumb();
}
} catch (error) {
console.error('Error resolving home folder:', error);
app.currentPath = '';
window.ui.updateBreadcrumb('');
app.breadcrumbPath = [];
window.ui.updateBreadcrumb();
}
}
+2 -1
View File
@@ -36,7 +36,8 @@ async function loadFiles(options = {}) {
if (app.userHomeFolderId) {
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
app.currentPath = app.userHomeFolderId;
window.ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
app.breadcrumbPath = [];
window.ui.updateBreadcrumb();
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
} else {
url = `/api/folders?t=${timestamp}`;
+10 -1
View File
@@ -52,6 +52,14 @@ const ACTIONS_BAR_TEMPLATES = {
<span data-i18n="trash.empty_trash">Empty trash</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`,
favorites: `
<div class="action-buttons"></div>
@@ -537,8 +545,9 @@ window.setActionsBarMode = setActionsBarMode;
// Set up global selectFolder function for navigation
window.selectFolder = (id, name) => {
app.breadcrumbPath.push({ id, name });
app.currentPath = id;
ui.updateBreadcrumb(name);
ui.updateBreadcrumb();
window.loadFiles();
};
+2 -1
View File
@@ -76,7 +76,8 @@ function switchToFilesView() {
if (window.app.userHomeFolderId) {
window.app.currentPath = window.app.userHomeFolderId;
window.ui.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
window.app.breadcrumbPath = [];
window.ui.updateBreadcrumb();
} else {
window.app.currentPath = '';
}
+2 -1
View File
@@ -19,7 +19,8 @@ window.app = {
isSearchMode: false,
shareDialogItem: null,
shareDialogItemType: null,
notificationShareUrl: null
notificationShareUrl: null,
breadcrumbPath: [] // Array of {id, name} tracking folder navigation hierarchy
};
window.appElements = {
+51 -51
View File
@@ -472,75 +472,72 @@ const ui = {
},
/**
* Update breadcrumb navigation
* @param {string} folderName - Name of the current folder
* Update breadcrumb navigation from the breadcrumbPath array.
* Renders: Home > folder1 > folder2 > ...
* Each segment is clickable to navigate back to that level.
*/
updateBreadcrumb(folderName) {
updateBreadcrumb() {
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb.innerHTML = '';
// Get user info to help determine home folder
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const username = userData.username || '';
// Create the home item - for users, this is their personal folder
const homeItem = document.createElement('span');
homeItem.className = 'breadcrumb-item';
const self = this;
const path = window.app.breadcrumbPath; // [{id, name}, ...]
// Helper function to safely get translation text
const getTranslatedText = (key, defaultValue) => {
if (!window.i18n || !window.i18n.t) return defaultValue;
return window.i18n.t(key);
};
// First determine if the current view is the user's home folder
const isUserHomeFolder = username && window.app.userHomeFolderName &&
window.app.userHomeFolderName.includes(username) &&
folderName === window.app.userHomeFolderName;
// Set appropriate text for home item
if (isUserHomeFolder) {
// If the current folder is the user's home folder, label it as "Home"
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
} else if (folderName && folderName.startsWith('My Folder')) {
// If viewing a root folder but not the user's home folder, use its full name
homeItem.textContent = folderName;
} else {
// Default - use "Home" label
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
// For searching, we might have a custom breadcrumb text
if (folderName && folderName.startsWith('Search:')) {
// We're in search mode - don't add click handler
breadcrumb.appendChild(homeItem);
return;
}
}
// Add click handler - but only if we have a user home folder to return to
if (window.app.userHomeFolderId) {
// -- Home item (always present) --
const homeItem = document.createElement('span');
homeItem.className = 'breadcrumb-item';
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
// If we have deeper segments, Home is clickable
if (path.length > 0 && window.app.userHomeFolderId) {
homeItem.classList.add('breadcrumb-link');
homeItem.addEventListener('click', () => {
window.app.breadcrumbPath = [];
window.app.currentPath = window.app.userHomeFolderId;
this.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
self.updateBreadcrumb();
window.loadFiles();
});
} else {
homeItem.classList.add('breadcrumb-current');
}
breadcrumb.appendChild(homeItem);
// If we have a subfolder, add it to the breadcrumb
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Search:')) {
// -- Intermediate + current segments --
path.forEach((segment, index) => {
const isLast = index === path.length - 1;
// Separator
const separator = document.createElement('span');
separator.className = 'breadcrumb-separator';
separator.textContent = '>';
breadcrumb.appendChild(separator);
const folderItem = document.createElement('span');
folderItem.className = 'breadcrumb-item';
folderItem.textContent = folderName;
breadcrumb.appendChild(folderItem);
}
// Segment item
const item = document.createElement('span');
item.className = 'breadcrumb-item';
item.textContent = segment.name;
if (!isLast) {
// Intermediate segment: clickable – truncate path to this level
item.classList.add('breadcrumb-link');
item.addEventListener('click', () => {
window.app.breadcrumbPath = path.slice(0, index + 1);
window.app.currentPath = segment.id;
self.updateBreadcrumb();
window.loadFiles();
});
} else {
// Last segment: current location, not clickable
item.classList.add('breadcrumb-current');
}
breadcrumb.appendChild(item);
});
},
/**
@@ -733,8 +730,11 @@ const ui = {
};
const navigateFolder = (card) => {
window.app.currentPath = card.dataset.folderId;
self.updateBreadcrumb(card.dataset.folderName);
const folderId = card.dataset.folderId;
const folderName = card.dataset.folderName;
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
window.app.currentPath = folderId;
self.updateBreadcrumb();
window.loadFiles();
};
+3
View File
@@ -200,6 +200,9 @@ function replaceIconsInElement(container) {
// Copy inline style if present
if (el.style.cssText) svg.style.cssText = el.style.cssText;
// Preserve id attribute if present
if (el.id) svg.id = el.id;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("fill", "currentColor");
path.setAttribute("d", d);
+12 -2
View File
@@ -87,8 +87,18 @@ const Modal = {
cancelText = null
} = options;
// Set modal content
this.icon.className = `fas ${icon}`;
// Set modal content - update the icon
const iconContainer = document.querySelector('.modal-icon');
if (iconContainer) {
// Replace contents with a fresh <i> that icons.js will convert
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
this.icon = document.getElementById('modal-icon');
// Let icons.js convert it to SVG
if (window.replaceIconsInElement) {
window.replaceIconsInElement(iconContainer);
this.icon = document.getElementById('modal-icon');
}
}
this.title.textContent = title;
this.label.textContent = label;
this.input.placeholder = placeholder;
+71 -23
View File
@@ -69,24 +69,35 @@ const sharedView = {
container.innerHTML = `
<div class="shared-header">
<div class="shared-filters">
<select id="filter-type" class="shared-filter-select">
<option value="all" data-i18n="shared_filterAll">All</option>
<option value="file" data-i18n="shared_filterFiles">Files</option>
<option value="folder" data-i18n="shared_filterFolders">Folders</option>
</select>
<select id="sort-by" class="shared-filter-select">
<option value="date" data-i18n="shared_sortByDate">Sort by date</option>
<option value="name" data-i18n="shared_sortByName">Sort by name</option>
<option value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</option>
</select>
<div class="shared-custom-select" id="filter-type-wrapper">
<button class="shared-select-toggle" id="filter-type-toggle">
<span class="shared-select-label" data-i18n="shared_filterAll">All</span>
<i class="fas fa-chevron-down shared-select-arrow"></i>
</button>
<div class="shared-select-dropdown" id="filter-type-dropdown">
<div class="shared-select-option active" data-value="all" data-i18n="shared_filterAll">All</div>
<div class="shared-select-option" data-value="file" data-i18n="shared_filterFiles">Files</div>
<div class="shared-select-option" data-value="folder" data-i18n="shared_filterFolders">Folders</div>
</div>
</div>
<div class="shared-custom-select" id="sort-by-wrapper">
<button class="shared-select-toggle" id="sort-by-toggle">
<span class="shared-select-label" data-i18n="shared_sortByDate">Sort by date</span>
<i class="fas fa-chevron-down shared-select-arrow"></i>
</button>
<div class="shared-select-dropdown" id="sort-by-dropdown">
<div class="shared-select-option active" data-value="date" data-i18n="shared_sortByDate">Sort by date</div>
<div class="shared-select-option" data-value="name" data-i18n="shared_sortByName">Sort by name</div>
<div class="shared-select-option" data-value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</div>
</div>
</div>
</div>
</div>
<div id="empty-shared-state" class="empty-state" style="display:none;">
<div class="empty-state-icon">📤</div>
<h3 data-i18n="shared_emptyStateTitle">No shared items</h3>
<i class="fas fa-share-alt" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p data-i18n="shared_emptyStateTitle">No shared items</p>
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
<button id="empty-go-to-files" class="button primary" data-i18n="shared_goToFiles">Go to Files</button>
</div>
<div class="shared-list-container" style="display:none;">
@@ -183,13 +194,17 @@ const sharedView = {
// Attach event listeners
attachEventListeners() {
const filterType = document.getElementById('filter-type');
const sortBy = document.getElementById('sort-by');
const emptyGoToFiles = document.getElementById('empty-go-to-files');
// Custom dropdown logic for filter-type
this._initCustomSelect('filter-type-wrapper', 'filter-type-toggle', 'filter-type-dropdown');
// Custom dropdown logic for sort-by
this._initCustomSelect('sort-by-wrapper', 'sort-by-toggle', 'sort-by-dropdown');
if (filterType) filterType.addEventListener('change', () => this.filterAndSortItems());
if (sortBy) sortBy.addEventListener('change', () => this.filterAndSortItems());
if (emptyGoToFiles) emptyGoToFiles.addEventListener('click', () => window.switchToFilesView());
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
if (!sel.contains(e.target)) sel.classList.remove('open');
});
});
// Share dialog (sharedView-specific IDs)
const shareDialog = document.getElementById('shared-view-edit-dialog');
@@ -226,13 +241,46 @@ const sharedView = {
}
},
// Initialize a custom select dropdown
_initCustomSelect(wrapperId, toggleId, dropdownId) {
const wrapper = document.getElementById(wrapperId);
const toggle = document.getElementById(toggleId);
const dropdown = document.getElementById(dropdownId);
if (!wrapper || !toggle || !dropdown) return;
toggle.addEventListener('click', (e) => {
e.stopPropagation();
// Close other open selects
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
if (sel !== wrapper) sel.classList.remove('open');
});
wrapper.classList.toggle('open');
});
dropdown.querySelectorAll('.shared-select-option').forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
// Update active state
dropdown.querySelectorAll('.shared-select-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
// Update label
const label = toggle.querySelector('.shared-select-label');
if (label) label.textContent = option.textContent;
// Close dropdown
wrapper.classList.remove('open');
// Trigger filter
this.filterAndSortItems();
});
});
},
// Filter and sort items
filterAndSortItems() {
const filterType = document.getElementById('filter-type');
const sortBy = document.getElementById('sort-by');
const filterTypeActive = document.querySelector('#filter-type-dropdown .shared-select-option.active');
const sortByActive = document.querySelector('#sort-by-dropdown .shared-select-option.active');
const type = filterType ? filterType.value : 'all';
const sort = sortBy ? sortBy.value : 'date';
const type = filterTypeActive ? filterTypeActive.dataset.value : 'all';
const sort = sortByActive ? sortByActive.dataset.value : 'date';
// Use the top-bar search if available, otherwise no filter
const topSearch = document.getElementById('shared-search');