Frontend optimizations: SVG icons, remove updateFileIcons, unify rendering, scope translatePage

- Replace Font Awesome CDN with inline SVG system (icons.js + MutationObserver)
- Remove updateFileIcons() (~140 lines) - redundant with backend icon_class + MutationObserver
- Migrate favorites.js and recent.js to use shared ui.renderFolders/renderFiles (eliminate ~260 lines of duplicate rendering + per-item event listeners)
- Add view-mode aware click delegation for favorites/recent views
- Fix _createFileCard to apply icon_special_class
- Add translateElement(root) for scoped i18n translation
- Replace full-page translatePage() calls with scoped translateElement() or inline t()
- Remove redundant translatePage() calls in shared.js and auth.js
- Remove Alpine.js from Service Worker cache
This commit is contained in:
Dionisio
2026-02-16 21:51:53 +01:00
parent a4709426d9
commit 1ceae0ce94
28 changed files with 486 additions and 1707 deletions
@@ -0,0 +1,14 @@
use serde::Serialize;
use super::file_dto::FileDto;
use super::folder_dto::FolderDto;
/// Combined DTO that returns both sub-folders and files for a given folder
/// in a single response, eliminating the double-fetch on every navigation.
#[derive(Debug, Serialize)]
pub struct FolderListingDto {
/// Sub-folders inside the requested folder
pub folders: Vec<FolderDto>,
/// Files inside the requested folder
pub files: Vec<FileDto>,
}
+1
View File
@@ -5,6 +5,7 @@ pub mod display_helpers;
pub mod favorites_dto;
pub mod file_dto;
pub mod folder_dto;
pub mod folder_listing_dto;
pub mod i18n_dto;
pub mod pagination;
pub mod recent_dto;
@@ -8,6 +8,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use crate::application::dtos::folder_dto::{CreateFolderDto, MoveFolderDto, RenameFolderDto};
use crate::application::dtos::folder_listing_dto::FolderListingDto;
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::application::ports::inbound::FolderUseCase;
use crate::application::services::folder_service::FolderService;
@@ -183,6 +184,43 @@ impl FolderHandler {
}
}
/// Returns both sub-folders and files for a given folder in a single
/// response, eliminating the double-fetch the frontend used to make.
///
/// Both queries run concurrently via `tokio::join!`.
pub async fn list_folder_listing(
State(state): State<GlobalAppState>,
auth_user: AuthUser,
Path(id): Path<String>,
) -> axum::response::Response {
let folder_service = &state.applications.folder_service;
let file_service = &state.applications.file_retrieval_service;
// Run both queries concurrently — no sequential wait.
let (folders_result, files_result) = tokio::join!(
folder_service.list_folders_for_owner(Some(&id), &auth_user.id),
file_service.list_files(Some(&id))
);
match (folders_result, files_result) {
(Ok(folders), Ok(files)) => {
let listing = FolderListingDto { folders, files };
(StatusCode::OK, Json(listing)).into_response()
}
(Err(err), _) | (_, Err(err)) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(
status,
Json(serde_json::json!({ "error": err.to_string() })),
)
.into_response()
}
}
}
/// Renames a folder (ownership enforced by service layer)
pub async fn rename_folder(
State(service): State<AppState>,
+34 -3
View File
@@ -15,6 +15,7 @@ use crate::{
ports::share_ports::ShareUseCase,
},
common::errors::ErrorKind,
domain::entities::share::ShareItemType,
interfaces::middleware::auth::OptionalAuthUser,
};
@@ -22,6 +23,8 @@ use crate::{
pub struct GetSharesQuery {
pub page: Option<usize>,
pub per_page: Option<usize>,
pub item_id: Option<String>,
pub item_type: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -69,21 +72,49 @@ pub async fn get_shared_link(
}
}
/// Get all shared links created by the current user
/// Get all shared links created by the current user.
/// Supports optional filtering by item_id + item_type query params.
pub async fn get_user_shares(
State(share_use_case): State<Arc<dyn ShareUseCase>>,
auth_user: OptionalAuthUser,
Query(query): Query<GetSharesQuery>,
) -> impl IntoResponse {
let user_id = auth_user
let _user_id = auth_user
.0
.map(|u| u.id)
.unwrap_or_else(|| "anonymous".to_string());
// If both item_id and item_type are provided, return shares for that specific item
if let (Some(item_id), Some(item_type_str)) = (&query.item_id, &query.item_type) {
let item_type = match ShareItemType::try_from(item_type_str.as_str()) {
Ok(t) => t,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": format!("Invalid item_type: {}", item_type_str) })),
)
.into_response();
}
};
return match share_use_case
.get_shared_links_for_item(item_id, &item_type)
.await
{
Ok(shares) => (StatusCode::OK, Json(shares)).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": err.to_string() })),
)
.into_response(),
};
}
// Default: paginated list of all user shares
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(20);
match share_use_case
.get_user_shared_links(&user_id, page, per_page)
.get_user_shared_links(&_user_id, page, per_page)
.await
{
Ok(shares) => (StatusCode::OK, Json(shares)).into_response(),
+9 -1
View File
@@ -144,6 +144,13 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
.route("/{id}/download", get(FolderHandler::download_folder_zip))
.with_state(app_state.clone());
// Combined listing endpoint: returns both sub-folders AND files in one
// response. Needs full AppState because it calls both FolderService
// and FileRetrievalService concurrently.
let folder_listing_router = Router::new()
.route("/{id}/listing", get(FolderHandler::list_folder_listing))
.with_state(app_state.clone());
// Create folder operations that use trash (requires full AppState)
let folders_ops_router =
Router::new().route("/{id}", delete(FolderHandler::delete_folder_with_trash));
@@ -151,7 +158,8 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
// Merge the routers
let folders_router = folders_basic_router
.merge(folders_ops_router)
.merge(folder_zip_router);
.merge(folder_zip_router)
.merge(folder_listing_router);
// Create file routes for basic operations and trash-enabled delete
let basic_file_router = Router::new()
+1 -1
View File
@@ -6,7 +6,7 @@
<title>OxiCloud — Admin Panel</title>
<!-- Apply saved theme immediately to prevent flash of light mode -->
<script>if(localStorage.getItem('oxicloud_theme')==='dark')document.documentElement.setAttribute('data-theme','dark');</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="/js/icons.js" defer></script>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}
body{background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;flex-direction:column}
-189
View File
@@ -1,189 +0,0 @@
/* OxiCloud File Viewer Styles */
.file-viewer-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
z-index: 3000;
display: none !important;
opacity: 0;
transition: opacity 0.3s ease;
justify-content: center;
align-items: center;
}
.file-viewer-container.active {
display: flex !important;
opacity: 1;
}
.file-viewer-content {
width: 90%;
height: 90%;
background-color: #fff;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
max-width: 1200px;
}
.file-viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background-color: #f8f9fa;
border-bottom: 1px solid #e2e8f0;
}
.file-viewer-title {
font-size: 18px;
font-weight: 500;
color: #2d3748;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-viewer-close {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #718096;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.file-viewer-close:hover {
background-color: #e2e8f0;
color: #4a5568;
}
.file-viewer-area {
flex-grow: 1;
overflow: auto;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.file-viewer-toolbar {
display: flex;
gap: 10px;
padding: 10px 20px;
background-color: #f8f9fa;
border-top: 1px solid #e2e8f0;
justify-content: flex-end;
}
.file-viewer-toolbar button {
background: none;
border: 1px solid #e2e8f0;
border-radius: 4px;
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #4a5568;
transition: all 0.2s;
}
.file-viewer-toolbar button:hover {
background-color: #e2e8f0;
color: #2d3748;
}
/* Image viewer styles */
.file-viewer-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transform-origin: center;
transition: transform 0.2s ease;
}
/* PDF viewer styles */
.file-viewer-pdf {
width: 100%;
height: 100%;
border: none;
}
/* Text viewer styles */
.file-viewer-text-content {
width: 100%;
height: 100%;
margin: 0;
padding: 16px 24px;
font-family: 'Courier New', Consolas, Monaco, monospace;
font-size: 14px;
line-height: 1.6;
color: #2d3748;
background-color: #fff;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
text-align: left;
tab-size: 4;
}
/* Loader */
.file-viewer-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #4a5568;
}
/* Unsupported file message */
.file-viewer-unsupported {
text-align: center;
padding: 40px;
max-width: 500px;
}
.file-viewer-unsupported i {
font-size: 48px;
color: #a0aec0;
margin-bottom: 20px;
}
.file-viewer-unsupported p {
margin-bottom: 20px;
color: #4a5568;
font-size: 16px;
}
.file-viewer-unsupported .download-btn {
margin-top: 20px;
}
/* Responsive styles */
@media (max-width: 768px) {
.file-viewer-content {
width: 100%;
height: 100%;
border-radius: 0;
}
.file-viewer-toolbar {
padding: 10px;
}
}
+16 -21
View File
@@ -10,31 +10,26 @@
<!-- Styles -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/inlineViewer.css">
<link rel="stylesheet" href="/css/fileViewer.css">
<link rel="stylesheet" href="/css/favorites.css">
<link rel="stylesheet" href="/css/recent.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<!-- Scripts -->
<script src="/js/i18n.js"></script>
<script src="/js/languageSelector.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/modal.js"></script>
<script src="/js/ui.js"></script>
<script src="/js/contextMenus.js"></script>
<script src="/js/fileOperations.js"></script>
<script src="/js/multiSelect.js"></script>
<script src="/js/search.js"></script>
<script src="/js/favorites.js"></script>
<script src="/js/recent.js"></script>
<script src="/js/fileRenderer.js"></script>
<script src="/js/fileSharing.js"></script>
<script src="/js/components/sharedView.js"></script>
<!-- Viewers are loaded last to ensure document.body is available -->
<script defer src="/js/fileViewer.js"></script>
<!-- Scripts (defer: download in parallel, execute in order, after HTML parsed) -->
<script defer src="/js/i18n.js"></script>
<script defer src="/js/languageSelector.js"></script>
<script defer src="/js/notifications.js"></script>
<script defer src="/js/modal.js"></script>
<script defer src="/js/ui.js"></script>
<script defer src="/js/contextMenus.js"></script>
<script defer src="/js/fileOperations.js"></script>
<script defer src="/js/multiSelect.js"></script>
<script defer src="/js/search.js"></script>
<script defer src="/js/favorites.js"></script>
<script defer src="/js/recent.js"></script>
<script defer src="/js/fileSharing.js"></script>
<script defer src="/js/components/sharedView.js"></script>
<script defer src="/js/inlineViewer.js"></script>
<script src="/js/app.js"></script>
<script defer src="/js/icons.js"></script>
<script defer src="/js/app.js"></script>
<!-- Service Worker Registration -->
<script>
+28 -88
View File
@@ -68,25 +68,10 @@ function initApp() {
// Setup event listeners
setupEventListeners();
// Initialize file renderer if available
if (window.fileRenderer) {
console.log('Using optimized file renderer');
} else {
console.log('Using standard file rendering');
}
// Check if inline viewer is initialized
if (window.inlineViewer) {
console.log('Inline viewer is available');
} else {
console.warn('Inline viewer not initialized yet, will initialize it now');
// Ensure inline viewer is initialized
if (!window.inlineViewer && typeof InlineViewer !== 'undefined') {
try {
// Create inline viewer if not already created and if the class exists
if (typeof InlineViewer !== 'undefined') {
window.inlineViewer = new InlineViewer();
} else {
console.warn('InlineViewer class is not defined, skipping initialization');
}
window.inlineViewer = new InlineViewer();
} catch (e) {
console.error('Error initializing inline viewer:', e);
}
@@ -719,7 +704,7 @@ async function loadFiles(options = {}) {
await resolveHomeFolder();
}
// Add timestamp to avoid cache
// Build the listing URL — single endpoint returns folders + files together
const timestamp = new Date().getTime();
let url;
@@ -727,7 +712,7 @@ async function loadFiles(options = {}) {
if (!app.currentPath || app.currentPath === '') {
// If at root, force user to their home folder
if (app.userHomeFolderId) {
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
app.currentPath = app.userHomeFolderId;
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
@@ -738,7 +723,7 @@ async function loadFiles(options = {}) {
}
} else {
// Normal case - viewing subfolder contents
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`;
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
console.log(`Loading subfolder content: ${app.currentPath}`);
}
@@ -762,13 +747,12 @@ async function loadFiles(options = {}) {
console.log('Forcing complete refresh ignoring cache');
}
console.log(`Loading files from ${url}`);
console.log(`Loading listing from ${url}`);
const response = await fetch(url, requestOptions);
// Critical error handling
if (response.status === 401 || response.status === 403) {
console.warn("Auth error when loading files, showing empty list");
// Just show empty state instead of causing redirect loops
elements.filesGrid.innerHTML = '<div class="empty-state"><p>Could not load files</p></div>';
elements.filesListView.innerHTML = `
<div class="list-header">
@@ -785,19 +769,22 @@ async function loadFiles(options = {}) {
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`);
}
const folders = await response.json();
// Clear existing files in both views
// Single response contains both folders and files
const listing = await response.json();
// Clear existing content in both views
if (window.multiSelect) window.multiSelect.clear();
ui._items.clear();
elements.filesGrid.innerHTML = '';
const _t = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
elements.filesListView.innerHTML = `
<div class="list-header">
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="files.modified">Modified</div>
<div data-i18n="files.name">${_t('files.name')}</div>
<div data-i18n="files.type">${_t('files.type')}</div>
<div data-i18n="files.size">${_t('files.size')}</div>
<div data-i18n="files.modified">${_t('files.modified')}</div>
</div>
`;
@@ -807,57 +794,14 @@ async function loadFiles(options = {}) {
selectAllCb.addEventListener('change', () => window.multiSelect.toggleAll());
}
// Translate the header if i18n is available
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// Render folders and files from the combined listing
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
const fileList = Array.isArray(listing.files) ? listing.files : [];
// Add folders (check if it's an array)
const folderList = Array.isArray(folders) ? folders : [];
// Backend already scopes folders to the authenticated user,
// so no client-side filtering is needed.
ui.renderFolders(folderList);
ui.renderFiles(fileList);
// Also load files in this folder
const cacheTimestamp = new Date().getTime();
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues
if (app.currentPath) {
filesUrl += `&folder_id=${app.currentPath}`;
}
console.log(`Loading files from: ${filesUrl}`);
try {
console.log(`Fetching files from: ${filesUrl}`);
const filesResponse = await fetch(filesUrl, requestOptions); // Use same auth token
console.log(`Files response status: ${filesResponse.status}`);
// Handle auth errors for files too
if (filesResponse.status === 401 || filesResponse.status === 403) {
console.warn("Auth error when loading files");
return; // Already showing folders, just stop here
}
if (filesResponse.ok) {
const files = await filesResponse.json();
console.log(`Files received:`, files);
// Add files (check if it's an array)
const fileList = Array.isArray(files) ? files : [];
console.log(`Processing ${fileList.length} files`);
ui.renderFiles(fileList);
} else {
const errorText = await filesResponse.text();
console.error(`Error loading files: ${filesResponse.status} - ${errorText}`);
}
} catch (error) {
console.error('Error loading files:', error);
// File API may not be implemented yet, so we silently ignore this error
}
// Update file icons based on file type
ui.updateFileIcons();
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
} catch (error) {
console.error('Error loading folders:', error);
ui.showNotification('Error', 'Could not load files and folders');
@@ -890,21 +834,17 @@ async function loadTrashItems() {
// Clear existing content
if (window.multiSelect) window.multiSelect.clear();
elements.filesGrid.innerHTML = '';
const _tt = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
elements.filesListView.innerHTML = `
<div class="list-header trash-header">
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="trash.original_location">Original location</div>
<div data-i18n="trash.deleted_date">Deletion date</div>
<div data-i18n="trash.actions">Actions</div>
<div data-i18n="files.name">${_tt('files.name')}</div>
<div data-i18n="files.type">${_tt('files.type')}</div>
<div data-i18n="trash.original_location">${_tt('trash.original_location')}</div>
<div data-i18n="trash.deleted_date">${_tt('trash.deleted_date')}</div>
<div data-i18n="trash.actions">${_tt('trash.actions')}</div>
</div>
`;
// Translate the header if i18n is available
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// Update breadcrumb - just show Home
ui.updateBreadcrumb('');
@@ -1150,7 +1090,7 @@ window.formatDateShort = function formatDateShort(value) {
/**
* Centralised "is this MIME type text-viewable?" check.
* Used by fileViewer, inlineViewer, and ui.isViewableFile.
* Used by inlineViewer and ui.isViewableFile.
*/
window.isTextViewable = function isTextViewable(mimeType) {
if (!mimeType) return false;
+1 -4
View File
@@ -286,10 +286,7 @@ function initLanguageSelector() {
await configureOidcLoginUI();
}
// Translate the page with new locale
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// setLocale() already calls translatePage() internally
});
}
+2 -2
View File
@@ -180,8 +180,8 @@ const sharedView = {
if (filesGrid) filesGrid.style.display = 'none';
if (filesListView) filesListView.style.display = 'none';
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
if (window.i18n && window.i18n.translateElement) {
window.i18n.translateElement(container);
}
},
+3 -5
View File
@@ -92,8 +92,6 @@ const contextMenus = {
// Open with inline viewer
if (window.inlineViewer) {
window.inlineViewer.openFile(fileDetails);
} else if (window.fileViewer) {
window.fileViewer.open(fileDetails);
} else {
// If no viewer is available, download directly
window.fileOps.downloadFile(file.id, file.name);
@@ -468,9 +466,9 @@ const contextMenus = {
window.app.selectedTargetFolderId = "";
});
// Translate new elements
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
// Translate new elements (scoped to container)
if (window.i18n && window.i18n.translateElement) {
window.i18n.translateElement(folderSelectContainer);
}
}
} catch (error) {
+24 -162
View File
@@ -185,8 +185,8 @@ const favorites = {
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header favorites-header">
<div></div>
<div class="list-header">
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
@@ -208,177 +208,39 @@ const favorites = {
return;
}
const folders = [];
const files = [];
for (const item of this._cache.values()) {
if (item.item_type === 'folder') {
this._renderFolder(item, filesGrid, filesListView);
folders.push({
id: item.item_id,
name: item.item_name || item.item_id,
parent_id: item.parent_id || '',
modified_at: item.modified_at || item.created_at
});
} else {
this._renderFile(item, filesGrid, filesListView);
files.push({
id: item.item_id,
name: item.item_name || item.item_id,
folder_id: item.parent_id || '',
mime_type: item.item_mime_type,
icon_class: item.icon_class,
icon_special_class: item.icon_special_class,
category: item.category,
size: item.item_size || 0,
size_formatted: item.size_formatted,
modified_at: item.modified_at || item.created_at
});
}
}
window.ui.updateFileIcons();
if (folders.length) window.ui.renderFolders(folders);
if (files.length) window.ui.renderFiles(files);
} catch (error) {
console.error('Error displaying favorites:', error);
if (window.ui && window.ui.showNotification) {
window.ui.showNotification('Error', 'Error loading favorite items');
}
}
},
// ───────────────────── renderers ─────────────────────
_renderFolder(item, filesGrid, filesListView) {
const name = item.item_name || item.item_id || 'Unknown';
const folderId = item.item_id;
const parentId = item.parent_id || '';
const formattedDate = window.formatDateTime(item.modified_at || item.created_at);
// --- grid element ---
const gridEl = document.createElement('div');
gridEl.className = 'file-card favorite-item';
gridEl.dataset.folderId = folderId;
gridEl.dataset.folderName = name;
gridEl.dataset.parentId = parentId;
gridEl.innerHTML = `
<div class="favorite-indicator active"><i class="fas fa-star"></i></div>
<div class="file-icon folder-icon"><i class="fas fa-folder"></i></div>
<div class="file-name">${escapeHtml(name)}</div>
<div class="file-info">Folder</div>
`;
gridEl.addEventListener('click', () => {
window.app.currentPath = folderId;
window.ui.updateBreadcrumb(name);
window.loadFiles();
});
gridEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.app.contextMenuTargetFolder = { id: folderId, name, parent_id: parentId };
const cm = document.getElementById('folder-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
});
filesGrid.appendChild(gridEl);
// --- list element ---
const listEl = document.createElement('div');
listEl.className = 'file-item favorite-item';
listEl.dataset.folderId = folderId;
listEl.dataset.folderName = name;
listEl.dataset.parentId = parentId;
listEl.innerHTML = `
<div class="favorite-indicator active"><i class="fas fa-star"></i></div>
<div class="name-cell">
<div class="file-icon folder-icon"><i class="fas fa-folder"></i></div>
<span>${escapeHtml(name)}</span>
</div>
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
<div class="size-cell">--</div>
<div class="date-cell">${formattedDate}</div>
`;
listEl.addEventListener('click', () => {
window.app.currentPath = folderId;
window.ui.updateBreadcrumb(name);
window.loadFiles();
});
listEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.app.contextMenuTargetFolder = { id: folderId, name, parent_id: parentId };
const cm = document.getElementById('folder-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
});
filesListView.appendChild(listEl);
},
_renderFile(item, filesGrid, filesListView) {
const name = item.item_name || item.item_id || 'Unknown';
const fileId = item.item_id;
const folderId = item.parent_id || '';
const mimeType = item.item_mime_type || 'application/octet-stream';
// Build a minimal file object for click handlers
const fileObj = {
id: fileId,
name,
folder_id: folderId,
mime_type: mimeType,
size: item.item_size || 0
};
// Use pre-computed display fields from the enriched API response
const iconClass = item.icon_class || 'fas fa-file';
const iconSpecialClass = item.icon_special_class || '';
const typeLabel = item.category
? (window.i18n ? window.i18n.t(`files.file_types.${item.category.toLowerCase()}`) || item.category : item.category)
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
const fileSize = item.size_formatted || (window.formatFileSize ? window.formatFileSize(item.item_size || 0) : '0 B');
const formattedDate = window.formatDateTime(item.modified_at || item.created_at);
// --- grid element ---
const gridEl = document.createElement('div');
gridEl.className = 'file-card favorite-item';
gridEl.dataset.fileId = fileId;
gridEl.dataset.fileName = name;
gridEl.dataset.folderId = folderId;
gridEl.innerHTML = `
<div class="favorite-indicator active"><i class="fas fa-star"></i></div>
<div class="file-icon ${iconSpecialClass}"><i class="${iconClass}"></i></div>
<div class="file-name">${escapeHtml(name)}</div>
<div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
`;
gridEl.addEventListener('click', () => {
if (window.ui && window.ui.isViewableFile(fileObj) && window.inlineViewer) {
window.inlineViewer.openFile(fileObj);
} else if (window.fileOps) {
window.fileOps.downloadFile(fileId, name);
}
});
gridEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.app.contextMenuTargetFile = { id: fileId, name, folder_id: folderId };
const cm = document.getElementById('file-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
});
filesGrid.appendChild(gridEl);
// --- list element ---
const listEl = document.createElement('div');
listEl.className = 'file-item favorite-item';
listEl.dataset.fileId = fileId;
listEl.dataset.fileName = name;
listEl.dataset.folderId = folderId;
listEl.innerHTML = `
<div class="favorite-indicator active"><i class="fas fa-star"></i></div>
<div class="name-cell">
<div class="file-icon ${iconSpecialClass}"><i class="${iconClass}"></i></div>
<span>${escapeHtml(name)}</span>
</div>
<div class="type-cell">${escapeHtml(typeLabel)}</div>
<div class="size-cell">${fileSize}</div>
<div class="date-cell">${formattedDate}</div>
`;
listEl.addEventListener('click', () => {
if (window.ui && window.ui.isViewableFile(fileObj) && window.inlineViewer) {
window.inlineViewer.openFile(fileObj);
} else if (window.fileOps) {
window.fileOps.downloadFile(fileId, name);
}
});
listEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.app.contextMenuTargetFile = { id: fileId, name, folder_id: folderId };
const cm = document.getElementById('file-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
});
filesListView.appendChild(listEl);
}
};
+10 -24
View File
@@ -173,9 +173,6 @@ const fileOps = {
// All done
this._finishUploadToast(successCount, totalFiles);
// Wait for backend to persist, then reload
await new Promise(resolve => setTimeout(resolve, 800));
// Refresh storage usage display
if (typeof window.refreshUserData === 'function') {
try { await window.refreshUserData(); } catch (_) {}
@@ -187,11 +184,9 @@ const fileOps = {
console.error('Error reloading files:', reloadError);
}
setTimeout(() => {
const dropzone = document.getElementById('dropzone');
if (dropzone) dropzone.style.display = 'none';
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
}, 500);
const dropzone = document.getElementById('dropzone');
if (dropzone) dropzone.style.display = 'none';
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
} finally {
this._isUploading = false;
}
@@ -317,25 +312,21 @@ const fileOps = {
// Finish
this._finishUploadToast(successCount, totalFiles);
await new Promise(resolve => setTimeout(resolve, 800));
// Refresh storage usage display
if (typeof window.refreshUserData === 'function') {
try { await window.refreshUserData(); } catch (_) {}
}
try {
await window.loadFiles({ forceRefresh: true });
} catch (reloadError) {
console.error('Error reloading files:', reloadError);
}
setTimeout(() => {
const dropzone = document.getElementById('dropzone');
if (dropzone) dropzone.style.display = 'none';
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
}, 500);
const dropzone = document.getElementById('dropzone');
if (dropzone) dropzone.style.display = 'none';
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
},
/**
@@ -365,15 +356,10 @@ const fileOps = {
const folder = await response.json();
console.log('Folder created successfully:', folder);
// Add the folder to the view immediately for instant feedback
// Optimistic UI: add folder card directly from server response
// — no reload needed since the backend already confirmed creation.
window.ui.addFolderToView(folder);
// Wait to allow the backend to save the changes
await new Promise(resolve => setTimeout(resolve, 1000));
// Reload files to refresh the view
await window.loadFiles({forceRefresh: true});
window.ui.showNotification('Folder created', `"${name}" created successfully`);
} else {
const errorData = await response.text();
-503
View File
@@ -1,503 +0,0 @@
/**
* OxiCloud File Renderer Module
* Optimized rendering for large file lists using virtual rendering
*/
// Configuration
const ITEMS_PER_PAGE = 100; // Number of items to render at once
const ROW_HEIGHT = 80; // Estimated row height for list view
const CARD_HEIGHT = 180; // Estimated height for grid view card
const CARD_WIDTH = 180; // Estimated width for grid view card
class FileRenderer {
constructor() {
this.files = [];
this.folders = [];
this.currentView = 'grid';
this.gridContainer = document.getElementById('files-grid');
this.listContainer = document.getElementById('files-list-view');
this.visibleItems = {};
this.offsetY = 0;
this.totalHeight = 0;
this.gridColumns = 0;
this.i18n = window.i18n || { t: key => key };
// Setup intersection observer for lazy loading
this.setupIntersectionObserver();
// Handle scroll event for virtual scrolling
this.handleScroll = this.handleScroll.bind(this);
this.setupScrollListeners();
}
/**
* Set up intersection observer for lazy loading
*/
setupIntersectionObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const elem = entry.target;
if (elem.dataset.lazySrc) {
elem.src = elem.dataset.lazySrc;
delete elem.dataset.lazySrc;
this.observer.unobserve(elem);
}
}
});
}, {
rootMargin: '200px', // Load images 200px before they come into view
});
}
/**
* Setup scroll listeners for virtual scrolling
*/
setupScrollListeners() {
const container = document.querySelector('.files-list');
if (container) {
container.addEventListener('scroll', this.handleScroll);
// Also update on resize
window.addEventListener('resize', this.updateVisibleItems.bind(this));
}
}
/**
* Handle scroll events for virtual scrolling
*/
handleScroll() {
const container = document.querySelector('.files-list');
if (!container) return;
this.offsetY = container.scrollTop;
requestAnimationFrame(() => this.updateVisibleItems());
}
/**
* Calculate how many items are visible and render only those
*/
updateVisibleItems() {
if (!this.files || !this.folders) return;
const container = document.querySelector('.files-list');
if (!container) return;
const viewportHeight = container.clientHeight;
const viewportWidth = container.clientWidth;
// Calculate grid columns based on container width and card width
this.gridColumns = Math.floor(viewportWidth / CARD_WIDTH);
if (this.gridColumns < 1) this.gridColumns = 1;
if (this.currentView === 'grid') {
this.updateGridView(viewportHeight);
} else {
this.updateListView(viewportHeight);
}
}
/**
* Update grid view with virtual scrolling
*/
updateGridView(viewportHeight) {
const allItems = [...this.folders, ...this.files];
const rows = Math.ceil(allItems.length / this.gridColumns);
this.totalHeight = rows * CARD_HEIGHT;
// Calculate visible range
const startRow = Math.floor(this.offsetY / CARD_HEIGHT);
const visibleRows = Math.ceil(viewportHeight / CARD_HEIGHT) + 1; // +1 for partial rows
const startIdx = startRow * this.gridColumns;
const endIdx = Math.min(allItems.length, (startRow + visibleRows) * this.gridColumns);
// Generate a map of visible items
const newVisibleItems = {};
for (let i = startIdx; i < endIdx; i++) {
newVisibleItems[i] = true;
}
// Remove items that are no longer visible
if (this.gridContainer) {
const children = Array.from(this.gridContainer.children);
children.forEach(child => {
const idx = parseInt(child.dataset.index, 10);
if (!newVisibleItems[idx]) {
this.gridContainer.removeChild(child);
} else {
// Item is still visible, remove from new items list
delete newVisibleItems[idx];
}
});
}
// Add new visible items
const fragment = document.createDocumentFragment();
Object.keys(newVisibleItems).forEach(idx => {
const i = parseInt(idx, 10);
if (i < allItems.length) {
const item = allItems[i];
const elem = this.renderGridItem(item, i);
fragment.appendChild(elem);
}
});
if (this.gridContainer) {
this.gridContainer.appendChild(fragment);
}
this.visibleItems = { ...this.visibleItems, ...newVisibleItems };
}
/**
* Update list view with virtual scrolling
*/
updateListView(viewportHeight) {
const allItems = [...this.folders, ...this.files];
this.totalHeight = allItems.length * ROW_HEIGHT;
// Calculate visible range
const startIdx = Math.floor(this.offsetY / ROW_HEIGHT);
const visibleCount = Math.ceil(viewportHeight / ROW_HEIGHT) + 1; // +1 for partial rows
const endIdx = Math.min(allItems.length, startIdx + visibleCount);
// Generate a map of visible items
const newVisibleItems = {};
for (let i = startIdx; i < endIdx; i++) {
newVisibleItems[i] = true;
}
// Remove items that are no longer visible
if (this.listContainer) {
const children = Array.from(this.listContainer.children);
// Skip the first child as it's the header
for (let i = 1; i < children.length; i++) {
const child = children[i];
const idx = parseInt(child.dataset.index, 10);
if (!newVisibleItems[idx]) {
this.listContainer.removeChild(child);
} else {
// Item is still visible, remove from new items list
delete newVisibleItems[idx];
}
}
}
// Add new visible items
const fragment = document.createDocumentFragment();
Object.keys(newVisibleItems).forEach(idx => {
const i = parseInt(idx, 10);
if (i < allItems.length) {
const item = allItems[i];
const elem = this.renderListItem(item, i);
fragment.appendChild(elem);
}
});
if (this.listContainer) {
this.listContainer.appendChild(fragment);
}
this.visibleItems = { ...this.visibleItems, ...newVisibleItems };
}
/**
* Render a single grid item (file or folder)
*/
renderGridItem(item, index) {
const elem = document.createElement('div');
elem.className = 'file-card';
elem.dataset.index = index;
const isFolder = 'parent_id' in item; // Folders have parent_id
if (isFolder) {
elem.dataset.folderId = item.id;
elem.dataset.folderName = item.name;
elem.dataset.parentId = item.parent_id || "";
elem.innerHTML = `
<div class="file-icon ${item.icon_special_class || 'folder-icon'}">
<i class="${item.icon_class || 'fas fa-folder'}"></i>
</div>
<div class="file-name">${window.escapeHtml(item.name)}</div>
`;
// Make draggable
if (item.parent_id) {
elem.setAttribute('draggable', 'true');
elem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', item.id);
e.dataTransfer.setData('application/oxicloud-folder', 'true');
elem.classList.add('dragging');
});
elem.addEventListener('dragend', () => {
elem.classList.remove('dragging');
document.querySelectorAll('.drop-target').forEach(el => {
el.classList.remove('drop-target');
});
});
}
// Click event
elem.addEventListener('click', () => {
if (typeof window.selectFolder === 'function') {
window.selectFolder(item.id, item.name);
}
});
} else {
// File card
elem.dataset.fileId = item.id;
elem.dataset.fileName = item.name;
elem.dataset.folderId = item.folder_id || "";
// Use pre-computed icon class from the API response
const iconClass = item.icon_class || 'fas fa-file';
elem.innerHTML = `
<div class="file-icon">
<i class="${iconClass}"></i>
</div>
<div class="file-name">${window.escapeHtml(item.name)}</div>
`;
// Make draggable
elem.setAttribute('draggable', 'true');
elem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', item.id);
elem.classList.add('dragging');
});
elem.addEventListener('dragend', () => {
elem.classList.remove('dragging');
document.querySelectorAll('.drop-target').forEach(el => {
el.classList.remove('drop-target');
});
});
// Click event (preview)
elem.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
console.log('File clicked:', item);
// Open file in our new inline viewer
if (window.inlineViewer) {
console.log('Using inline viewer');
window.inlineViewer.openFile(item);
} else {
console.warn('Inline viewer not available, downloading directly');
// Fallback to authenticated download if viewer is not available
if (window.fileOps) {
window.fileOps.downloadFile(item.id, item.name);
}
}
});
}
// Add to intersection observer (for future thumbnail support)
this.observer.observe(elem);
return elem;
}
/**
* Render a single list item (file or folder)
*/
renderListItem(item, index) {
const elem = document.createElement('div');
elem.className = 'file-item';
elem.dataset.index = index;
const isFolder = 'parent_id' in item; // Folders have parent_id
if (isFolder) {
elem.dataset.folderId = item.id;
elem.dataset.folderName = item.name;
elem.dataset.parentId = item.parent_id || "";
const formattedDate = window.formatDateTime(item.modified_at);
elem.innerHTML = `
<div class="name-cell">
<div class="file-icon ${item.icon_special_class || 'folder-icon'}">
<i class="${item.icon_class || 'fas fa-folder'}"></i>
</div>
<span>${window.escapeHtml(item.name)}</span>
</div>
<div>${item.category ? (this.i18n.t(`files.file_types.${item.category.toLowerCase()}`) || item.category) : this.i18n.t('files.file_types.folder')}</div>
<div>--</div>
<div>${formattedDate}</div>
`;
// Make draggable
if (item.parent_id) {
elem.setAttribute('draggable', 'true');
elem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', item.id);
e.dataTransfer.setData('application/oxicloud-folder', 'true');
elem.classList.add('dragging');
});
elem.addEventListener('dragend', () => {
elem.classList.remove('dragging');
document.querySelectorAll('.drop-target').forEach(el => {
el.classList.remove('drop-target');
});
});
}
// Click event
elem.addEventListener('click', () => {
if (typeof window.selectFolder === 'function') {
window.selectFolder(item.id, item.name);
}
});
} else {
// File item
elem.dataset.fileId = item.id;
elem.dataset.fileName = item.name;
elem.dataset.folderId = item.folder_id || "";
// Use pre-computed display fields from the API response
const iconClass = item.icon_class || 'fas fa-file';
const typeLabel = item.category
? (this.i18n.t(`files.file_types.${item.category.toLowerCase()}`) || item.category)
: this.i18n.t('files.file_types.document');
// Format file size and date
const fileSize = item.size_formatted || this.formatFileSize(item.size);
const formattedDate = window.formatDateTime(item.modified_at);
elem.innerHTML = `
<div class="name-cell">
<div class="file-icon">
<i class="${iconClass}"></i>
</div>
<span>${window.escapeHtml(item.name)}</span>
</div>
<div>${typeLabel}</div>
<div>${fileSize}</div>
<div>${formattedDate}</div>
`;
// Make draggable
elem.setAttribute('draggable', 'true');
elem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', item.id);
elem.classList.add('dragging');
});
elem.addEventListener('dragend', () => {
elem.classList.remove('dragging');
document.querySelectorAll('.drop-target').forEach(el => {
el.classList.remove('drop-target');
});
});
// Click event (preview)
elem.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
console.log('File clicked:', item);
// Open file in our new inline viewer
if (window.inlineViewer) {
console.log('Using inline viewer');
window.inlineViewer.openFile(item);
} else {
console.warn('Inline viewer not available, downloading directly');
// Fallback to authenticated download if viewer is not available
if (window.fileOps) {
window.fileOps.downloadFile(item.id, item.name);
}
}
});
}
return elem;
}
/**
* Format file size — delegates to the single global definition in app.js
*/
formatFileSize(bytes) {
return window.formatFileSize ? window.formatFileSize(bytes) : `${bytes} Bytes`;
}
/**
* Set current view mode (grid or list)
*/
setView(view) {
this.currentView = view;
this.updateVisibleItems();
}
/**
* Load and render files and folders
*/
loadData(folders, files) {
this.folders = folders || [];
this.files = files || [];
// Reset containers
if (this.gridContainer) {
this.gridContainer.innerHTML = '';
}
if (this.listContainer) {
// Preserve the header
const header = this.listContainer.querySelector('.list-header');
this.listContainer.innerHTML = '';
if (header) {
this.listContainer.appendChild(header);
}
}
this.visibleItems = {};
this.updateVisibleItems();
}
}
// Create the file renderer when the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.fileRenderer = new FileRenderer();
// Expose the selectFolder function for navigation
window.selectFolder = (id, name) => {
// Update current path
window.currentPath = id;
// Update breadcrumb
const breadcrumb = document.querySelector('.breadcrumb');
if (breadcrumb) {
const home = breadcrumb.querySelector('.breadcrumb-item');
breadcrumb.innerHTML = '';
if (home) {
breadcrumb.appendChild(home);
if (name) {
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 = name;
breadcrumb.appendChild(folderItem);
}
}
}
// Load files for this folder
window.loadFiles();
};
});
+9 -4
View File
@@ -66,15 +66,20 @@ const fileSharing = {
},
/**
* Get shared links for a specific item
* Get shared links for a specific item (server-side filtered)
* @param {string} itemId
* @param {string} itemType - 'file' or 'folder'
* @returns {Promise<Array>} Filtered shares
* @returns {Promise<Array>} Shares for this item
*/
async getSharedLinksForItem(itemId, itemType) {
try {
const all = await this.getSharedLinks();
return all.filter(s => s.item_id === itemId && s.item_type === itemType);
const params = new URLSearchParams({ item_id: itemId, item_type: itemType });
const res = await fetch(`/api/shares?${params}`, {
headers: this._headers(false)
});
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : (data.items || []);
} catch (error) {
console.error('Error getting shared links for item:', error);
return [];
-413
View File
@@ -1,413 +0,0 @@
/**
* OxiCloud File Viewer Module
* Provides integrated viewers for images, PDFs and other file types
*/
class FileViewer {
constructor() {
this.viewerContainer = null;
this.fileData = null;
this.isOpen = false;
// Create the viewer container on initialization
this.createViewerContainer();
}
/**
* Create the viewer container DOM element
*/
createViewerContainer() {
console.log('Creating file viewer container');
// Check if container already exists
if (document.getElementById('file-viewer-container')) {
console.log('Viewer container already exists');
this.viewerContainer = document.getElementById('file-viewer-container');
return;
}
// Create viewer container
this.viewerContainer = document.createElement('div');
this.viewerContainer.id = 'file-viewer-container';
this.viewerContainer.className = 'file-viewer-container';
// Create viewer content
const viewerContent = document.createElement('div');
viewerContent.className = 'file-viewer-content';
// Create header
const header = document.createElement('div');
header.className = 'file-viewer-header';
const title = document.createElement('div');
title.className = 'file-viewer-title';
title.textContent = 'File Viewer';
header.appendChild(title);
const closeBtn = document.createElement('button');
closeBtn.className = 'file-viewer-close';
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
closeBtn.addEventListener('click', () => {
console.log('Close button clicked');
this.close();
});
header.appendChild(closeBtn);
// Create viewer area
const viewerArea = document.createElement('div');
viewerArea.className = 'file-viewer-area';
// Create toolbar
const toolbar = document.createElement('div');
toolbar.className = 'file-viewer-toolbar';
const downloadBtn = document.createElement('button');
downloadBtn.className = 'file-viewer-download';
downloadBtn.innerHTML = '<i class="fas fa-download"></i>';
downloadBtn.addEventListener('click', () => this.downloadFile());
toolbar.appendChild(downloadBtn);
// Assemble the viewer
viewerContent.appendChild(header);
viewerContent.appendChild(viewerArea);
viewerContent.appendChild(toolbar);
this.viewerContainer.appendChild(viewerContent);
// Add to the document
if (document.body) {
console.log('Adding viewer container to body');
document.body.appendChild(this.viewerContainer);
} else {
console.warn('Document body not ready, will add viewer later');
// Try to add it after document is ready
setTimeout(() => {
if (document.body) {
console.log('Adding viewer container to body (delayed)');
document.body.appendChild(this.viewerContainer);
} else {
console.error('Document body still not available after delay');
}
}, 500);
}
// Add event listeners for keyboard navigation
document.addEventListener('keydown', (e) => {
if (this.isOpen && e.key === 'Escape') {
console.log('Escape key pressed, closing viewer');
this.close();
}
});
console.log('Viewer container created');
}
/**
* Open the viewer with the specified file
* @param {Object} fileData - File data with id, name, mime_type
*/
async open(fileData) {
console.log('FileViewer: Opening file', fileData);
this.fileData = fileData;
this.isOpen = true;
// Reset the viewer area
const viewerArea = this.viewerContainer.querySelector('.file-viewer-area');
viewerArea.innerHTML = '';
// Set the title
const title = this.viewerContainer.querySelector('.file-viewer-title');
title.textContent = fileData.name;
// Show the container
this.viewerContainer.classList.add('active');
// Determine content type and load appropriate viewer
if (fileData.mime_type && fileData.mime_type.startsWith('image/')) {
console.log('FileViewer: Loading image viewer');
this.loadImageViewer(fileData.id, viewerArea);
} else if (fileData.mime_type && fileData.mime_type === 'application/pdf') {
console.log('FileViewer: Loading PDF viewer');
this.loadPdfViewer(fileData.id, viewerArea);
} else if (fileData.mime_type && this.isTextViewable(fileData.mime_type)) {
console.log('FileViewer: Loading text viewer');
this.loadTextViewer(fileData.id, viewerArea);
} else {
console.log('FileViewer: Unsupported file type', fileData.mime_type);
// For unsupported files, show download prompt
this.showUnsupportedFileMessage(viewerArea);
}
}
/**
* Load the image viewer
* @param {string} fileId - ID of the file to view
* @param {HTMLElement} container - Container element to render into
*/
loadImageViewer(fileId, container) {
// Create loader
const loader = document.createElement('div');
loader.className = 'file-viewer-loader';
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
container.appendChild(loader);
// Fetch image with auth header and create blob URL
this.fetchFileAsBlob(fileId).then(blob => {
const blobUrl = URL.createObjectURL(blob);
this.currentBlobUrl = blobUrl;
const img = document.createElement('img');
img.className = 'file-viewer-image';
img.src = blobUrl;
img.alt = this.fileData.name;
img.onload = () => {
if (loader.parentNode) container.removeChild(loader);
};
img.onerror = () => {
if (loader.parentNode) container.removeChild(loader);
this.showErrorMessage(container);
};
container.appendChild(img);
}).catch(error => {
console.error('Error loading image:', error);
if (loader.parentNode) container.removeChild(loader);
this.showErrorMessage(container);
});
// Add zoom controls to toolbar
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
const zoomInBtn = document.createElement('button');
zoomInBtn.className = 'file-viewer-zoom-in';
zoomInBtn.innerHTML = '<i class="fas fa-search-plus"></i>';
zoomInBtn.addEventListener('click', () => this.zoomImage(1.2));
toolbar.appendChild(zoomInBtn);
const zoomOutBtn = document.createElement('button');
zoomOutBtn.className = 'file-viewer-zoom-out';
zoomOutBtn.innerHTML = '<i class="fas fa-search-minus"></i>';
zoomOutBtn.addEventListener('click', () => this.zoomImage(0.8));
toolbar.appendChild(zoomOutBtn);
const resetZoomBtn = document.createElement('button');
resetZoomBtn.className = 'file-viewer-zoom-reset';
resetZoomBtn.innerHTML = '<i class="fas fa-expand"></i>';
resetZoomBtn.addEventListener('click', () => this.resetZoom());
toolbar.appendChild(resetZoomBtn);
}
/**
* Zoom the image
* @param {number} factor - Zoom factor
*/
zoomImage(factor) {
const img = this.viewerContainer.querySelector('.file-viewer-image');
if (!img) return;
// Get current scale
let scale = img.style.transform ?
parseFloat(img.style.transform.replace('scale(', '').replace(')', '')) : 1;
// Apply new scale
scale *= factor;
// Limit scale range
scale = Math.max(0.5, Math.min(5, scale));
img.style.transform = `scale(${scale})`;
}
/**
* Reset image zoom
*/
resetZoom() {
const img = this.viewerContainer.querySelector('.file-viewer-image');
if (!img) return;
img.style.transform = 'scale(1)';
}
/**
* Load the PDF viewer
* @param {string} fileId - ID of the file to view
* @param {HTMLElement} container - Container element to render into
*/
loadPdfViewer(fileId, container) {
// Create loader
const loader = document.createElement('div');
loader.className = 'file-viewer-loader';
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
container.appendChild(loader);
// Fetch PDF with auth header and create blob URL
this.fetchFileAsBlob(fileId).then(blob => {
const blobUrl = URL.createObjectURL(blob);
this.currentBlobUrl = blobUrl;
const iframe = document.createElement('iframe');
iframe.className = 'file-viewer-pdf';
iframe.src = blobUrl;
iframe.title = this.fileData.name;
iframe.onload = () => {
if (loader.parentNode) container.removeChild(loader);
};
container.appendChild(iframe);
}).catch(error => {
console.error('Error loading PDF:', error);
if (loader.parentNode) container.removeChild(loader);
this.showErrorMessage(container);
});
}
/**
* Load the text viewer
*/
async loadTextViewer(fileId, container) {
const loader = document.createElement('div');
loader.className = 'file-viewer-loader';
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
container.appendChild(loader);
try {
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await response.text();
if (loader.parentNode) container.removeChild(loader);
const pre = document.createElement('pre');
pre.className = 'file-viewer-text-content';
pre.textContent = text;
container.appendChild(pre);
} catch (error) {
console.error('Error loading text:', error);
if (loader.parentNode) container.removeChild(loader);
this.showErrorMessage(container);
}
}
/**
* Check if a MIME type is text-viewable — delegates to the single global
* definition exposed by app.js (window.isTextViewable).
*/
isTextViewable(mimeType) {
return window.isTextViewable ? window.isTextViewable(mimeType) : false;
}
/**
* Fetch a file as blob with auth headers
*/
async fetchFileAsBlob(fileId) {
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.blob();
}
/**
* Show message for unsupported file types
* @param {HTMLElement} container - Container element to render into
*/
showUnsupportedFileMessage(container) {
const message = document.createElement('div');
message.className = 'file-viewer-unsupported';
message.innerHTML = `
<i class="fas fa-file-download"></i>
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'This file type cannot be previewed.'}</p>
<button class="btn btn-primary download-btn">
<i class="fas fa-download"></i>
${window.i18n ? window.i18n.t('viewer.download_file') : 'Download file'}
</button>
`;
// Add download button click event
message.querySelector('.download-btn').addEventListener('click', () => {
this.downloadFile();
});
container.appendChild(message);
}
/**
* Download the current file
*/
downloadFile() {
if (!this.fileData) return;
// Download with auth headers
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
fetch(`/api/files/${this.fileData.id}`, { headers })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = this.fileData.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
})
.catch(err => console.error('Download error:', err));
}
/**
* Show error message
*/
showErrorMessage(container) {
const message = document.createElement('div');
message.className = 'file-viewer-unsupported';
message.innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
<p>Error loading the file. Try downloading it directly.</p>
`;
container.appendChild(message);
}
/**
* Close the viewer
*/
close() {
this.isOpen = false;
this.fileData = null;
this.viewerContainer.classList.remove('active');
// Clean up blob URL if exists
if (this.currentBlobUrl) {
URL.revokeObjectURL(this.currentBlobUrl);
this.currentBlobUrl = null;
}
// Reset toolbar (remove zoom controls)
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
const downloadBtn = toolbar.querySelector('.file-viewer-download');
toolbar.innerHTML = '';
toolbar.appendChild(downloadBtn);
}
}
// Create the file viewer immediately and make it accessible globally
window.fileViewer = new FileViewer();
// Ensure the file viewer is available when the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
console.log('FileViewer initialized:', window.fileViewer ? 'Yes' : 'No');
if (!window.fileViewer) {
console.warn('Re-initializing fileViewer as it was not properly set');
window.fileViewer = new FileViewer();
}
});
+14 -3
View File
@@ -207,17 +207,27 @@ async function initI18n() {
* Translate all elements with data-i18n attribute
*/
function translatePage() {
document.querySelectorAll('[data-i18n]').forEach(element => {
translateElement(document);
}
/**
* Translate only elements within a given root (scoped).
* Use this instead of translatePage() when you know which container changed.
* @param {Element|Document} root - The root element to search within
*/
function translateElement(root) {
const el = root || document;
el.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
element.textContent = t(key);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
el.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = t(key);
});
document.querySelectorAll('[data-i18n-title]').forEach(element => {
el.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
element.title = t(key);
});
@@ -267,5 +277,6 @@ window.i18n = {
getCurrentLocale,
getSupportedLocales,
translatePage,
translateElement,
isLoaded: () => translationsLoaded
};
+232
View File
@@ -0,0 +1,232 @@
/**
* OxiCloud – SVG Icon System
*
* Replaces Font Awesome webfont with inline SVGs for:
* - Zero external CDN dependency (privacy, offline support)
* - ~96% size reduction (~12 KB vs ~300 KB)
* - Instant rendering (no FOIT / layout shift)
*
* Icon paths extracted from Font Awesome Free 6.7.2 (CC BY 4.0 License).
* https://fontawesome.com/license/free
*/
"use strict";
// ── Icon registry ──────────────────────────────────────────────
// Each entry: [viewBox-width, svg-path-d]
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
// Keys use FA5 class names (without "fa-" prefix) for backward compatibility.
const _ICONS = {
"arrow-left": [448, "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"],
"arrows-alt": [512, "M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 96-96 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6l0-32 96 0 0 96-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-96 96 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 32-96 0 0-96 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z"],
"bell": [448, "M224 0c-17.7 0-32 14.3-32 32l0 19.2C119 66 64 130.6 64 208l0 18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416l384 0c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8l0-18.8c0-77.4-55-142-128-156.8L256 32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3l-64 0-64 0c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"],
"bell-slash": [640, "M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7l-90.2-70.7c.2-.4 .4-.9 .6-1.3c5.2-11.5 3.1-25-5.3-34.4l-7.4-8.3C497.3 319.2 480 273.9 480 226.8l0-18.8c0-77.4-55-142-128-156.8L352 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 19.2c-42.6 8.6-79 34.2-102 69.3L38.8 5.1zM406.2 416L160 222.1l0 4.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S115.4 416 128 416l278.2 0zm-40.9 77.3c12-12 18.7-28.3 18.7-45.3l-64 0-64 0c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"],
"box": [448, "M50.7 58.5L0 160l208 0 0-128L93.7 32C75.5 32 58.9 42.3 50.7 58.5zM240 160l208 0L397.3 58.5C389.1 42.3 372.5 32 354.3 32L240 32l0 128zm208 32L0 192 0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-224z"],
"broom": [576, "M566.6 54.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192-34.7-34.7c-4.2-4.2-10-6.6-16-6.6c-12.5 0-22.6 10.1-22.6 22.6l0 29.1L364.3 320l29.1 0c12.5 0 22.6-10.1 22.6-22.6c0-6-2.4-11.8-6.6-16l-34.7-34.7 192-192zM341.1 353.4L222.6 234.9c-42.7-3.7-85.2 11.7-115.8 42.3l-8 8C76.5 307.5 64 337.7 64 369.2c0 6.8 7.1 11.2 13.2 8.2l51.1-25.5c5-2.5 9.5 4.1 5.4 7.9L7.3 473.4C2.7 477.6 0 483.6 0 489.9C0 502.1 9.9 512 22.1 512l173.3 0c38.8 0 75.9-15.4 103.4-42.8c30.6-30.6 45.9-73.1 42.3-115.8z"],
"caret-down": [320, "M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"],
"chart-pie": [576, "M304 240l0-223.4c0-9 7-16.6 16-16.6C443.7 0 544 100.3 544 224c0 9-7.6 16-16.6 16L304 240zM32 272C32 150.7 122.1 50.3 239 34.3c9.2-1.3 17 6.1 17 15.4L256 288 412.5 444.5c6.7 6.7 6.2 17.7-1.5 23.1C371.8 495.6 323.8 512 272 512C139.5 512 32 404.6 32 272zm526.4 16c9.3 0 16.6 7.8 15.4 17c-7.7 55.9-34.6 105.6-73.9 142.3c-6 5.6-15.4 5.2-21.2-.7L320 288l238.4 0z"],
"check": [448, "M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"],
"check-circle": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"],
"check-double": [448, "M342.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 178.7l-57.4-57.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l80 80c12.5 12.5 32.8 12.5 45.3 0l160-160zm96 128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 402.7 54.6 297.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l256-256z"],
"chevron-down": [512, "M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"],
"chevron-left": [320, "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"],
"chevron-right": [320, "M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"],
"circle-notch": [512, "M222.7 32.1c5 16.9-4.6 34.8-21.5 39.8C121.8 95.6 64 169.1 64 256c0 106 86 192 192 192s192-86 192-192c0-86.9-57.8-160.4-137.1-184.1c-16.9-5-26.6-22.9-21.5-39.8s22.9-26.6 39.8-21.5C434.9 42.1 512 140 512 256c0 141.4-114.6 256-256 256S0 397.4 0 256C0 140 77.1 42.1 182.9 10.6c16.9-5 34.8 4.6 39.8 21.5z"],
"clock": [512, "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120l0 136c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2 280 120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"],
"cloud": [640, "M0 336c0 79.5 64.5 144 144 144l368 0c70.7 0 128-57.3 128-128c0-61.9-44-113.6-102.4-125.4c4.1-10.7 6.4-22.4 6.4-34.6c0-53-43-96-96-96c-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32C167.6 32 96 103.6 96 192c0 2.7 .1 5.4 .2 8.1C40.2 219.8 0 273.2 0 336z"],
"cloud-upload-alt": [640, "M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128l-368 0zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39L296 392c0 13.3 10.7 24 24 24s24-10.7 24-24l0-134.1 39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z"],
"cog": [512, "M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"],
"cogs": [640, "M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25c-2.3-5.3-4.8-10.5-7.6-15.5L304 89.4c-3-5-6.3-9.9-9.8-14.6c-5.7-7.6-15.7-10.1-24.7-7.1l-28.2 9.3c-10.7-8.8-23-16-36.2-20.9L199 27.1c-1.9-9.3-9.1-16.7-18.5-17.8C173.9 8.4 167.2 8 160.4 8l-.7 0c-6.8 0-13.5 .4-20.1 1.2c-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9L50.5 67.8c-9-3-19-.5-24.7 7.1c-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6c-3.7 8.7-.9 18.6 6.2 25l22.2 19.8C32.6 161.9 32 168.9 32 176s.6 14.1 1.7 20.9L11.5 216.7c-7.1 6.3-9.9 16.2-6.2 25c2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6c5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8c6.7 .8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1c3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5c3.7-8.7 .9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM504.7 500.5c6.3 7.1 16.2 9.9 25 6.2c5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8c7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2l29.1-6.1c9.3-1.9 16.7-9.1 17.8-18.5c.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5L583.9 307c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7c-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6c-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1 .6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2c-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9c-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2L315.1 313c-9.3 1.9-16.7 9.1-17.8 18.5c-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7c4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6c8.7 3.7 18.6 .9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"],
"copy": [448, "M208 0L332.1 0c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9L448 336c0 26.5-21.5 48-48 48l-192 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48zM48 128l80 0 0 64-64 0 0 256 192 0 0-32 64 0 0 48c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 176c0-26.5 21.5-48 48-48z"],
"database": [448, "M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z"],
"download": [512, "M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 242.7-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7 288 32zM64 352c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-101.5 0-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352 64 352zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"],
"ellipsis-v": [128, "M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z"],
"envelope": [512, "M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"],
"exclamation-circle": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"],
"exclamation-triangle": [512, "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"],
"expand": [448, "M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"],
"eye": [576, "M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"],
"file": [384, "M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z"],
"file-alt": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM112 256l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"],
"file-archive": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM96 48c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm-6.3 71.8c3.7-14 16.4-23.8 30.9-23.8l14.8 0c14.5 0 27.2 9.7 30.9 23.8l23.5 88.2c1.4 5.4 2.1 10.9 2.1 16.4c0 35.2-28.8 63.7-64 63.7s-64-28.5-64-63.7c0-5.5 .7-11.1 2.1-16.4l23.5-88.2zM112 336c-8.8 0-16 7.2-16 16s7.2 16 16 16l32 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0z"],
"file-audio": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"],
"file-code": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM153 289l-31 31 31 31c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0L71 337c-9.4-9.4-9.4-24.6 0-33.9l48-48c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9zM265 255l48 48c9.4 9.4 9.4 24.6 0 33.9l-48 48c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l31-31-31-31c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z"],
"file-excel": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM155.7 250.2L192 302.1l36.3-51.9c7.6-10.9 22.6-13.5 33.4-5.9s13.5 22.6 5.9 33.4L221.3 344l46.4 66.2c7.6 10.9 5 25.8-5.9 33.4s-25.8 5-33.4-5.9L192 385.8l-36.3 51.9c-7.6 10.9-22.6 13.5-33.4 5.9s-13.5-22.6-5.9-33.4L162.7 344l-46.4-66.2c-7.6-10.9-5-25.8 5.9-33.4s25.8-5 33.4 5.9z"],
"file-image": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM64 256a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm152 32c5.3 0 10.2 2.6 13.2 6.9l88 128c3.4 4.9 3.7 11.3 1 16.5s-8.2 8.6-14.2 8.6l-88 0-40 0-48 0-48 0c-5.8 0-11.1-3.1-13.9-8.1s-2.8-11.2 .2-16.1l48-80c2.9-4.8 8.1-7.8 13.7-7.8s10.8 2.9 13.7 7.8l12.8 21.4 48.3-70.2c3-4.3 7.9-6.9 13.2-6.9z"],
"file-pdf": [512, "M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 144-208 0c-35.3 0-64 28.7-64 64l0 144-48 0c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM176 352l32 0c30.9 0 56 25.1 56 56s-25.1 56-56 56l-16 0 0 32c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-48 0-80c0-8.8 7.2-16 16-16zm32 80c13.3 0 24-10.7 24-24s-10.7-24-24-24l-16 0 0 48 16 0zm96-80l32 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-32 0c-8.8 0-16-7.2-16-16l0-128c0-8.8 7.2-16 16-16zm32 128c8.8 0 16-7.2 16-16l0-64c0-8.8-7.2-16-16-16l-16 0 0 96 16 0zm80-112c0-8.8 7.2-16 16-16l48 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0 0 32 32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0 0 48c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-64 0-64z"],
"file-powerpoint": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM136 240l68 0c42 0 76 34 76 76s-34 76-76 76l-44 0 0 32c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-56 0-104c0-13.3 10.7-24 24-24zm68 104c15.5 0 28-12.5 28-28s-12.5-28-28-28l-44 0 0 56 44 0z"],
"file-video": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM64 288c0-17.7 14.3-32 32-32l96 0c17.7 0 32 14.3 32 32l0 96c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32l0-96zM300.9 397.9L256 368l0-64 44.9-29.9c2-1.3 4.4-2.1 6.8-2.1c6.8 0 12.3 5.5 12.3 12.3l0 103.4c0 6.8-5.5 12.3-12.3 12.3c-2.4 0-4.8-.7-6.8-2.1z"],
"file-word": [384, "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM111 257.1l26.8 89.2 31.6-90.3c3.4-9.6 12.5-16.1 22.7-16.1s19.3 6.4 22.7 16.1l31.6 90.3L273 257.1c3.8-12.7 17.2-19.9 29.9-16.1s19.9 17.2 16.1 29.9l-48 160c-3 10-12 16.9-22.4 17.1s-19.8-6.2-23.2-16.1L192 336.6l-33.3 95.3c-3.4 9.8-12.8 16.3-23.2 16.1s-19.5-7.1-22.4-17.1l-48-160c-3.8-12.7 3.4-26.1 16.1-29.9s26.1 3.4 29.9 16.1z"],
"folder": [512, "M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"],
"folder-open": [576, "M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"],
"folder-plus": [512, "M512 416c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l128 0c20.1 0 39.1 9.5 51.2 25.6l19.2 25.6c6 8.1 15.5 12.8 25.6 12.8l160 0c35.3 0 64 28.7 64 64l0 256zM232 376c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l64 0 0 64z"],
"github": [496, "M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"],
"globe": [512, "M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z"],
"hdd": [512, "M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 184.4c-17-15.2-39.4-24.4-64-24.4L64 256c-24.6 0-47 9.2-64 24.4L0 96zM64 288l384 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64l0-64c0-35.3 28.7-64 64-64zM320 416a32 32 0 1 0 0-64 32 32 0 1 0 0 64zm128-32a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"],
"id-card": [576, "M0 96l576 0c0-35.3-28.7-64-64-64L64 32C28.7 32 0 60.7 0 96zm0 32L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-288L0 128zM64 405.3c0-29.5 23.9-53.3 53.3-53.3l117.3 0c29.5 0 53.3 23.9 53.3 53.3c0 5.9-4.8 10.7-10.7 10.7L74.7 416c-5.9 0-10.7-4.8-10.7-10.7zM176 192a64 64 0 1 1 0 128 64 64 0 1 1 0-128zm176 16c0-8.8 7.2-16 16-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16z"],
"info-circle": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"],
"key": [512, "M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17l0 80c0 13.3 10.7 24 24 24l80 0c13.3 0 24-10.7 24-24l0-40 40 0c13.3 0 24-10.7 24-24l0-40 40 0c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zM376 96a40 40 0 1 1 0 80 40 40 0 1 1 0-80z"],
"keyboard": [576, "M64 64C28.7 64 0 92.7 0 128L0 384c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64L64 64zm16 64l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM64 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm16 80l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zm80-176c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm16 80l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM160 336c0-8.8 7.2-16 16-16l224 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-224 0c-8.8 0-16-7.2-16-16l0-32zM272 128l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM256 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM368 128l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM352 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM464 128l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM448 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm16 80l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16z"],
"list": [512, "M40 48C26.7 48 16 58.7 16 72l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24L40 48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM16 232l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0z"],
"lock": [448, "M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z"],
"moon": [384, "M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"],
"pen": [512, "M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"],
"question-circle": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"],
"save": [448, "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm0 96c0-17.7 14.3-32 32-32l192 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32L96 224c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"],
"search": [512, "M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"],
"search-minus": [512, "M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM136 184c-13.3 0-24 10.7-24 24s10.7 24 24 24l144 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-144 0z"],
"search-plus": [512, "M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM184 296c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l64 0 0 64z"],
"server": [512, "M64 32C28.7 32 0 60.7 0 96l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 32zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 288zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"],
"share-alt": [448, "M352 224c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96c0 4 .2 8 .7 11.9l-94.1 47C145.4 170.2 121.9 160 96 160c-53 0-96 43-96 96s43 96 96 96c25.9 0 49.4-10.2 66.6-26.9l94.1 47c-.5 3.9-.7 7.8-.7 11.9c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-25.9 0-49.4 10.2-66.6 26.9l-94.1-47c.5-3.9 .7-7.8 .7-11.9s-.2-8-.7-11.9l94.1-47C302.6 213.8 326.1 224 352 224z"],
"shield-alt": [512, "M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8l0 378.1C394 378 431.1 230.1 432 141.4L256 66.8s0 0 0 0z"],
"sign-in-alt": [512, "M217.9 105.9L340.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L217.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1L32 320c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM352 416l64 0c17.7 0 32-14.3 32-32l0-256c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c53 0 96 43 96 96l0 256c0 53-43 96-96 96l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z"],
"sign-out-alt": [512, "M377.9 105.9L500.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L377.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1-128 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM160 96L96 96c-17.7 0-32 14.3-32 32l0 256c0 17.7 14.3 32 32 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c-53 0-96-43-96-96L0 128C0 75 43 32 96 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32z"],
"sliders-h": [512, "M0 416c0 17.7 14.3 32 32 32l54.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 448c17.7 0 32-14.3 32-32s-14.3-32-32-32l-246.7 0c-12.3-28.3-40.5-48-73.3-48s-61 19.7-73.3 48L32 384c-17.7 0-32 14.3-32 32zm128 0a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM320 256a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm32-80c-32.8 0-61 19.7-73.3 48L32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l246.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48l54.7 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-54.7 0c-12.3-28.3-40.5-48-73.3-48zM192 128a32 32 0 1 1 0-64 32 32 0 1 1 0 64zm73.3-64C253 35.7 224.8 16 192 16s-61 19.7-73.3 48L32 64C14.3 64 0 78.3 0 96s14.3 32 32 32l86.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 128c17.7 0 32-14.3 32-32s-14.3-32-32-32L265.3 64z"],
"spinner": [512, "M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"],
"star": [576, "M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"],
"terminal": [576, "M9.4 86.6C-3.1 74.1-3.1 53.9 9.4 41.4s32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L178.7 256 9.4 86.6zM256 416l288 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-288 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z"],
"th": [512, "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm88 64l0 64-88 0 0-64 88 0zm56 0l88 0 0 64-88 0 0-64zm240 0l0 64-88 0 0-64 88 0zM64 224l88 0 0 64-88 0 0-64zm232 0l0 64-88 0 0-64 88 0zm64 0l88 0 0 64-88 0 0-64zM152 352l0 64-88 0 0-64 88 0zm56 0l88 0 0 64-88 0 0-64zm240 0l0 64-88 0 0-64 88 0z"],
"times": [384, "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"],
"times-circle": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"],
"trash": [448, "M135.2 17.7C140.6 6.8 151.7 0 163.8 0L284.2 0c12.1 0 23.2 6.8 28.6 17.7L320 32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 7.2-14.3zM32 128l384 0 0 320c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-320zm96 64c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16z"],
"trash-alt": [448, "M135.2 17.7C140.6 6.8 151.7 0 163.8 0L284.2 0c12.1 0 23.2 6.8 28.6 17.7L320 32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 7.2-14.3zM32 128l384 0 0 320c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-320zm96 64c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16z"],
"undo": [512, "M48.5 224L40 224c-13.3 0-24-10.7-24-24L16 72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8L48.5 224z"],
"user": [448, "M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z"],
"user-circle": [512, "M399 384.2C376.9 345.8 335.4 320 288 320l-64 0c-47.4 0-88.9 25.8-111 64.2c35.2 39.2 86.2 63.8 143 63.8s107.8-24.7 143-63.8zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm256 16a72 72 0 1 0 0-144 72 72 0 1 0 0 144z"],
"user-plus": [640, "M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304l91.4 0C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7L29.7 512C13.3 512 0 498.7 0 482.3zM504 312l0-64-64 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l64 0 0-64c0-13.3 10.7-24 24-24s24 10.7 24 24l0 64 64 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-64 0 0 64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"],
"users": [640, "M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192l42.7 0c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0L21.3 320C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7l42.7 0C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3l-213.3 0zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352l117.3 0C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7l-330.7 0c-14.7 0-26.7-11.9-26.7-26.7z"],
"users-cog": [640, "M144 160A80 80 0 1 0 144 0a80 80 0 1 0 0 160zm368 0A80 80 0 1 0 512 0a80 80 0 1 0 0 160zM0 298.7C0 310.4 9.6 320 21.3 320l213.3 0c.2 0 .4 0 .7 0c-26.6-23.5-43.3-57.8-43.3-96c0-7.6 .7-15 1.9-22.3c-13.6-6.3-28.7-9.7-44.6-9.7l-42.7 0C47.8 192 0 239.8 0 298.7zM320 320c24 0 45.9-8.8 62.7-23.3c2.5-3.7 5.2-7.3 8-10.7c2.7-3.3 5.7-6.1 9-8.3C410 262.3 416 243.9 416 224c0-53-43-96-96-96s-96 43-96 96s43 96 96 96zm65.4 60.2c-10.3-5.9-18.1-16.2-20.8-28.2l-103.2 0C187.7 352 128 411.7 128 485.3c0 14.7 11.9 26.7 26.7 26.7l300.6 0c-2.1-5.2-3.2-10.9-3.2-16.4l0-3c-1.3-.7-2.7-1.5-4-2.3l-2.6 1.5c-16.8 9.7-40.5 8-54.7-9.7c-4.5-5.6-8.6-11.5-12.4-17.6l-.1-.2-.1-.2-2.4-4.1-.1-.2-.1-.2c-3.4-6.2-6.4-12.6-9-19.3c-8.2-21.2 2.2-42.6 19-52.3l2.7-1.5c0-.8 0-1.5 0-2.3s0-1.5 0-2.3l-2.7-1.5zM533.3 192l-42.7 0c-15.9 0-31 3.5-44.6 9.7c1.3 7.2 1.9 14.7 1.9 22.3c0 17.4-3.5 33.9-9.7 49c2.5 .9 4.9 2 7.1 3.3l2.6 1.5c1.3-.8 2.6-1.6 4-2.3l0-3c0-19.4 13.3-39.1 35.8-42.6c7.9-1.2 16-1.9 24.2-1.9s16.3 .6 24.2 1.9c22.5 3.5 35.8 23.2 35.8 42.6l0 3c1.3 .7 2.7 1.5 4 2.3l2.6-1.5c16.8-9.7 40.5-8 54.7 9.7c2.3 2.8 4.5 5.8 6.6 8.7c-2.1-57.1-49-102.7-106.6-102.7zm91.3 163.9c6.3-3.6 9.5-11.1 6.8-18c-2.1-5.5-4.6-10.8-7.4-15.9l-2.3-4c-3.1-5.1-6.5-9.9-10.2-14.5c-4.6-5.7-12.7-6.7-19-3l-2.9 1.7c-9.2 5.3-20.4 4-29.6-1.3s-16.1-14.5-16.1-25.1l0-3.4c0-7.3-4.9-13.8-12.1-14.9c-6.5-1-13.1-1.5-19.9-1.5s-13.4 .5-19.9 1.5c-7.2 1.1-12.1 7.6-12.1 14.9l0 3.4c0 10.6-6.9 19.8-16.1 25.1s-20.4 6.6-29.6 1.3l-2.9-1.7c-6.3-3.6-14.4-2.6-19 3c-3.7 4.6-7.1 9.5-10.2 14.6l-2.3 3.9c-2.8 5.1-5.3 10.4-7.4 15.9c-2.6 6.8 .5 14.3 6.8 17.9l2.9 1.7c9.2 5.3 13.7 15.8 13.7 26.4s-4.5 21.1-13.7 26.4l-3 1.7c-6.3 3.6-9.5 11.1-6.8 17.9c2.1 5.5 4.6 10.7 7.4 15.8l2.4 4.1c3 5.1 6.4 9.9 10.1 14.5c4.6 5.7 12.7 6.7 19 3l2.9-1.7c9.2-5.3 20.4-4 29.6 1.3s16.1 14.5 16.1 25.1l0 3.4c0 7.3 4.9 13.8 12.1 14.9c6.5 1 13.1 1.5 19.9 1.5s13.4-.5 19.9-1.5c7.2-1.1 12.1-7.6 12.1-14.9l0-3.4c0-10.6 6.9-19.8 16.1-25.1s20.4-6.6 29.6-1.3l2.9 1.7c6.3 3.6 14.4 2.6 19-3c3.7-4.6 7.1-9.4 10.1-14.5l2.4-4.2c2.8-5.1 5.3-10.3 7.4-15.8c2.6-6.8-.5-14.3-6.8-17.9l-3-1.7c-9.2-5.3-13.7-15.8-13.7-26.4s4.5-21.1 13.7-26.4l3-1.7zM472 384a40 40 0 1 1 80 0 40 40 0 1 1 -80 0z"],
"vial": [512, "M342.6 9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l9.4 9.4L28.1 342.6C10.1 360.6 0 385 0 410.5L0 416c0 53 43 96 96 96l5.5 0c25.5 0 49.9-10.1 67.9-28.1L448 205.3l9.4 9.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-32-32-96-96-32-32zM205.3 256L352 109.3 402.7 160l-96 96-101.5 0z"],
};
// ── CSS injection ──────────────────────────────────────────────
// Injects minimal CSS for SVG icons (replaces ~80 KB of FA CSS).
(function injectIconCSS() {
if (document.getElementById("oxi-icon-css")) return;
const style = document.createElement("style");
style.id = "oxi-icon-css";
style.textContent = `
.oxi-icon {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -0.125em;
fill: currentColor;
overflow: hidden;
flex-shrink: 0;
}
.oxi-icon-spin {
animation: oxi-spin 1s linear infinite;
}
@keyframes oxi-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
})();
// ── Public API ─────────────────────────────────────────────────
/**
* Build an SVG string for a given icon name.
* @param {string} name Icon name without "fa-" prefix (e.g. "folder", "trash-alt")
* @param {string} [extraClass] Additional CSS classes
* @returns {string} SVG markup string, or empty string if icon not found
*/
function oxiIcon(name, extraClass) {
const entry = _ICONS[name];
if (!entry) return "";
const [w, d] = entry;
const cls = extraClass ? "oxi-icon " + extraClass : "oxi-icon";
return `<svg class="${cls}" viewBox="0 0 ${w} 512" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`;
}
/**
* Replace all <i class="fas/fab fa-xxx"> elements inside a container
* with equivalent inline SVGs.
*
* This is the migration bridge: existing code keeps generating <i> tags
* with FA classes, and this function post-processes them into SVGs.
*
* @param {Element} [container=document.body] Scope of replacement
*/
function replaceIconsInElement(container) {
if (!container) container = document.body;
const icons = container.querySelectorAll("i[class*=\"fa-\"]");
for (let i = 0; i < icons.length; i++) {
const el = icons[i];
const classes = el.className.split(/\s+/);
// Find the icon name (fa-xxx)
let iconName = null;
let isSpin = false;
const extraClasses = [];
for (const cls of classes) {
if (cls === "fa-spin") { isSpin = true; continue; }
if (cls.startsWith("fa-") && cls !== "fa") {
iconName = cls.substring(3); // strip "fa-"
} else if (cls !== "fas" && cls !== "fab" && cls !== "far" && cls !== "fa") {
extraClasses.push(cls);
}
}
if (!iconName || !_ICONS[iconName]) continue;
const [w, d] = _ICONS[iconName];
// Build SVG element
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 " + w + " 512");
svg.setAttribute("aria-hidden", "true");
let svgClass = "oxi-icon";
if (isSpin) svgClass += " oxi-icon-spin";
if (extraClasses.length) svgClass += " " + extraClasses.join(" ");
svg.setAttribute("class", svgClass);
// Copy inline style if present
if (el.style.cssText) svg.style.cssText = el.style.cssText;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("fill", "currentColor");
path.setAttribute("d", d);
svg.appendChild(path);
el.replaceWith(svg);
}
}
// ── Expose globally ────────────────────────────────────────────
window.oxiIcon = oxiIcon;
window.replaceIconsInElement = replaceIconsInElement;
window.OxiIcons = _ICONS;
// ── Auto-replace: MutationObserver bridge ──────────────────────
// Watches the DOM for new <i class="fa-..."> elements and converts them
// to inline SVGs automatically. Debounced to at most once per frame.
(function autoReplace() {
let raf = 0;
const scan = () => { raf = 0; replaceIconsInElement(); };
// Initial sweep once the DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scan);
} else {
scan();
}
// Observe future mutations (dynamic renders, modals, etc.)
new MutationObserver(function (mutations) {
if (raf) return;
for (let i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length) {
raf = requestAnimationFrame(scan);
return;
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
})();
+2 -2
View File
@@ -284,8 +284,8 @@ const multiSelect = {
// Re-wire the select-all checkbox
const cb = document.getElementById('select-all-checkbox');
if (cb) cb.addEventListener('change', () => this.toggleAll());
// Translate restored header
if (window.i18n && window.i18n.translatePage) window.i18n.translatePage();
// Translate restored header (scoped to list header)
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(listHeader);
}
// Hide grid bar
+28 -117
View File
@@ -94,8 +94,8 @@ const recent = {
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header recent-header">
<div></div>
<div class="list-header">
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
@@ -117,129 +117,40 @@ const recent = {
return;
}
const folders = [];
const files = [];
for (const item of recentItems) {
this._renderRecentItem(item, filesGrid, filesListView);
const isFolder = item.item_type === 'folder';
if (isFolder) {
folders.push({
id: item.item_id,
name: item.item_name || item.item_id,
parent_id: item.parent_id || '',
modified_at: item.accessed_at
});
} else {
files.push({
id: item.item_id,
name: item.item_name || item.item_id,
folder_id: item.parent_id || '',
mime_type: item.item_mime_type,
icon_class: item.icon_class,
icon_special_class: item.icon_special_class,
category: item.category,
size: item.item_size || 0,
size_formatted: item.size_formatted,
modified_at: item.accessed_at
});
}
}
window.ui.updateFileIcons();
if (folders.length) window.ui.renderFolders(folders);
if (files.length) window.ui.renderFiles(files);
} catch (error) {
console.error('Error displaying recent files:', error);
if (window.ui && window.ui.showNotification) {
window.ui.showNotification('Error', 'Error loading recent files');
}
}
},
// ───────────────────── renderer ─────────────────────
_renderRecentItem(item, filesGrid, filesListView) {
const name = item.item_name || item.item_id || 'Unknown';
const itemId = item.item_id;
const folderId = item.parent_id || '';
const mimeType = item.item_mime_type || 'application/octet-stream';
const isFolder = item.item_type === 'folder';
// Build a minimal file object for click handlers
const fileObj = {
id: itemId,
name,
folder_id: folderId,
mime_type: mimeType,
size: item.item_size || 0
};
// Use pre-computed display fields from the enriched API response
const iconClass = item.icon_class || (isFolder ? 'fas fa-folder' : 'fas fa-file');
const iconSpecialClass = item.icon_special_class || (isFolder ? 'folder-icon' : '');
const typeLabel = item.category
? (window.i18n ? window.i18n.t(`files.file_types.${item.category.toLowerCase()}`) || item.category : item.category)
: (isFolder
? (window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder')
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document'));
const fileSize = isFolder ? '--' : (item.size_formatted || (window.formatFileSize ? window.formatFileSize(item.item_size || 0) : '0 B'));
const formattedDate = window.formatDateTime(item.accessed_at);
// Click handler
const onClick = () => {
if (isFolder) {
window.app.currentPath = itemId;
window.ui.updateBreadcrumb(name);
window.loadFiles();
} else {
// Re-record access
document.dispatchEvent(new CustomEvent('file-accessed', { detail: { file: fileObj } }));
if (window.ui && window.ui.isViewableFile(fileObj) && window.inlineViewer) {
window.inlineViewer.openFile(fileObj);
} else if (window.fileOps) {
window.fileOps.downloadFile(itemId, name);
}
}
};
// Context menu handler
const onContextMenu = (e) => {
e.preventDefault();
if (isFolder) {
window.app.contextMenuTargetFolder = { id: itemId, name, parent_id: folderId };
const cm = document.getElementById('folder-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
} else {
window.app.contextMenuTargetFile = { id: itemId, name, folder_id: folderId };
const cm = document.getElementById('file-context-menu');
cm.style.left = `${e.pageX}px`;
cm.style.top = `${e.pageY}px`;
cm.style.display = 'block';
}
};
// --- grid element ---
const gridEl = document.createElement('div');
gridEl.className = `file-card recent-item`;
if (isFolder) {
gridEl.dataset.folderId = itemId;
gridEl.dataset.folderName = name;
} else {
gridEl.dataset.fileId = itemId;
gridEl.dataset.fileName = name;
gridEl.dataset.folderId = folderId;
}
gridEl.innerHTML = `
<div class="recent-indicator"><i class="fas fa-clock"></i></div>
<div class="file-icon ${iconSpecialClass}"><i class="${iconClass}"></i></div>
<div class="file-name">${escapeHtml(name)}</div>
<div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
`;
gridEl.addEventListener('click', onClick);
gridEl.addEventListener('contextmenu', onContextMenu);
filesGrid.appendChild(gridEl);
// --- list element ---
const listEl = document.createElement('div');
listEl.className = `file-item recent-item`;
if (isFolder) {
listEl.dataset.folderId = itemId;
listEl.dataset.folderName = name;
} else {
listEl.dataset.fileId = itemId;
listEl.dataset.fileName = name;
listEl.dataset.folderId = folderId;
}
listEl.innerHTML = `
<div class="recent-indicator"><i class="fas fa-clock"></i></div>
<div class="name-cell">
<div class="file-icon ${iconSpecialClass}"><i class="${iconClass}"></i></div>
<span>${escapeHtml(name)}</span>
</div>
<div class="type-cell">${escapeHtml(typeLabel)}</div>
<div class="size-cell">${fileSize}</div>
<div class="date-cell">${formattedDate}</div>
`;
listEl.addEventListener('click', onClick);
listEl.addEventListener('contextmenu', onContextMenu);
filesListView.appendChild(listEl);
}
};
-2
View File
@@ -191,8 +191,6 @@ const search = {
results.files.forEach(file => {
window.ui.addFileToView(file);
});
window.ui.updateFileIcons();
},
/**
+1 -7
View File
@@ -13,13 +13,7 @@ function checkAuthentication() {
}
document.addEventListener('DOMContentLoaded', async () => {
// ── i18n ──
if (window.i18n && window.i18n.init) {
await window.i18n.init();
setTimeout(() => {
if (window.i18n.translatePage) window.i18n.translatePage();
}, 500);
}
// ── i18n ── (translatePage is called automatically by i18n.js on init)
function t(key, fallback) {
return (window.i18n && window.i18n.t) ? window.i18n.t(key, fallback) : fallback;
+13 -148
View File
@@ -474,8 +474,7 @@ const ui = {
/**
* Get FontAwesome icon class for a filename based on its extension.
* Used as fallback when the backend DTO doesn't include icon_class
* (e.g. trash items). Kept intentionally small — the comprehensive
* visual icon map lives in updateFileIcons().
* (e.g. trash items).
*/
getIconClass(fileName) {
if (!fileName) return 'fas fa-file';
@@ -552,150 +551,6 @@ const ui = {
}
},
/**
* Update file icons based on file type
*/
updateFileIcons() {
const fileCards = document.querySelectorAll('.file-card');
fileCards.forEach(card => {
const fileName = card.querySelector('.file-name')?.textContent || '';
const iconElement = card.querySelector('.file-icon');
if (!iconElement) return;
if (iconElement.classList.contains('folder-icon')) {
iconElement.innerHTML = '';
return;
}
const extension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : '';
// Map extensions to icon types
const iconMap = {
// Documents
pdf: { cls: 'pdf-icon', fa: 'fas fa-file-pdf' },
doc: { cls: 'doc-icon', fa: 'fas fa-file-word' },
docx: { cls: 'doc-icon', fa: 'fas fa-file-word' },
txt: { cls: 'doc-icon', fa: 'fas fa-file-alt' },
rtf: { cls: 'doc-icon', fa: 'fas fa-file-alt' },
odt: { cls: 'doc-icon', fa: 'fas fa-file-alt' },
// Spreadsheets
xlsx: { cls: 'spreadsheet-icon' },
xls: { cls: 'spreadsheet-icon' },
csv: { cls: 'spreadsheet-icon' },
ods: { cls: 'spreadsheet-icon' },
// Presentations
pptx: { cls: 'presentation-icon' },
ppt: { cls: 'presentation-icon' },
odp: { cls: 'presentation-icon' },
// Images
jpg: { cls: 'image-icon' },
jpeg: { cls: 'image-icon' },
png: { cls: 'image-icon' },
gif: { cls: 'image-icon' },
svg: { cls: 'image-icon' },
webp: { cls: 'image-icon' },
bmp: { cls: 'image-icon' },
ico: { cls: 'image-icon' },
// Videos
mp4: { cls: 'video-icon' },
avi: { cls: 'video-icon' },
mov: { cls: 'video-icon' },
mkv: { cls: 'video-icon' },
webm: { cls: 'video-icon' },
flv: { cls: 'video-icon' },
// Audio
mp3: { cls: 'audio-icon' },
wav: { cls: 'audio-icon' },
ogg: { cls: 'audio-icon' },
flac: { cls: 'audio-icon' },
aac: { cls: 'audio-icon' },
m4a: { cls: 'audio-icon' },
// Archives
zip: { cls: 'archive-icon' },
rar: { cls: 'archive-icon' },
'7z': { cls: 'archive-icon' },
tar: { cls: 'archive-icon' },
gz: { cls: 'archive-icon' },
bz2: { cls: 'archive-icon' },
// Installers
dmg: { cls: 'installer-icon' },
exe: { cls: 'installer-icon' },
msi: { cls: 'installer-icon' },
deb: { cls: 'installer-icon' },
rpm: { cls: 'installer-icon' },
pkg: { cls: 'installer-icon' },
app: { cls: 'installer-icon' },
// Scripts
sh: { cls: 'script-icon', fa: 'fas fa-terminal' },
bash: { cls: 'script-icon', fa: 'fas fa-terminal' },
zsh: { cls: 'script-icon', fa: 'fas fa-terminal' },
bat: { cls: 'script-icon', fa: 'fas fa-terminal' },
ps1: { cls: 'script-icon', fa: 'fas fa-terminal' },
// Code — each with sub-type
json: { cls: 'code-icon', sub: 'json-icon' },
js: { cls: 'code-icon', sub: 'js-icon' },
jsx: { cls: 'code-icon', sub: 'js-icon' },
ts: { cls: 'code-icon', sub: 'ts-icon' },
tsx: { cls: 'code-icon', sub: 'ts-icon' },
html: { cls: 'code-icon', sub: 'html-icon' },
htm: { cls: 'code-icon', sub: 'html-icon' },
css: { cls: 'code-icon', sub: 'css-icon' },
scss: { cls: 'code-icon', sub: 'css-icon' },
py: { cls: 'code-icon', sub: 'py-icon' },
rs: { cls: 'code-icon', sub: 'rust-icon' },
go: { cls: 'code-icon', sub: 'go-icon' },
java: { cls: 'code-icon', sub: 'java-icon' },
c: { cls: 'code-icon', sub: 'c-icon' },
cpp: { cls: 'code-icon', sub: 'c-icon' },
cs: { cls: 'code-icon', sub: 'cs-icon' },
php: { cls: 'code-icon', sub: 'php-icon' },
rb: { cls: 'code-icon', sub: 'ruby-icon' },
swift: { cls: 'code-icon', sub: 'swift-icon' },
kt: { cls: 'code-icon', sub: 'kotlin-icon' },
sql: { cls: 'code-icon', sub: 'sql-icon' },
yaml: { cls: 'code-icon', sub: 'yaml-icon' },
yml: { cls: 'code-icon', sub: 'yaml-icon' },
toml: { cls: 'code-icon', sub: 'toml-icon' },
xml: { cls: 'code-icon', sub: 'html-icon' },
md: { cls: 'code-icon', sub: 'md-icon' },
// Config
ini: { cls: 'config-icon', fa: 'fas fa-cog' },
cfg: { cls: 'config-icon', fa: 'fas fa-cog' },
conf: { cls: 'config-icon', fa: 'fas fa-cog' },
env: { cls: 'config-icon', fa: 'fas fa-cog' },
};
const mapping = iconMap[extension];
if (mapping) {
iconElement.className = `file-icon ${mapping.cls}`;
if (mapping.cls === 'code-icon') {
// Code icons use pseudo-element lines
iconElement.innerHTML = `
<div class="code-line-1"></div>
<div class="code-line-2"></div>
<div class="code-line-3"></div>
`;
if (mapping.sub) iconElement.classList.add(mapping.sub);
} else {
// Types with pure CSS visuals — clear the <i>
const pureCssTypes = ['image-icon','video-icon','spreadsheet-icon','presentation-icon','audio-icon','archive-icon','installer-icon'];
if (pureCssTypes.includes(mapping.cls)) {
iconElement.innerHTML = '';
} else if (mapping.fa) {
// Types that keep the FA icon — update <i> class
let iEl = iconElement.querySelector('i');
if (!iEl) {
iEl = document.createElement('i');
iconElement.innerHTML = '';
iconElement.appendChild(iEl);
}
iEl.className = mapping.fa;
}
}
}
});
},
/* ================================================================
@@ -739,7 +594,6 @@ const ui = {
}
if (self.isViewableFile(file)) {
if (window.inlineViewer) window.inlineViewer.openFile(file);
else if (window.fileViewer) window.fileViewer.open(file);
else window.fileOps.downloadFile(file.id, file.name);
} else {
window.fileOps.downloadFile(file.id, file.name);
@@ -791,6 +645,16 @@ const ui = {
return;
}
// In favorites/recent view, single-click navigates/opens (like list)
if (window.app.isFavoritesView || window.app.isRecentView) {
const info = itemInfo(card);
if (info) {
if (info.type === 'folder') navigateFolder(card);
else openFile(info.data);
}
return;
}
toggleCardSelection(card, e);
});
@@ -996,6 +860,7 @@ const ui = {
/** Create a grid card for a file */
_createFileCard(file) {
const iconClass = file.icon_class || 'fas fa-file';
const iconSpecialClass = file.icon_special_class || '';
const isFileFav = window.favorites &&
window.favorites.isFavorite(file.id, 'file');
const formattedDate = window.formatDateTime(file.modified_at);
@@ -1011,7 +876,7 @@ const ui = {
<div class="file-card-checkbox"><i class="fas fa-check"></i></div>
<button class="file-card-more"><i class="fas fa-ellipsis-v"></i></button>
${isFileFav ? '<div class="favorite-star active"><i class="fas fa-star"></i></div>' : ''}
<div class="file-icon">
<div class="file-icon ${iconSpecialClass}">
<i class="${iconClass}"></i>
</div>
<div class="file-name">${escapeHtml(file.name)}</div>
+1 -2
View File
@@ -10,11 +10,10 @@
<!-- Styles -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/auth.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<!-- Scripts -->
<script src="/js/i18n.js"></script>
<script src="/js/icons.js" defer></script>
<script src="/js/auth.js" defer></script>
</head>
<body>
+1 -1
View File
@@ -6,7 +6,7 @@
<title>OxiCloud — My Profile</title>
<!-- Apply saved theme immediately to prevent flash of light mode -->
<script>if(localStorage.getItem('oxicloud_theme')==='dark')document.documentElement.setAttribute('data-theme','dark');</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="/js/icons.js" defer></script>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}
body{background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;flex-direction:column}
+1 -1
View File
@@ -8,7 +8,7 @@
<script>if(localStorage.getItem('oxicloud_theme')==='dark')document.documentElement.setAttribute('data-theme','dark');</script>
<link rel="stylesheet" href="/css/style.css">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="/js/icons.js" defer></script>
</head>
<body>
<!-- Main layout similar to index.html -->
+3 -4
View File
@@ -1,8 +1,9 @@
// OxiCloud Service Worker
const CACHE_NAME = 'oxicloud-cache-v13';
const CACHE_NAME = 'oxicloud-cache-v14';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/js/icons.js',
'/js/i18n.js',
'/js/languageSelector.js',
'/js/notifications.js',
@@ -10,9 +11,7 @@ const ASSETS_TO_CACHE = [
'/locales/es.json',
'/locales/fa.json',
'/locales/de.json',
'/favicon.ico',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css',
'https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js'
'/favicon.ico'
];
// Install event - cache assets