style: apply biome CSS/JS format
This commit is contained in:
+27
-27
@@ -1,60 +1,60 @@
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background-color: #e64a29;
|
||||
background-color: #e64a29;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background-color: #cbd5e0;
|
||||
background-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #f56565;
|
||||
color: white;
|
||||
background-color: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.danger:hover {
|
||||
background-color: #e53e3e;
|
||||
background-color: #e53e3e;
|
||||
}
|
||||
|
||||
+17
-14
@@ -1,24 +1,27 @@
|
||||
* {
|
||||
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;
|
||||
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 {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html[dir='rtl'] .fa-arrow-left::before {
|
||||
content: "\f061";
|
||||
html[dir="rtl"] .fa-arrow-left::before {
|
||||
content: "\f061";
|
||||
}
|
||||
|
||||
html[dir='rtl'] .fa-sign-out-alt {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
html[dir="rtl"] .fa-sign-out-alt {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
/* Utility: hide elements without inline style="" (CSP-safe) */
|
||||
.hidden { display: none !important; }
|
||||
.hidden {
|
||||
/* biome-ignore lint/complexity/noImportantStyles: this case to overide the display when hiddenis selected */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
gap: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
cursor: pointer;
|
||||
color: #5a6f8a;
|
||||
cursor: pointer;
|
||||
color: #5a6f8a;
|
||||
}
|
||||
|
||||
.breadcrumb-link.drop-target {
|
||||
@@ -27,37 +29,37 @@
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
text-decoration: underline;
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.06);
|
||||
text-decoration: underline;
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.06);
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 4px;
|
||||
color: #adb5bd;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
margin: 0 4px;
|
||||
color: #adb5bd;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.breadcrumb-home {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-home i {
|
||||
font-size: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.breadcrumb-home.breadcrumb-link:hover {
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(255, 94, 58, 0.3);
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f8fafc;
|
||||
color: #4a5568;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
color: #4a5568;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #edf2f7;
|
||||
border-color: #cbd5e0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background-color: #edf2f7;
|
||||
border-color: #cbd5e0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
transform: translateY(0);
|
||||
background-color: #e2e8f0;
|
||||
transform: translateY(0);
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3);
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* View Toggle Buttons */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
background-color: #f0f3f7;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
background-color: #f0f3f7;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #64748b;
|
||||
background-color: #e2e8f0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background-color: white;
|
||||
color: #ff5e3a;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
background-color: white;
|
||||
color: #ff5e3a;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toggle-btn i {
|
||||
pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
+434
-429
File diff suppressed because it is too large
Load Diff
@@ -1,74 +1,76 @@
|
||||
/* Context menu */
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
padding: 6px;
|
||||
min-width: 200px;
|
||||
z-index: 2000;
|
||||
display: none;
|
||||
animation: contextMenuIn 0.15s ease-out;
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 10px 36px rgba(0, 0, 0, 0.12),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
padding: 6px;
|
||||
min-width: 200px;
|
||||
z-index: 2000;
|
||||
display: none;
|
||||
animation: contextMenuIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes contextMenuIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.12s ease;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: #f1f5f9;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.context-menu-item:active {
|
||||
background: #e2e8f0;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.context-menu-item i {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item-danger {
|
||||
color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.context-menu-item-danger:hover {
|
||||
background: #fef2f2;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.context-menu-item-danger i {
|
||||
color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.context-menu-separator {
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 4px 8px;
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
@@ -2,149 +2,211 @@
|
||||
|
||||
/* ── Empty state icons (large, muted) ── */
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
margin-bottom: 16px;
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.empty-state-icon.error {
|
||||
color: #f44336;
|
||||
color: #f44336;
|
||||
}
|
||||
.empty-state-icon.spinner {
|
||||
color: #666;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* ── Dialog header icons (accent color) ── */
|
||||
.dialog-header-icon {
|
||||
color: #ff5e3a;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
/* ── Icon spacing ── */
|
||||
.icon-mr { margin-right: 5px; }
|
||||
.icon-ml { margin-left: 4px; font-size: 12px; }
|
||||
.icon-mr {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.icon-ml {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── Success check icon ── */
|
||||
.check-icon { color: #48bb78; }
|
||||
.check-icon {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
/* ── Move dialog ── */
|
||||
.move-dialog-hint {
|
||||
margin: 0 0 12px;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
margin: 0 0 12px;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
.folder-select-container {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Share dialog sections ── */
|
||||
.share-section {
|
||||
margin: 15px 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
/* ── Search spinner ── */
|
||||
.search-spinner {
|
||||
margin-right: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ── Search empty state text ── */
|
||||
.search-empty-text {
|
||||
color: var(--text-secondary, #64748b);
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
/* ── Notification upload current file ── */
|
||||
.notif-upload-current {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin: 3px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin: 3px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Login auth hint ── */
|
||||
.auth-hint {
|
||||
color: var(--text-secondary, #666);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
color: var(--text-secondary, #666);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── About modal (userMenu.js profile popup) ── */
|
||||
.about-modal-body { max-width: 380px; }
|
||||
.about-modal-header { text-align: center; padding: 20px 20px 0; }
|
||||
.about-modal-body {
|
||||
max-width: 380px;
|
||||
}
|
||||
.about-modal-header {
|
||||
text-align: center;
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
.about-modal-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.about-modal-username {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.about-modal-email {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
.about-modal-username { margin: 0; font-size: 18px; color: #1a1a2e; }
|
||||
.about-modal-email { margin: 4px 0 0; font-size: 13px; color: #64748b; }
|
||||
.about-modal-role {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.about-modal-role-admin {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.about-modal-role-user {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
.about-modal-storage {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.about-modal-role-admin { background: #dbeafe; color: #1d4ed8; }
|
||||
.about-modal-role-user { background: #f1f5f9; color: #64748b; }
|
||||
.about-modal-storage { padding: 16px 20px; }
|
||||
.about-modal-storage-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.about-modal-storage-label i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.about-modal-storage-label i { margin-right: 4px; }
|
||||
.about-modal-bar-bg {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.about-modal-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width .3s;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.about-modal-bar-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-align: right;
|
||||
}
|
||||
.about-modal-footer {
|
||||
padding: 0 20px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.about-modal-bar-text { font-size: 12px; color: #64748b; text-align: right; }
|
||||
.about-modal-footer { padding: 0 20px 16px; display: flex; justify-content: center; }
|
||||
.about-modal-close-btn {
|
||||
padding: 8px 24px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
padding: 8px 24px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
/* ── Dark theme overrides ── */
|
||||
[data-theme="dark"] .empty-state-icon { color: #475569; }
|
||||
[data-theme="dark"] .empty-state-icon.error { color: #ef4444; }
|
||||
[data-theme="dark"] .about-modal-username { color: #f1f5f9; }
|
||||
[data-theme="dark"] .about-modal-email { color: #94a3b8; }
|
||||
[data-theme="dark"] .about-modal-role-admin { background: #1e3a5f; color: #60a5fa; }
|
||||
[data-theme="dark"] .about-modal-role-user { background: #334155; color: #94a3b8; }
|
||||
[data-theme="dark"] .about-modal-storage-label { color: #94a3b8; }
|
||||
[data-theme="dark"] .about-modal-bar-bg { background: #334155; }
|
||||
[data-theme="dark"] .about-modal-bar-text { color: #94a3b8; }
|
||||
[data-theme="dark"] .about-modal-close-btn {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
[data-theme="dark"] .empty-state-icon {
|
||||
color: #475569;
|
||||
}
|
||||
[data-theme="dark"] .empty-state-icon.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-username {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-email {
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-role-admin {
|
||||
background: #1e3a5f;
|
||||
color: #60a5fa;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-role-user {
|
||||
background: #334155;
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-storage-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-bar-bg {
|
||||
background: #334155;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-bar-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .about-modal-close-btn {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
[data-theme="dark"] .move-dialog-hint {
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .search-empty-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
[data-theme="dark"] .move-dialog-hint { color: #94a3b8; }
|
||||
[data-theme="dark"] .search-empty-text { color: #94a3b8; }
|
||||
|
||||
+478
-468
File diff suppressed because it is too large
Load Diff
+179
-181
@@ -1,369 +1,369 @@
|
||||
/* File list - Improved style */
|
||||
.files-list-view {
|
||||
--files-list-columns: 36px minmax(200px, 2fr) 100px 110px 130px 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
--files-list-columns: 36px minmax(200px, 2fr) 100px 110px 130px 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: grid;
|
||||
grid-template-columns: var(--files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 15px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: var(--files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 15px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.files-list-view .file-item {
|
||||
display: grid;
|
||||
grid-template-columns: var(--files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
display: grid;
|
||||
grid-template-columns: var(--files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.files-list-view .file-item.selected {
|
||||
background-color: #fff8f6;
|
||||
background-color: #fff8f6;
|
||||
}
|
||||
|
||||
.files-list-view .file-item.selected:hover {
|
||||
background-color: #fff0ec;
|
||||
background-color: #fff0ec;
|
||||
}
|
||||
|
||||
.list-header.trash-header {
|
||||
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 140px 100px;
|
||||
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 140px 100px;
|
||||
}
|
||||
|
||||
.trash-item.file-item {
|
||||
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 140px 100px;
|
||||
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 140px 100px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item:hover {
|
||||
background-color: #f0f8ff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.list-header > div,
|
||||
.files-list-view .file-item > div {
|
||||
min-width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-header > div:nth-child(4),
|
||||
.files-list-view .file-item .size-cell {
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .name-cell span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.folder-icon {
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.folder-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background-color: #fdcb6e;
|
||||
border-radius: 8px 8px 0 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background-color: #fdcb6e;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.folder-icon i,
|
||||
.files-list-view .file-item .file-icon.folder-icon svg {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.doc-icon {
|
||||
background-color: #e0ecff;
|
||||
background-color: #e0ecff;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.doc-icon i,
|
||||
.files-list-view .file-item .file-icon.doc-icon svg {
|
||||
color: #3171d8;
|
||||
display: flex !important;
|
||||
color: #3171d8;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.doc-icon::before,
|
||||
.files-list-view .file-item .file-icon.doc-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.pdf-icon {
|
||||
background-color: #fee2e2;
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.pdf-icon i,
|
||||
.files-list-view .file-item .file-icon.pdf-icon svg {
|
||||
color: #e53e3e;
|
||||
display: flex !important;
|
||||
color: #e53e3e;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.pdf-icon::before,
|
||||
.files-list-view .file-item .file-icon.pdf-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.image-icon {
|
||||
background-color: #e0f2fe;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background-color: #e0f2fe;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.image-icon i,
|
||||
.files-list-view .file-item .file-icon.image-icon svg {
|
||||
color: #3b82f6;
|
||||
display: flex !important;
|
||||
color: #3b82f6;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.image-icon::before,
|
||||
.files-list-view .file-item .file-icon.image-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.video-icon {
|
||||
background: linear-gradient(135deg, #ede9fe, #fce7f3);
|
||||
background: linear-gradient(135deg, #ede9fe, #fce7f3);
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.video-icon i,
|
||||
.files-list-view .file-item .file-icon.video-icon svg {
|
||||
color: #8b5cf6;
|
||||
display: flex !important;
|
||||
color: #8b5cf6;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.video-icon::before,
|
||||
.files-list-view .file-item .file-icon.video-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.audio-icon {
|
||||
background-color: #fef3c7;
|
||||
background-color: #fef3c7;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.audio-icon i,
|
||||
.files-list-view .file-item .file-icon.audio-icon svg {
|
||||
color: #f59e0b;
|
||||
display: flex !important;
|
||||
color: #f59e0b;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.audio-icon::before,
|
||||
.files-list-view .file-item .file-icon.audio-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.spreadsheet-icon {
|
||||
background-color: #e6f4ea;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
background-color: #e6f4ea;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.spreadsheet-icon i,
|
||||
.files-list-view .file-item .file-icon.spreadsheet-icon svg {
|
||||
color: #0d904f;
|
||||
display: flex !important;
|
||||
color: #0d904f;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.spreadsheet-icon::before,
|
||||
.files-list-view .file-item .file-icon.spreadsheet-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.presentation-icon {
|
||||
background-color: #fef3e2;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
background-color: #fef3e2;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.presentation-icon i,
|
||||
.files-list-view .file-item .file-icon.presentation-icon svg {
|
||||
color: #d04423;
|
||||
display: flex !important;
|
||||
color: #d04423;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.presentation-icon::before,
|
||||
.files-list-view .file-item .file-icon.presentation-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.audio-icon {
|
||||
background-color: #fff3e0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
background-color: #fff3e0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.archive-icon {
|
||||
background-color: #f5f0eb;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
background-color: #f5f0eb;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.archive-icon i,
|
||||
.files-list-view .file-item .file-icon.archive-icon svg {
|
||||
color: #8d6e63;
|
||||
display: flex !important;
|
||||
color: #8d6e63;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.archive-icon::before,
|
||||
.files-list-view .file-item .file-icon.archive-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.installer-icon {
|
||||
background-color: #f3e8ff;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
background-color: #f3e8ff;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.installer-icon i,
|
||||
.files-list-view .file-item .file-icon.installer-icon svg {
|
||||
color: #7c3aed;
|
||||
display: flex !important;
|
||||
color: #7c3aed;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.installer-icon::before,
|
||||
.files-list-view .file-item .file-icon.installer-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.script-icon {
|
||||
background-color: #e8f5e9;
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.script-icon i,
|
||||
.files-list-view .file-item .file-icon.script-icon svg {
|
||||
color: #4eaa25;
|
||||
display: flex !important;
|
||||
color: #4eaa25;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.script-icon::before,
|
||||
.files-list-view .file-item .file-icon.script-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.config-icon {
|
||||
background-color: #f1f3f5;
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.config-icon i,
|
||||
.files-list-view .file-item .file-icon.config-icon svg {
|
||||
color: #718096;
|
||||
display: flex !important;
|
||||
color: #718096;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon.config-icon::before,
|
||||
.files-list-view .file-item .file-icon.config-icon::after {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-header > div:nth-child(5),
|
||||
.files-list-view .file-item .date-cell {
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
.files-list-view .file-item .date-cell {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .size-cell {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .type-cell {
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: #f9fafb;
|
||||
opacity: 0.5;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.files-list-view .file-item.drop-target {
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
border: 2px dashed #ffc107;
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
border: 2px dashed #ffc107;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .action-cell {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .action-cell button {
|
||||
display: inline;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .action-cell button:hover {
|
||||
background: #e0e5e8;
|
||||
color: #334155;
|
||||
background: #e0e5e8;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* could be visible if we want */
|
||||
/* could be visible if we want */
|
||||
.files-list-view .file-item .action-cell button.favorite-star {
|
||||
display: none;
|
||||
border: none;
|
||||
@@ -373,62 +373,60 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] .files-list-view {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .list-header {
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item {
|
||||
background-color: #1e293b;
|
||||
border-bottom-color: #293548;
|
||||
background-color: #1e293b;
|
||||
border-bottom-color: #293548;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item:hover {
|
||||
background-color: #253345;
|
||||
background-color: #253345;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item.selected {
|
||||
background-color: #1a1520;
|
||||
background-color: #1a1520;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item.selected:hover {
|
||||
background-color: #231a2a;
|
||||
background-color: #231a2a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item .name-cell {
|
||||
color: #e2e8f0;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item .date-cell,
|
||||
[data-theme="dark"] .files-list-view .file-item .size-cell {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item .type-cell {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item.dragging {
|
||||
background-color: #334155;
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-item.drop-target {
|
||||
background-color: rgba(255, 193, 7, 0.05);
|
||||
background-color: rgba(255, 193, 7, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-actions {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .files-list-view .file-actions:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
/* SVG icon base styles (replaces inline <style> from icons.js) */
|
||||
.oxi-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.125em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
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;
|
||||
animation: oxi-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes oxi-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
/* Language Selector - Custom Dropdown */
|
||||
.language-selector {
|
||||
position: relative;
|
||||
margin-right: 15px;
|
||||
position: relative;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.language-selector-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background-color: #f0f3f7;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background-color: #f0f3f7;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.language-selector-toggle:hover {
|
||||
background-color: #e2e8f0;
|
||||
border-color: #cbd5e0;
|
||||
background-color: #e2e8f0;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.language-selector-toggle i {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.language-selector-toggle .lang-code {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.language-selector-toggle .dropdown-arrow {
|
||||
font-size: 10px;
|
||||
color: #718096;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: 2px;
|
||||
font-size: 10px;
|
||||
color: #718096;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.language-selector.open .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-selector-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.language-selector-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
.language-selector-dropdown::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
margin: 8px 0;
|
||||
background: transparent;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.language-selector-dropdown::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e0;
|
||||
border-radius: 3px;
|
||||
background-color: #cbd5e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.language-selector-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #a0aec0;
|
||||
background-color: #a0aec0;
|
||||
}
|
||||
|
||||
.language-selector.open .language-selector-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
transition: background-color 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background-color: #f7fafc;
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.language-option.active {
|
||||
background-color: #fff5f3;
|
||||
color: #ff5e3a;
|
||||
background-color: #fff5f3;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.language-option .lang-flag {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.language-option .lang-name {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.language-option .lang-check {
|
||||
color: #ff5e3a;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
color: #ff5e3a;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.language-option.active .lang-check {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-selector-toggle {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-selector-toggle:hover {
|
||||
background-color: #334155;
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-selector-dropdown {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-selector-dropdown::-webkit-scrollbar-thumb {
|
||||
background-color: #475569;
|
||||
background-color: #475569;
|
||||
}
|
||||
[data-theme="dark"] .language-selector-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #64748b;
|
||||
background-color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-option {
|
||||
color: #cbd5e1;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-option:hover {
|
||||
background-color: #334155;
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-option.active {
|
||||
background-color: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
background-color: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
+217
-215
@@ -1,381 +1,383 @@
|
||||
/* About Modal */
|
||||
.about-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 3000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 3000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.about-modal-overlay.show {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.about-modal {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
animation: userMenuIn 0.25s ease-out;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
animation: userMenuIn 0.25s ease-out;
|
||||
}
|
||||
|
||||
.about-modal-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
box-shadow: 0 8px 24px rgba(255, 94, 58, 0.3);
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
box-shadow: 0 8px 24px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.about-modal-logo i {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.about-modal h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.about-modal .about-version {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.about-modal .about-description {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.about-modal .about-tech {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.about-modal .about-tech-badge {
|
||||
padding: 4px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.about-modal .about-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.about-modal .about-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #ff5e3a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #ff5e3a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.about-modal .about-link:hover {
|
||||
opacity: 0.8;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.about-modal .about-close-btn {
|
||||
padding: 10px 32px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||
padding: 10px 32px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.about-modal .about-close-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
|
||||
}
|
||||
|
||||
/* Modern Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
width: 420px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
transition: transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
width: 420px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
transition: transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal-container {
|
||||
transform: scale(1) translateY(0);
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 14px;
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 14px;
|
||||
flex-shrink: 0;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 14px;
|
||||
margin-right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
margin-left: 14px;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-icon i {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f0f3f7;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f0f3f7;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
[dir='rtl'] & {
|
||||
left: 16px;
|
||||
right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
left: 16px;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #1a202c;
|
||||
background: #e2e8f0;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 15px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #1a202c;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 15px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #1a202c;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
border-color: #ff5e3a;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
|
||||
border-color: #ff5e3a;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.modal-input::placeholder {
|
||||
color: #a0aec0;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-footer .btn-secondary {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #4a5568;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.modal-footer .btn-secondary:hover {
|
||||
background: #f0f3f7;
|
||||
border-color: #cbd5e0;
|
||||
background: #f0f3f7;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary {
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary:hover {
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary:active {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .about-modal {
|
||||
background: #1e293b;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .about-modal h2 {
|
||||
color: #f1f5f9;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .about-modal .about-version,
|
||||
[data-theme="dark"] .about-modal .about-description {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .about-modal .about-tech-badge {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #cbd5e1;
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-container {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header {
|
||||
border-bottom-color: #334155;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header h3 {
|
||||
color: #f1f5f9;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-close-btn {
|
||||
background: #334155;
|
||||
color: #94a3b8;
|
||||
background: #334155;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-close-btn:hover {
|
||||
background: #475569;
|
||||
color: #e2e8f0;
|
||||
background: #475569;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-body label {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-input {
|
||||
border-color: #334155;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-color: #334155;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-input:focus {
|
||||
border-color: #ff5e3a;
|
||||
background: #0f172a;
|
||||
border-color: #ff5e3a;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-input::placeholder {
|
||||
color: #475569;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-footer {
|
||||
background: #162032;
|
||||
border-top-color: #334155;
|
||||
background: #162032;
|
||||
border-top-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-footer .btn-secondary {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #cbd5e1;
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-footer .btn-secondary:hover {
|
||||
background: #475569;
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
@@ -1,143 +1,150 @@
|
||||
/* Multi-Select – checkboxes & batch action bar */
|
||||
.list-header-checkbox,
|
||||
.file-item .checkbox-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.list-header-checkbox input[type="checkbox"],
|
||||
.file-item .checkbox-cell input[type="checkbox"] {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
cursor: pointer;
|
||||
accent-color: #ff5e3a;
|
||||
border-radius: 4px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
cursor: pointer;
|
||||
accent-color: #ff5e3a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.list-header.selection-mode {
|
||||
grid-template-columns: 36px 1fr;
|
||||
background-color: #1e293b;
|
||||
color: #fff;
|
||||
border-bottom-color: #334155;
|
||||
grid-template-columns: 36px 1fr;
|
||||
background-color: #1e293b;
|
||||
color: #fff;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
.list-header.selection-mode .list-header-checkbox input[type="checkbox"] {
|
||||
accent-color: #ff5e3a;
|
||||
accent-color: #ff5e3a;
|
||||
}
|
||||
|
||||
.batch-selection-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.batch-selection-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
margin: 0 0 12px;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition: opacity 0.2s, max-height 0.25s, transform 0.2s, margin 0.2s, padding 0.2s;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
margin: 0 0 12px;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
max-height 0.25s,
|
||||
transform 0.2s,
|
||||
margin 0.2s,
|
||||
padding 0.2s;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.batch-bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.batch-bar-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.7);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
|
||||
.batch-bar-close:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.batch-bar-count {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.batch-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.batch-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.batch-btn:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.batch-btn-danger {
|
||||
background: rgba(239,68,68,0.25);
|
||||
color: #fca5a5;
|
||||
background: rgba(239, 68, 68, 0.25);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.batch-btn-danger:hover {
|
||||
background: rgba(239,68,68,0.4);
|
||||
color: #fff;
|
||||
background: rgba(239, 68, 68, 0.4);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.batch-btn span {
|
||||
display: none;
|
||||
}
|
||||
.batch-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.batch-btn {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.batch-btn {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .list-header-checkbox input,
|
||||
[data-theme="dark"] .file-item .checkbox-cell input,
|
||||
[data-theme="dark"] .file-item .checkbox-cell input {
|
||||
accent-color: #ff5e3a;
|
||||
accent-color: #ff5e3a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .batch-selection-bar {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .list-header.selection-mode {
|
||||
background-color: #0f172a;
|
||||
border-bottom-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background-color: #0f172a;
|
||||
border-bottom-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -1,579 +1,614 @@
|
||||
/* Notification */
|
||||
.notification {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 20px;
|
||||
background-color: white;
|
||||
width: 250px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ff5e3a;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 20px;
|
||||
background-color: white;
|
||||
width: 250px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ff5e3a;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
|
||||
[dir='rtl'] & {
|
||||
left: 20px;
|
||||
border-right: 4px solid #ff5e3a;
|
||||
right: unset;
|
||||
border-left: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
left: 20px;
|
||||
border-right: 4px solid #ff5e3a;
|
||||
right: unset;
|
||||
border-left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
color: #2d3748;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Notification banner */
|
||||
.notification-banner {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 400px;
|
||||
z-index: 2000;
|
||||
transform: translateY(-100px);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 400px;
|
||||
z-index: 2000;
|
||||
transform: translateY(-100px);
|
||||
opacity: 0;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
}
|
||||
|
||||
.notification-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notification-banner.success {
|
||||
border-left: 4px solid #48bb78;
|
||||
border-left: 4px solid #48bb78;
|
||||
}
|
||||
|
||||
.notification-banner.error {
|
||||
border-left: 4px solid #f56565;
|
||||
border-left: 4px solid #f56565;
|
||||
}
|
||||
|
||||
.close-notification-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #a0aec0;
|
||||
margin-left: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #a0aec0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Notification Bell */
|
||||
.notif-wrapper {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notif-bell-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #64748b;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #64748b;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notif-bell-btn:hover {
|
||||
background: rgba(255, 94, 58, 0.08);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.08);
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.notif-bell-btn.active {
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.notif-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
border-radius: 8px;
|
||||
background: #ff3b30;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
border-radius: 8px;
|
||||
background: #ff3b30;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes bellRing {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
13% { transform: rotate(22deg); }
|
||||
26% { transform: rotate(-22deg); }
|
||||
39% { transform: rotate(14deg); }
|
||||
52% { transform: rotate(-14deg); }
|
||||
65% { transform: rotate(8deg); }
|
||||
78% { transform: rotate(-8deg); }
|
||||
91% { transform: rotate(3deg); }
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
13% {
|
||||
transform: rotate(22deg);
|
||||
}
|
||||
26% {
|
||||
transform: rotate(-22deg);
|
||||
}
|
||||
39% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
52% {
|
||||
transform: rotate(-14deg);
|
||||
}
|
||||
65% {
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
78% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
91% {
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notif-bell-btn.ring {
|
||||
animation: bellRing 1s ease;
|
||||
animation: bellRing 1s ease;
|
||||
}
|
||||
|
||||
.notif-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: -40px;
|
||||
width: 380px;
|
||||
max-height: 480px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05);
|
||||
z-index: 2000;
|
||||
overflow: hidden;
|
||||
animation: notifPanelIn 0.2s ease-out;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: -40px;
|
||||
width: 380px;
|
||||
max-height: 480px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
z-index: 2000;
|
||||
overflow: hidden;
|
||||
animation: notifPanelIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.notif-wrapper.open .notif-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes notifPanelIn {
|
||||
from { opacity: 0; transform: translateY(-8px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.notif-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.notif-panel-title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.notif-clear-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.notif-clear-btn:hover {
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.08);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.08);
|
||||
}
|
||||
|
||||
.notif-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.notif-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: #94a3b8;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: #94a3b8;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notif-empty i {
|
||||
font-size: 28px;
|
||||
opacity: 0.5;
|
||||
font-size: 28px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.notif-empty span {
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notif-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
transition: background 0.15s;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
transition: background 0.15s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.notif-item:last-child {
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notif-item:hover {
|
||||
background: #f8fafc;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.notif-item-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notif-item-icon.upload {
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.notif-item-icon.success {
|
||||
background: rgba(52, 199, 89, 0.1);
|
||||
color: #34c759;
|
||||
background: rgba(52, 199, 89, 0.1);
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.notif-item-icon.error {
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
color: #ff3b30;
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.notif-item-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notif-item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.notif-item-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notif-item-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.notif-upload-progress {
|
||||
margin-top: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.notif-upload-bar {
|
||||
height: 3px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
height: 3px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notif-upload-fill {
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.notif-upload-fill.done {
|
||||
background: #34c759;
|
||||
background: #34c759;
|
||||
}
|
||||
|
||||
.notif-upload-fill.error {
|
||||
background: #ff3b30;
|
||||
background: #ff3b30;
|
||||
}
|
||||
|
||||
.notif-upload-detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.notif-upload-pct,
|
||||
.notif-upload-stats {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Upload Progress Toast (legacy — hidden, kept for compat) */
|
||||
.upload-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 360px;
|
||||
max-height: 400px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
animation: uploadToastSlideIn 0.3s ease-out;
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 360px;
|
||||
max-height: 400px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
animation: uploadToastSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.upload-toast.visible {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes uploadToastSlideIn {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-toast-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #ff5e3a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #ff5e3a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.upload-toast-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
opacity: 0.8;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.upload-toast-close:hover {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.upload-toast-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
max-height: 260px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
.upload-toast-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-toast-file-icon {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-toast-file-icon.done {
|
||||
color: #34c759;
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.upload-toast-file-icon.error {
|
||||
color: #ff3b30;
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.upload-toast-file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.upload-toast-file-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.upload-toast-file-bar {
|
||||
height: 3px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
height: 3px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-toast-file-fill {
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.upload-toast-file-fill.done {
|
||||
background: #34c759;
|
||||
background: #34c759;
|
||||
}
|
||||
|
||||
.upload-toast-file-fill.error {
|
||||
background: #ff3b30;
|
||||
background: #ff3b30;
|
||||
}
|
||||
|
||||
.upload-toast-file-pct {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 38px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 38px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.upload-toast-footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.upload-toast-overall-bar {
|
||||
height: 4px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
height: 4px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.upload-toast-overall-fill {
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.25s ease;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
background: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.25s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.upload-toast-stats {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notification {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notification-title {
|
||||
color: #f1f5f9;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notification-message {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notification-banner {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-bell-btn {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-bell-btn:hover,
|
||||
[data-theme="dark"] .notif-bell-btn.active {
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.15);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-panel {
|
||||
background: #1e293b;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06);
|
||||
background: #1e293b;
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-panel-header {
|
||||
border-bottom-color: #334155;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-panel-title {
|
||||
color: #f1f5f9;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-clear-btn {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-clear-btn:hover {
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.12);
|
||||
color: #ff5e3a;
|
||||
background: rgba(255, 94, 58, 0.12);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-empty {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-item {
|
||||
border-bottom-color: #1a2536;
|
||||
border-bottom-color: #1a2536;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-item:hover {
|
||||
background: #162032;
|
||||
background: #162032;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-item-title {
|
||||
color: #e2e8f0;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-item-text {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .notif-upload-bar {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-toast {
|
||||
background: #1e293b;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
background: #1e293b;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-toast-file-name {
|
||||
color: #e2e8f0;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-toast-file-bar {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-toast-file-pct {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-toast-footer {
|
||||
border-top-color: #334155;
|
||||
border-top-color: #334155;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
/* Styles for search results */
|
||||
.search-results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-results-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.search-results-header .search-time {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-sort-select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-sort-select:focus {
|
||||
border-color: var(--primary-color, #4a90d9);
|
||||
border-color: var(--primary-color, #4a90d9);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
.permission-options {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.existing-share-item {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.share-url {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 5px;
|
||||
color: #1565c0;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 5px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.share-info {
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #757575;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.share-protected {
|
||||
color: #1565c0;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.share-expiration {
|
||||
color: #b71c1c;
|
||||
color: #b71c1c;
|
||||
}
|
||||
|
||||
.share-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.share-link-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: 12px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
#notification-message {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
/* Loading spinner overlay */
|
||||
.files-loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
gap: 16px;
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
gap: 16px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.files-loading-spinner .spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top-color: #ff5e3a;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top-color: #ff5e3a;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.files-loading-spinner span {
|
||||
font-size: 14px;
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
color: #666;
|
||||
display: none;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
color: #666;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropzone-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dropzone.active {
|
||||
border-color: #ff5e3a;
|
||||
background-color: rgba(255, 94, 58, 0.05);
|
||||
border-color: #ff5e3a;
|
||||
background-color: rgba(255, 94, 58, 0.05);
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: #f0f0f0;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
height: 100%;
|
||||
background-color: #ff5e3a;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dropzone {
|
||||
border-color: #475569;
|
||||
color: #94a3b8;
|
||||
background-color: #0f172a;
|
||||
border-color: #475569;
|
||||
color: #94a3b8;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .progress-bar {
|
||||
background-color: #334155;
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
/* Upload Dropdown */
|
||||
.upload-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.upload-dropdown .btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.upload-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
animation: dropdownFadeIn 0.15s ease-out;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
animation: dropdownFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.upload-dropdown-menu.show {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.upload-dropdown-item:hover {
|
||||
background: #f1f5f9;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.upload-dropdown-item:active {
|
||||
background: #e2e8f0;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.upload-dropdown-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.upload-dropdown-item:first-child {
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-dropdown-menu {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-dropdown-item {
|
||||
color: #cbd5e1;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-dropdown-item:hover {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-dropdown-item:active {
|
||||
background: #475569;
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .upload-dropdown-item i {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-caret {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
+180
-178
@@ -1,340 +1,342 @@
|
||||
/* User Menu */
|
||||
.user-menu-wrapper {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar-btn {
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar-btn:hover {
|
||||
border-color: rgba(255, 94, 58, 0.4);
|
||||
transform: scale(1.05);
|
||||
border-color: rgba(255, 94, 58, 0.4);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.user-avatar-btn:hover .user-avatar {
|
||||
box-shadow: 0 0 0 2px rgba(255, 94, 58, 0.2);
|
||||
box-shadow: 0 0 0 2px rgba(255, 94, 58, 0.2);
|
||||
}
|
||||
|
||||
.user-menu-wrapper.open .user-avatar-btn {
|
||||
border-color: #ff5e3a;
|
||||
border-color: #ff5e3a;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
user-select: none;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
width: 300px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
z-index: 2000;
|
||||
overflow: hidden;
|
||||
animation: userMenuIn 0.2s ease-out;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
width: 300px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
z-index: 2000;
|
||||
overflow: hidden;
|
||||
animation: userMenuIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.user-menu-wrapper.open .user-menu {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes userMenuIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px 20px 16px;
|
||||
background: linear-gradient(135deg, #fef5f3 0%, #fdf2f8 100%);
|
||||
border-bottom: 1px solid #fce7e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px 20px 16px;
|
||||
background: linear-gradient(135deg, #fef5f3 0%, #fdf2f8 100%);
|
||||
border-bottom: 1px solid #fce7e1;
|
||||
}
|
||||
|
||||
.user-menu-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
min-width: 48px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.3);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
min-width: 48px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.user-menu-info {
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-menu-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-menu-email {
|
||||
font-size: 12.5px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.user-menu-storage {
|
||||
padding: 14px 20px;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.user-menu-storage-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-menu-storage-label i {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.user-menu-storage-bar {
|
||||
height: 6px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
height: 6px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.user-menu-storage-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff5e3a, #ff2d55);
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width 0.5s ease;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff5e3a, #ff2d55);
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.user-menu-storage-text {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.user-menu-divider {
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 4px 0;
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.user-menu-item:hover {
|
||||
background: #f8fafc;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.user-menu-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.user-menu-item .theme-toggle-pill {
|
||||
margin-left: auto;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background 0.3s ease;
|
||||
margin-left: auto;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.user-menu-item .theme-toggle-pill.active {
|
||||
background: #ff5e3a;
|
||||
background: #ff5e3a;
|
||||
}
|
||||
|
||||
.user-menu-item .theme-toggle-knob {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.user-menu-item .theme-toggle-pill.active .theme-toggle-knob {
|
||||
transform: translateX(18px);
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.user-menu-admin {
|
||||
color: #1d4ed8;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.user-menu-admin i {
|
||||
color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.user-menu-admin:hover {
|
||||
background: #eff6ff;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.user-menu-role-badge {
|
||||
padding: 0 20px 4px;
|
||||
padding: 0 20px 4px;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.role-badge-admin {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.role-badge i {
|
||||
font-size: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.user-menu-logout {
|
||||
color: #ef4444;
|
||||
margin-bottom: 4px;
|
||||
color: #ef4444;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-menu-logout i {
|
||||
color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.user-menu-logout:hover {
|
||||
background: #fef2f2;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-header {
|
||||
background: linear-gradient(135deg, #1a2332 0%, #1e2940 100%);
|
||||
border-bottom-color: #334155;
|
||||
background: linear-gradient(135deg, #1a2332 0%, #1e2940 100%);
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-name {
|
||||
color: #f1f5f9;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-email {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-storage-label {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-storage-bar {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-storage-text {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-divider {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-item {
|
||||
color: #cbd5e1;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-item:hover {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-item i {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .theme-toggle-pill {
|
||||
background: #475569;
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-admin {
|
||||
color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-admin:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .role-badge-admin {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #60a5fa;
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu-logout:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
@@ -1,146 +1,145 @@
|
||||
/* Content area */
|
||||
.content-area {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-gutter: stable;
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #2d3748;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 12px;
|
||||
height: 60px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 12px;
|
||||
height: 60px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* invisible element, permits building of drag element without altering display */
|
||||
.drag-preview {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
pointer-events: none;
|
||||
width: 360px;
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
pointer-events: none;
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.dragged-items {
|
||||
--dragged-files-list-columns: 36px minmax(200px, 1fr);
|
||||
/*display: flex;*/
|
||||
flex-direction: column;
|
||||
/*width: 360px;*/
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
/*box-shadow: 0 1px 3px rgba(0,0,0,0.05);*/
|
||||
--dragged-files-list-columns: 36px minmax(200px, 1fr);
|
||||
/*display: flex;*/
|
||||
flex-direction: column;
|
||||
/*width: 360px;*/
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
/*box-shadow: 0 1px 3px rgba(0,0,0,0.05);*/
|
||||
}
|
||||
|
||||
.dragged-items .file-item {
|
||||
display: grid;
|
||||
grid-template-columns: var(--dragged-files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 12px 15px;
|
||||
/*border-bottom: 1px solid #f0f0f0;*/
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
display: grid;
|
||||
grid-template-columns: var(--dragged-files-list-columns);
|
||||
column-gap: 12px;
|
||||
padding: 12px 15px;
|
||||
/*border-bottom: 1px solid #f0f0f0;*/
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.dragged-items .file-item {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
width: 336px;
|
||||
height: 46px;
|
||||
.dragged-items .file-item {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
width: 336px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
/* file name, text is wrapped */
|
||||
.dragged-items .file-item div:nth-child(2) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dragged-items .file-item .file-icon {
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dragged-items .file-item .file-icon.folder-icon {
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
background-color: #ffeaa7;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dragged-items .file-item .file-icon.folder-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background-color: #fdcb6e;
|
||||
border-radius: 8px 8px 0 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background-color: #fdcb6e;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.dragged-items .file-item .file-icon.folder-icon i,
|
||||
.dragged-items .file-item .file-icon.folder-icon svg {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dragged-items div.fading {
|
||||
-webkit-mask-image: linear-gradient(to bottom, white 10%, transparent);
|
||||
mask-image: linear-gradient(to bottom, white 10%, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, white 10%, transparent);
|
||||
mask-image: linear-gradient(to bottom, white 10%, transparent);
|
||||
}
|
||||
|
||||
.dragged-items-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
transform: translate(50%, -50%);
|
||||
transform: translate(50%, -50%);
|
||||
|
||||
background: red;
|
||||
color: white;
|
||||
background: red;
|
||||
color: white;
|
||||
|
||||
border-radius: 50%;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
padding: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
+191
-151
@@ -1,226 +1,266 @@
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: linear-gradient(180deg, #2a3042 0%, #232838 100%);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 2px 0 12px rgba(0,0,0,0.15);
|
||||
transition: transform 0.3s ease;
|
||||
width: 250px;
|
||||
background: linear-gradient(180deg, #2a3042 0%, #232838 100%);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Sidebar overlay for mobile */
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-overlay.active {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Mobile responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
[dir='rtl'] .sidebar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
[dir="rtl"] .sidebar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
[dir='rtl'] .sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
[dir="rtl"] .sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
padding: 22px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.07);
|
||||
margin-bottom: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
padding: 22px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
margin-bottom: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff8a5c 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 3px 10px rgba(255,94,58,0.35);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff8a5c 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 3px 10px rgba(255, 94, 58, 0.35);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 12px;
|
||||
margin-right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
margin-left: 12px;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 14px rgba(255,94,58,0.45);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 14px rgba(255, 94, 58, 0.45);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: white;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 8px 12px;
|
||||
gap: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 8px 12px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px 14px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: rgba(255,255,255,0.65);
|
||||
font-size: 14.5px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px 14px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 14.5px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
[dir='rtl'] & {
|
||||
border-left: none;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
border-left: none;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: rgba(255,255,255,0.06);
|
||||
color: rgba(255,255,255,0.9);
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: rgba(255,94,58,0.12);
|
||||
color: #fff;
|
||||
border-left-color: #ff5e3a;
|
||||
font-weight: 600;
|
||||
background-color: rgba(255, 94, 58, 0.12);
|
||||
color: #fff;
|
||||
border-left-color: #ff5e3a;
|
||||
font-weight: 600;
|
||||
|
||||
[dir='rtl'] & {
|
||||
border-left-color: transparent;
|
||||
border-right-color: #ff5e3a;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
border-left-color: transparent;
|
||||
border-right-color: #ff5e3a;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item i,
|
||||
.nav-item .oxi-icon {
|
||||
margin-right: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
margin-right: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
transition:
|
||||
color 0.2s,
|
||||
transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 14px;
|
||||
margin-right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
margin-left: 14px;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored icons per section */
|
||||
.nav-item:nth-child(1) i, .nav-item:nth-child(1) .oxi-icon { color: #ffa94d; } /* Files - orange */
|
||||
.nav-item:nth-child(2) i, .nav-item:nth-child(2) .oxi-icon { color: #74b9ff; } /* Shared - blue */
|
||||
.nav-item:nth-child(3) i, .nav-item:nth-child(3) .oxi-icon { color: #81ecec; } /* Recent - teal */
|
||||
.nav-item:nth-child(4) i, .nav-item:nth-child(4) .oxi-icon { color: #ffd43b; } /* Favorites - gold */
|
||||
.nav-item:nth-child(5) i, .nav-item:nth-child(5) .oxi-icon { color: #ff6b9d; } /* Photos - pink */
|
||||
.nav-item:nth-child(6) i, .nav-item:nth-child(6) .oxi-icon { color: #ff7675; } /* Trash - red */
|
||||
.nav-item:nth-child(1) i,
|
||||
.nav-item:nth-child(1) .oxi-icon {
|
||||
color: #ffa94d;
|
||||
} /* Files - orange */
|
||||
.nav-item:nth-child(2) i,
|
||||
.nav-item:nth-child(2) .oxi-icon {
|
||||
color: #74b9ff;
|
||||
} /* Shared - blue */
|
||||
.nav-item:nth-child(3) i,
|
||||
.nav-item:nth-child(3) .oxi-icon {
|
||||
color: #81ecec;
|
||||
} /* Recent - teal */
|
||||
.nav-item:nth-child(4) i,
|
||||
.nav-item:nth-child(4) .oxi-icon {
|
||||
color: #ffd43b;
|
||||
} /* Favorites - gold */
|
||||
.nav-item:nth-child(5) i,
|
||||
.nav-item:nth-child(5) .oxi-icon {
|
||||
color: #ff6b9d;
|
||||
} /* Photos - pink */
|
||||
.nav-item:nth-child(6) i,
|
||||
.nav-item:nth-child(6) .oxi-icon {
|
||||
color: #ff7675;
|
||||
} /* Trash - red */
|
||||
|
||||
.nav-item.active:nth-child(1) i, .nav-item.active:nth-child(1) .oxi-icon { color: #ff5e3a; }
|
||||
.nav-item.active:nth-child(2) i, .nav-item.active:nth-child(2) .oxi-icon { color: #0984e3; }
|
||||
.nav-item.active:nth-child(3) i, .nav-item.active:nth-child(3) .oxi-icon { color: #00cec9; }
|
||||
.nav-item.active:nth-child(4) i, .nav-item.active:nth-child(4) .oxi-icon { color: #f0c800; }
|
||||
.nav-item.active:nth-child(5) i, .nav-item.active:nth-child(5) .oxi-icon { color: #e84393; }
|
||||
.nav-item.active:nth-child(6) i, .nav-item.active:nth-child(6) .oxi-icon { color: #e74c3c; }
|
||||
.nav-item.active:nth-child(1) i,
|
||||
.nav-item.active:nth-child(1) .oxi-icon {
|
||||
color: #ff5e3a;
|
||||
}
|
||||
.nav-item.active:nth-child(2) i,
|
||||
.nav-item.active:nth-child(2) .oxi-icon {
|
||||
color: #0984e3;
|
||||
}
|
||||
.nav-item.active:nth-child(3) i,
|
||||
.nav-item.active:nth-child(3) .oxi-icon {
|
||||
color: #00cec9;
|
||||
}
|
||||
.nav-item.active:nth-child(4) i,
|
||||
.nav-item.active:nth-child(4) .oxi-icon {
|
||||
color: #f0c800;
|
||||
}
|
||||
.nav-item.active:nth-child(5) i,
|
||||
.nav-item.active:nth-child(5) .oxi-icon {
|
||||
color: #e84393;
|
||||
}
|
||||
.nav-item.active:nth-child(6) i,
|
||||
.nav-item.active:nth-child(6) .oxi-icon {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.nav-item:hover i,
|
||||
.nav-item:hover .oxi-icon {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Storage indicator */
|
||||
.storage-container {
|
||||
margin: auto 12px 16px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin: auto 12px 16px 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.storage-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.8);
|
||||
letter-spacing: 0.3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.storage-bar {
|
||||
height: 6px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
height: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.storage-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff5e3a 0%, #ff8a5c 100%);
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width 0.8s ease;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff5e3a 0%, #ff8a5c 100%);
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width 0.8s ease;
|
||||
}
|
||||
|
||||
.storage-info {
|
||||
text-align: center;
|
||||
font-size: 11.5px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
+103
-101
@@ -1,163 +1,165 @@
|
||||
/* Main content */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top bar */
|
||||
.top-bar {
|
||||
height: 70px;
|
||||
min-height: 70px;
|
||||
max-height: 70px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
height: 70px;
|
||||
min-height: 70px;
|
||||
max-height: 70px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Sidebar toggle button (hidden on desktop) */
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: #4a5568;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: #4a5568;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background-color: #f7fafc;
|
||||
color: #ff5e3a;
|
||||
background-color: #f7fafc;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.sidebar-toggle i {
|
||||
font-size: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Mobile responsive styles for top bar */
|
||||
@media (max-width: 768px) {
|
||||
.top-bar {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.top-bar {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-grow: 1;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 20px;
|
||||
margin-right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
margin-left: 20px;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
width: 100%;
|
||||
padding: 12px 50px 12px 44px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
height: 46px;
|
||||
color: #1a202c;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
padding: 12px 50px 12px 44px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
height: 46px;
|
||||
color: #1a202c;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-container input:hover {
|
||||
border-color: #cbd5e0;
|
||||
background-color: #fff;
|
||||
border-color: #cbd5e0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-container input:focus {
|
||||
outline: none;
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
|
||||
outline: none;
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.search-container input::placeholder {
|
||||
color: #a0aec0;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #a0aec0;
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #a0aec0;
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
[dir='rtl'] & {
|
||||
right: 16px;
|
||||
left: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
right: 16px;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container input:focus + .search-icon,
|
||||
.search-container:focus-within .search-icon {
|
||||
color: #ff5e3a;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
|
||||
[dir='rtl'] & {
|
||||
left: 6px;
|
||||
right: unset;
|
||||
}
|
||||
[dir="rtl"] & {
|
||||
left: 6px;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
}
|
||||
|
||||
.search-button:active {
|
||||
transform: translateY(-50%) scale(0.98);
|
||||
transform: translateY(-50%) scale(0.98);
|
||||
}
|
||||
|
||||
.search-button i {
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
+1139
-247
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,7 @@
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -148,8 +148,14 @@
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.auth-subtitle { margin: 20px 0; color: var(--color-text-muted); font-size: 14px; }
|
||||
.auth-action-wrap { margin-top: 20px; }
|
||||
.auth-subtitle {
|
||||
margin: 20px 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.auth-action-wrap {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* SSO / OIDC button */
|
||||
.auth-button-oidc {
|
||||
@@ -188,7 +194,7 @@
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: '';
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
@@ -379,8 +385,14 @@
|
||||
}
|
||||
|
||||
@keyframes langPickerSlideDown {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Search inside dropdown */
|
||||
|
||||
@@ -1,89 +1,157 @@
|
||||
/* device-verify.css — stand-alone styles for the device authorization page */
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--danger: #dc2626;
|
||||
--danger-hover: #b91c1c;
|
||||
--success: #16a34a;
|
||||
--bg: #f8fafc;
|
||||
--card: #ffffff;
|
||||
--text: #1e293b;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--radius: 12px;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
padding: 2.5rem;
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
}
|
||||
.logo { text-align: center; margin-bottom: 1.5rem; }
|
||||
.logo h1 { font-size: 1.5rem; font-weight: 700; }
|
||||
.logo span { color: var(--primary); }
|
||||
h2 { font-size: 1.15rem; margin-bottom: 0.5rem; }
|
||||
p.subtitle { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||||
label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 0.4rem; }
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input[type="text"]:focus { border-color: var(--primary); }
|
||||
.device-info {
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.device-info .row { display: flex; justify-content: space-between; margin-bottom: 0.3rem; }
|
||||
.device-info .label { color: var(--muted); font-size: 0.85rem; }
|
||||
.device-info .value { font-weight: 600; font-size: 0.85rem; }
|
||||
.actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-approve { background: var(--primary); color: #fff; }
|
||||
.btn-approve:hover { background: var(--primary-hover); }
|
||||
.btn-deny { background: var(--danger); color: #fff; }
|
||||
.btn-deny:hover { background: var(--danger-hover); }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status.success { background: #dcfce7; color: var(--success); }
|
||||
.status.denied { background: #fef2f2; color: var(--danger); }
|
||||
.status.error { background: #fef2f2; color: var(--danger); }
|
||||
.error-text { color: var(--danger); font-size: 0.85rem; margin-top: 0.5rem; }
|
||||
.hidden { display: none !important; }
|
||||
/* device-verify.css — stand-alone styles for the device authorization page */
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--danger: #dc2626;
|
||||
--danger-hover: #b91c1c;
|
||||
--success: #16a34a;
|
||||
--bg: #f8fafc;
|
||||
--card: #ffffff;
|
||||
--text: #1e293b;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--radius: 12px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
padding: 2.5rem;
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
}
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.logo h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.logo span {
|
||||
color: var(--primary);
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p.subtitle {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.device-info {
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.device-info .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.device-info .label {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.device-info .value {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-approve {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-approve:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
.btn-deny {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-deny:hover {
|
||||
background: var(--danger-hover);
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status.success {
|
||||
background: #dcfce7;
|
||||
color: var(--success);
|
||||
}
|
||||
.status.denied {
|
||||
background: #fef2f2;
|
||||
color: var(--danger);
|
||||
}
|
||||
.status.error {
|
||||
background: #fef2f2;
|
||||
color: var(--danger);
|
||||
}
|
||||
.error-text {
|
||||
color: var(--danger);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
z-index: 5;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
left: 10px;
|
||||
right: unset;
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
.file-item.favorite-item {
|
||||
border-left: 3px solid #ffc107;
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
border-right: 3px solid #ffc107;
|
||||
border-left: unset;
|
||||
}
|
||||
@@ -89,9 +89,15 @@
|
||||
|
||||
/* Animation for the favorite star */
|
||||
@keyframes favorite-pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-indicator.active {
|
||||
@@ -119,7 +125,7 @@
|
||||
vertical-align: middle;
|
||||
filter: drop-shadow(0 0 1px rgba(255, 193, 7, 0.4));
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
margin-left: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
+201
-194
@@ -1,324 +1,331 @@
|
||||
/* OxiCloud Inline Viewer Styles */
|
||||
|
||||
.inline-viewer-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.inline-viewer-modal.active {
|
||||
display: flex !important;
|
||||
opacity: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
display: flex !important;
|
||||
opacity: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.inline-viewer-content {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
max-width: 1200px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
max-width: 1200px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.inline-viewer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.inline-viewer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inline-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;
|
||||
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;
|
||||
}
|
||||
|
||||
.inline-viewer-close:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.inline-viewer-container {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: #f0f3f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: #f0f3f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inline-viewer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.inline-viewer-download {
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.inline-viewer-download:hover {
|
||||
background-color: #e74c3c;
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
.inline-viewer-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inline-viewer-controls button {
|
||||
background-color: #f1f5f9;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: all 0.2s;
|
||||
background-color: #f1f5f9;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inline-viewer-controls button:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #334155;
|
||||
background-color: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* Image viewer */
|
||||
.inline-viewer-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transform-origin: center;
|
||||
transition: transform 0.2s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transform-origin: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* PDF viewer */
|
||||
.inline-viewer-pdf,
|
||||
.inline-viewer-pdf-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Only show fallback if object fails */
|
||||
.inline-viewer-pdf + .inline-viewer-pdf-fallback {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inline-viewer-pdf:not([data]),
|
||||
.inline-viewer-pdf[data=""] + .inline-viewer-pdf-fallback {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.inline-viewer-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 36px;
|
||||
color: #64748b;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 36px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Download progress container */
|
||||
.inline-viewer-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Progress bar track */
|
||||
.inline-viewer-progress-bar {
|
||||
width: 200px;
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
width: 200px;
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Progress bar fill */
|
||||
.inline-viewer-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
border-radius: 4px;
|
||||
transition: width 0.2s ease;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
border-radius: 4px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* Progress percentage text */
|
||||
.inline-viewer-progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-theme="dark"] .inline-viewer-progress {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .inline-viewer-progress-bar {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .inline-viewer-progress-text {
|
||||
color: #cbd5e1;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Error and unsupported message */
|
||||
.inline-viewer-message {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.inline-viewer-icon {
|
||||
font-size: 64px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 24px;
|
||||
font-size: 64px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.inline-viewer-text {
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.inline-viewer-text p {
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* Text viewer */
|
||||
.inline-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;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Video viewer */
|
||||
.inline-viewer-video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
/* Audio viewer */
|
||||
.inline-viewer-audio-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 48px 32px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 48px 32px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-viewer-audio-icon {
|
||||
font-size: 80px;
|
||||
color: #94a3b8;
|
||||
animation: audio-pulse 2s ease-in-out infinite;
|
||||
font-size: 80px;
|
||||
color: #94a3b8;
|
||||
animation: audio-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes audio-pulse {
|
||||
0%, 100% { opacity: 0.6; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.05); }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.inline-viewer-audio-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.inline-viewer-audio {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.inline-viewer-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.inline-viewer-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.inline-viewer-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.inline-viewer-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
border: 2px solid #e2e8f0;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.photo-tile img {
|
||||
@@ -93,7 +96,7 @@
|
||||
|
||||
.photo-tile:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
@@ -134,7 +137,9 @@
|
||||
.photo-tile.selected {
|
||||
border-color: #ff5e3a;
|
||||
background: #fff8f6;
|
||||
box-shadow: 0 0 0 1px rgba(255, 94, 58, 0.15), 0 4px 12px rgba(255, 94, 58, 0.1);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 94, 58, 0.15),
|
||||
0 4px 12px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.photo-tile.selected img {
|
||||
@@ -221,7 +226,9 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Selection bar */
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 70px 16px 20px;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);
|
||||
color: #fff;
|
||||
z-index: 10001;
|
||||
}
|
||||
@@ -129,7 +129,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), transparent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
+967
-230
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
color: #6c757d;
|
||||
z-index: 5;
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
left: 10px;
|
||||
right: unset;
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
.files-grid-view .file-item.recent-item {
|
||||
border-left: 3px solid #6c757d;
|
||||
|
||||
[dir='rtl'] & {
|
||||
[dir="rtl"] & {
|
||||
border-right: 3px solid #6c757d;
|
||||
border-left: unset;
|
||||
}
|
||||
@@ -87,6 +87,12 @@
|
||||
}
|
||||
|
||||
@keyframes recent-fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
+229
-229
@@ -1,420 +1,420 @@
|
||||
.page-description {
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
margin-top: -15px;
|
||||
margin-bottom: 25px;
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
margin-top: -15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.shared-page-container {
|
||||
max-width: 1280px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
max-width: 1280px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.shared-header {
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.shared-header h2 {
|
||||
font-size: 24px;
|
||||
color: #2d3748;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
color: #2d3748;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.shared-header p {
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* In-app Shared view (index.html dynamic container) */
|
||||
.shared-view-container .shared-header {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-custom-select {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a202c;
|
||||
cursor: pointer;
|
||||
min-width: 170px;
|
||||
height: 44px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a202c;
|
||||
cursor: pointer;
|
||||
min-width: 170px;
|
||||
height: 44px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-toggle:hover {
|
||||
border-color: #cbd5e0;
|
||||
background-color: #fff;
|
||||
border-color: #cbd5e0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-custom-select.open .shared-select-toggle {
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-arrow {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
color: #718096;
|
||||
transition: transform 0.2s ease;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
color: #718096;
|
||||
transition: transform 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-custom-select.open .shared-select-arrow {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-custom-select.open .shared-select-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4a5568;
|
||||
transition: background-color 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4a5568;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-option:hover {
|
||||
background-color: #f7fafc;
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.shared-view-container .shared-select-option.active {
|
||||
background-color: #fff5f3;
|
||||
color: #ff5e3a;
|
||||
font-weight: 500;
|
||||
background-color: #fff5f3;
|
||||
color: #ff5e3a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shared-filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
width: 250px;
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.shared-list-container {
|
||||
background-color: white;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
margin-bottom: 25px;
|
||||
background-color: white;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.shared-list,
|
||||
.shared-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.shared-list thead th,
|
||||
.shared-table thead th {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
}
|
||||
|
||||
.shared-list tbody td,
|
||||
.shared-table tbody td {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.shared-item-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.shared-item-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.shared-list .action-btn,
|
||||
.shared-table .action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.shared-list .action-btn:hover,
|
||||
.shared-table .action-btn:hover {
|
||||
background-color: #f0f8ff;
|
||||
border-color: #90cdf4;
|
||||
background-color: #f0f8ff;
|
||||
border-color: #90cdf4;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#empty-shared-state.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50px 20px;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50px 20px;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
#empty-shared-state p {
|
||||
margin-bottom: 10px;
|
||||
max-width: 400px;
|
||||
margin-bottom: 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shared-filters {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.shared-filters {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.shared-list thead th:nth-child(4),
|
||||
.shared-list thead th:nth-child(5),
|
||||
.shared-list tbody td:nth-child(4),
|
||||
.shared-list tbody td:nth-child(5),
|
||||
.shared-table thead th:nth-child(4),
|
||||
.shared-table thead th:nth-child(5),
|
||||
.shared-table tbody td:nth-child(4),
|
||||
.shared-table tbody td:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
.shared-list thead th:nth-child(4),
|
||||
.shared-list thead th:nth-child(5),
|
||||
.shared-list tbody td:nth-child(4),
|
||||
.shared-list tbody td:nth-child(5),
|
||||
.shared-table thead th:nth-child(4),
|
||||
.shared-table thead th:nth-child(5),
|
||||
.shared-table tbody td:nth-child(4),
|
||||
.shared-table tbody td:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-filters {
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-toggle {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-filters {
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-toggle:hover {
|
||||
background-color: #334155;
|
||||
border-color: #475569;
|
||||
background-color: #334155;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-custom-select.open .shared-select-toggle {
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.15);
|
||||
background-color: #1e293b;
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.15);
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-arrow {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-dropdown {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-option {
|
||||
color: #cbd5e1;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-option:hover {
|
||||
background-color: #334155;
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-view-container .shared-select-option.active {
|
||||
background-color: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
background-color: rgba(255, 94, 58, 0.1);
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .filter-group label {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .filter-group select {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme="dark"] .filter-group select:focus {
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.15);
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-box input {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-box input::placeholder {
|
||||
color: #64748b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list-container {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list thead th,
|
||||
[data-theme="dark"] .shared-table thead th {
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
background-color: #162032;
|
||||
color: #94a3b8;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list tbody td,
|
||||
[data-theme="dark"] .shared-table tbody td {
|
||||
border-bottom-color: #334155;
|
||||
color: #e2e8f0;
|
||||
border-bottom-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list tbody tr:hover,
|
||||
[data-theme="dark"] .shared-table tbody tr:hover {
|
||||
background-color: #162032;
|
||||
background-color: #162032;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .page-description {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #empty-shared-state.empty-state {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
color: #64748b;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #empty-shared-state p {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list .action-btn,
|
||||
[data-theme="dark"] .shared-table .action-btn {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-list .action-btn:hover,
|
||||
[data-theme="dark"] .shared-table .action-btn:hover {
|
||||
color: #ff5e3a;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
+31
-31
@@ -1,68 +1,68 @@
|
||||
.trash-item {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trash-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.files-grid-view .file-card.trash-item:hover .trash-actions,
|
||||
.files-list-view .file-item.trash-item:hover .actions-cell {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.trash-actions button,
|
||||
.actions-cell button {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.trash-actions button:hover,
|
||||
.actions-cell button:hover {
|
||||
background: #f0f0f0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-restore {
|
||||
color: #4caf50;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
color: #f44336;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .trash-actions button,
|
||||
[data-theme="dark"] .actions-cell button {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .trash-actions button:hover,
|
||||
[data-theme="dark"] .actions-cell button:hover {
|
||||
background: #334155;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ async function refreshUserData() {
|
||||
|
||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||
window.updateStorageUsageDisplay(userData);
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing user data:', error);
|
||||
@@ -82,7 +81,7 @@ async function checkAuthentication() {
|
||||
if (userData.username) {
|
||||
// We have cached user data — render immediately, refresh in background
|
||||
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => {
|
||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => {
|
||||
el.textContent = userInitials;
|
||||
});
|
||||
const menuName = document.getElementById('user-menu-name');
|
||||
@@ -103,7 +102,10 @@ async function checkAuthentication() {
|
||||
const r = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getCsrfHeaders()
|
||||
},
|
||||
body: '{}'
|
||||
});
|
||||
if (r.ok) {
|
||||
@@ -128,7 +130,7 @@ async function checkAuthentication() {
|
||||
const freshData = await refreshUserData();
|
||||
if (freshData && freshData.username) {
|
||||
const userInitials = freshData.username.substring(0, 2).toUpperCase();
|
||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => el.textContent = userInitials);
|
||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => (el.textContent = userInitials));
|
||||
window.updateStorageUsageDisplay(freshData);
|
||||
resolveHomeFolder().then(() => window.loadFiles());
|
||||
} else {
|
||||
@@ -154,7 +156,9 @@ async function resolveHomeFolder() {
|
||||
|
||||
if (app.userHomeFolderId) return;
|
||||
try {
|
||||
const response = await fetch('/api/folders', { credentials: 'same-origin' });
|
||||
const response = await fetch('/api/folders', {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`Could not fetch home folder: ${response.status}`);
|
||||
return;
|
||||
|
||||
+31
-30
@@ -21,12 +21,11 @@
|
||||
* @param {string} id the id of the folder
|
||||
* @returns {Promise<FolderInfo>}
|
||||
*/
|
||||
async function getFolder( id) {
|
||||
|
||||
async function getFolder(id) {
|
||||
/** @type {HeadersInit} */
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache'
|
||||
Pragma: 'no-cache'
|
||||
};
|
||||
|
||||
/** @type {RequestInit} */
|
||||
@@ -35,12 +34,11 @@ async function getFolder( id) {
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
};
|
||||
|
||||
let folderInformations = await fetch( `/api/folders/${id}`, requestOptions);
|
||||
|
||||
let folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
|
||||
if (folderInformations.ok) {
|
||||
return folderInformations.json()
|
||||
}
|
||||
else {
|
||||
return folderInformations.json();
|
||||
} else {
|
||||
console.warn(`Error fetching folder ${id}`);
|
||||
return Promise.reject(null);
|
||||
}
|
||||
@@ -50,15 +48,15 @@ async function getFolder( id) {
|
||||
* rebuild breadcrumb from selected folder (iterate up to root)
|
||||
*/
|
||||
async function rebuildBreadCrumb() {
|
||||
|
||||
const app = window.app;
|
||||
/**
|
||||
|
||||
/**
|
||||
* Store the leaf (this is the current displayed folder)
|
||||
* @type {FolderInfo | null}
|
||||
* @type {FolderInfo | null}
|
||||
*/
|
||||
let currentFolderInfo = null;
|
||||
|
||||
// rebuild full breadcrumb,
|
||||
// rebuild full breadcrumb,
|
||||
// TODO: to optimize, data may already be known / or ETAG could be interesting to reduce load
|
||||
app.breadcrumbPath = [];
|
||||
|
||||
@@ -77,61 +75,64 @@ async function rebuildBreadCrumb() {
|
||||
}
|
||||
|
||||
// XXX do not enter root into bread crumb updateBreadcrumb() method always display it
|
||||
if(!folderInfo.is_root) {
|
||||
app.breadcrumbPath.unshift( {id: folderInfo.id, name: folderInfo.name});
|
||||
if (!folderInfo.is_root) {
|
||||
app.breadcrumbPath.unshift({
|
||||
id: folderInfo.id,
|
||||
name: folderInfo.name
|
||||
});
|
||||
}
|
||||
|
||||
// iterate to parent folder
|
||||
id = folderInfo.parent_id;
|
||||
}
|
||||
catch( e) {
|
||||
} catch (e) {
|
||||
console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`);
|
||||
// fallback of root
|
||||
window.uiNotifications.show(
|
||||
'error: folder not found or permission denied',
|
||||
'the given folder is not available or you do not have sufficient rights'
|
||||
'error: folder not found or permission denied',
|
||||
'the given folder is not available or you do not have sufficient rights'
|
||||
);
|
||||
app.breadcrumbPath = [];
|
||||
id = app.userHomeFolderId;
|
||||
app.currentPath = id;
|
||||
}
|
||||
}
|
||||
// store informations on the current folder
|
||||
|
||||
// store informations on the current folder
|
||||
app.currentFolderInfo = currentFolderInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files view loading logic
|
||||
*
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.insertHistory] add browser history (default true)
|
||||
* @param {boolean} [options.forceRefresh] force refresh of content
|
||||
*/
|
||||
async function loadFiles(options = { insertHistory: true}) {
|
||||
async function loadFiles(options = { insertHistory: true }) {
|
||||
const app = window.app;
|
||||
const elements = window.appElements;
|
||||
|
||||
try {
|
||||
console.log("Starting loadFiles() - loading files...", options);
|
||||
console.log('Starting loadFiles() - loading files...', options);
|
||||
|
||||
const forceRefresh = options.forceRefresh || false;
|
||||
|
||||
if (window.isLoadingFiles) {
|
||||
console.log("A file load is already in progress, ignoring request");
|
||||
console.log('A file load is already in progress, ignoring request');
|
||||
return;
|
||||
}
|
||||
|
||||
window.isLoadingFiles = true;
|
||||
|
||||
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
|
||||
let loadingFiles = setTimeout( () => {
|
||||
let loadingFiles = setTimeout(() => {
|
||||
// display loader after few delay (will be canceled if result take less time)
|
||||
window.ui.showError(`
|
||||
<div class="files-loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
||||
</div>
|
||||
`)
|
||||
`);
|
||||
}, 100);
|
||||
|
||||
if (!app.userHomeFolderId) {
|
||||
@@ -145,7 +146,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
||||
// request a breadcrumb paint
|
||||
window.ui.updateBreadcrumb();
|
||||
|
||||
window.updateHistory( options.insertHistory || false);
|
||||
window.updateHistory(options.insertHistory || false);
|
||||
|
||||
let url;
|
||||
|
||||
@@ -158,7 +159,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
||||
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
||||
} else {
|
||||
url = `/api/folders?t=${timestamp}`;
|
||||
console.warn("Emergency fallback to root folder - this should not normally happen");
|
||||
console.warn('Emergency fallback to root folder - this should not normally happen');
|
||||
}
|
||||
} else {
|
||||
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
|
||||
@@ -168,7 +169,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
||||
/** @type {HeadersInit} */
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache'
|
||||
Pragma: 'no-cache'
|
||||
};
|
||||
|
||||
/** @type {RequestInit} */
|
||||
@@ -189,10 +190,10 @@ async function loadFiles(options = { insertHistory: true}) {
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
// not required anymore
|
||||
clearTimeout( loadingFiles);
|
||||
clearTimeout(loadingFiles);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn("Auth error when loading files, showing empty list");
|
||||
console.warn('Auth error when loading files, showing empty list');
|
||||
// FIXME: i18n
|
||||
window.ui.showError(`<p>Could not load files</p>`);
|
||||
return;
|
||||
|
||||
+47
-49
@@ -103,9 +103,9 @@ function setActionsBarMode(mode, force = false) {
|
||||
if (!elements.actionsBar) return;
|
||||
|
||||
if (mode === 'hidden') {
|
||||
elements.actionsBar.classList.add("hidden");
|
||||
elements.actionsBar.classList.add('hidden');
|
||||
elements.actionsBar.dataset.mode = 'hidden';
|
||||
console.log("......setup actions bar to hidden");
|
||||
console.log('......setup actions bar to hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function setActionsBarMode(mode, force = false) {
|
||||
if (!html) return;
|
||||
|
||||
elements.actionsBar.innerHTML = html;
|
||||
elements.actionsBar.classList.remove("hidden");
|
||||
elements.actionsBar.classList.remove('hidden');
|
||||
elements.actionsBar.dataset.mode = mode;
|
||||
|
||||
// Refresh cached action elements after rebuild
|
||||
@@ -220,7 +220,7 @@ function deserializeHash() {
|
||||
// FIXME rename files into drive ?
|
||||
hashContext.section = 'files';
|
||||
|
||||
const hash_elements = window.location.hash.split("/");
|
||||
const hash_elements = window.location.hash.split('/');
|
||||
|
||||
const section = hash_elements[1];
|
||||
|
||||
@@ -245,13 +245,13 @@ function deserializeHash() {
|
||||
*
|
||||
* @param {boolean} insertHistory true to change url and browser's history, false to change url only
|
||||
*/
|
||||
function updateHistory( insertHistory) {
|
||||
function updateHistory(insertHistory) {
|
||||
const app = window.app;
|
||||
|
||||
let historyData = {
|
||||
section: app.currentSection,
|
||||
id: app.currentFolder,
|
||||
file: app.viewFile,
|
||||
file: app.viewFile
|
||||
};
|
||||
|
||||
let historyUrl = `#/${app.currentSection}`;
|
||||
@@ -268,12 +268,11 @@ function updateHistory( insertHistory) {
|
||||
}
|
||||
|
||||
if (insertHistory) {
|
||||
console.log(`adding history with ${historyUrl}`)
|
||||
window.history.pushState(historyData, "", historyUrl);
|
||||
}
|
||||
else {
|
||||
console.log(`replace history with ${historyUrl}`)
|
||||
window.history.replaceState(historyData, "", historyUrl);
|
||||
console.log(`adding history with ${historyUrl}`);
|
||||
window.history.pushState(historyData, '', historyUrl);
|
||||
} else {
|
||||
console.log(`replace history with ${historyUrl}`);
|
||||
window.history.replaceState(historyData, '', historyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,33 +282,32 @@ function updateHistory( insertHistory) {
|
||||
* @returns
|
||||
*/
|
||||
function switchSectionTo(section) {
|
||||
|
||||
if (window.app.currentSection === section)
|
||||
// no change ...
|
||||
return;
|
||||
|
||||
switch (section) {
|
||||
case "files":
|
||||
case 'files':
|
||||
switchToFilesSection();
|
||||
break;
|
||||
|
||||
case "shared":
|
||||
case 'shared':
|
||||
switchToSharedSection();
|
||||
break;
|
||||
|
||||
case "recent":
|
||||
case 'recent':
|
||||
switchToRecentFilesSection();
|
||||
break;
|
||||
|
||||
case "favorites":
|
||||
case 'favorites':
|
||||
switchToFavoritesSection();
|
||||
break;
|
||||
|
||||
case "photos":
|
||||
case 'photos':
|
||||
switchToPhotosSection();
|
||||
break;
|
||||
|
||||
case "trash":
|
||||
case 'trash':
|
||||
switchToTrashSection();
|
||||
break;
|
||||
|
||||
@@ -375,8 +373,8 @@ function initApp() {
|
||||
window.addEventListener('authenticationDone', () => {
|
||||
// Check if a context was provided in the URL
|
||||
let hashContext = deserializeHash();
|
||||
switchSectionTo( hashContext.section);
|
||||
if (hashContext.section === "files") {
|
||||
switchSectionTo(hashContext.section);
|
||||
if (hashContext.section === 'files') {
|
||||
if (hashContext.path) {
|
||||
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
|
||||
window.app.currentPath = hashContext.path;
|
||||
@@ -387,7 +385,6 @@ function initApp() {
|
||||
}
|
||||
window.loadFiles();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Wait for translations to load before checking authentication
|
||||
@@ -448,15 +445,19 @@ function setupUploadDropdown() {
|
||||
const signal = uploadDropdownBindingsController.signal;
|
||||
|
||||
// Toggle dropdown on button click
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = menu.classList.contains('show');
|
||||
// Close any other open dropdowns
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
||||
if (!isOpen) {
|
||||
menu.classList.add('show');
|
||||
}
|
||||
}, { signal });
|
||||
uploadBtn.addEventListener(
|
||||
'click',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = menu.classList.contains('show');
|
||||
// Close any other open dropdowns
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
|
||||
if (!isOpen) {
|
||||
menu.classList.add('show');
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
// remove+add stable handler: guarantees exactly one global listener
|
||||
@@ -466,7 +467,7 @@ function setupUploadDropdown() {
|
||||
}
|
||||
uploadDropdownDocumentClickHandler = (e) => {
|
||||
if (e.target.closest('#upload-dropdown')) return;
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
|
||||
};
|
||||
// @ts-ignore
|
||||
document.addEventListener('click', uploadDropdownDocumentClickHandler);
|
||||
@@ -485,21 +486,20 @@ function setupEventListeners() {
|
||||
const SEARCH_MIN_CHARS = 3;
|
||||
|
||||
// handle history / url change
|
||||
window.addEventListener("popstate", (e) => {
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (e.state === null) {
|
||||
// change is from user (url explicitely change, read information from hash)
|
||||
let hashContext = deserializeHash();
|
||||
switchSectionTo( hashContext.section);
|
||||
const hashContext = deserializeHash();
|
||||
switchSectionTo(hashContext.section);
|
||||
if (hashContext.path) {
|
||||
window.app.currentPath = hashContext.path;
|
||||
window.loadFiles({insertHistory: false});
|
||||
window.loadFiles({ insertHistory: false });
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// change is from history, data provided in event
|
||||
switchSectionTo( e.state.section);
|
||||
switchSectionTo(e.state.section);
|
||||
window.app.currentPath = e.state.id;
|
||||
window.loadFiles({insertHistory: false});
|
||||
window.loadFiles({ insertHistory: false });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -584,17 +584,17 @@ function setupEventListeners() {
|
||||
}
|
||||
|
||||
// Sidebar navigation
|
||||
elements.navItems.forEach(item => {
|
||||
elements.navItems.forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
// Remove active class from all nav items
|
||||
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
elements.navItems.forEach((navItem) => navItem.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked item
|
||||
item.classList.add('active');
|
||||
let _updateHistory = true;
|
||||
|
||||
let itemI18nKey=item.querySelector('span').getAttribute('data-i18n')
|
||||
switch(itemI18nKey) {
|
||||
let itemI18nKey = item.querySelector('span').getAttribute('data-i18n');
|
||||
switch (itemI18nKey) {
|
||||
case 'nav.shared':
|
||||
// Switch to shared view
|
||||
switchToSharedSection();
|
||||
@@ -628,7 +628,7 @@ function setupEventListeners() {
|
||||
document.title = `OxiCloud: ${window.i18n.t(itemI18nKey)}`;
|
||||
|
||||
if (_updateHistory) {
|
||||
updateHistory( true);
|
||||
updateHistory(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -649,13 +649,11 @@ function setupEventListeners() {
|
||||
const folderMenu = document.getElementById('folder-context-menu');
|
||||
const fileMenu = document.getElementById('file-context-menu');
|
||||
|
||||
if (folderMenu && folderMenu.style.display === 'block' &&
|
||||
!folderMenu.contains(e.target)) {
|
||||
if (folderMenu && folderMenu.style.display === 'block' && !folderMenu.contains(e.target)) {
|
||||
ui.closeContextMenu();
|
||||
}
|
||||
|
||||
if (fileMenu && fileMenu.style.display === 'block' &&
|
||||
!fileMenu.contains(e.target)) {
|
||||
if (fileMenu && fileMenu.style.display === 'block' && !fileMenu.contains(e.target)) {
|
||||
ui.closeFileContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
+70
-78
@@ -14,15 +14,14 @@ function syncViewContainers() {
|
||||
|
||||
const isGrid = window.app.currentView === 'grid';
|
||||
if (isGrid) {
|
||||
filesList.classList.remove("files-list-view");
|
||||
filesList.classList.add("files-grid-view");
|
||||
filesList.classList.remove('files-list-view');
|
||||
filesList.classList.add('files-grid-view');
|
||||
|
||||
gridViewBtn?.classList.add('active');
|
||||
listViewBtn?.classList.remove('active');
|
||||
}
|
||||
else {
|
||||
filesList.classList.add("files-list-view");
|
||||
filesList.classList.remove("files-grid-view");
|
||||
} else {
|
||||
filesList.classList.add('files-list-view');
|
||||
filesList.classList.remove('files-grid-view');
|
||||
|
||||
gridViewBtn?.classList.remove('active');
|
||||
listViewBtn?.classList.add('active');
|
||||
@@ -42,61 +41,61 @@ function toggleFileContainer(show) {
|
||||
* Mobile sidebar toggle functionality
|
||||
*/
|
||||
function initSidebarToggle() {
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
|
||||
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
|
||||
|
||||
function openSidebar() {
|
||||
sidebar.classList.add('open');
|
||||
sidebarOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function openSidebar() {
|
||||
sidebar.classList.add('open');
|
||||
sidebarOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
function closeSidebar() {
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
if (sidebar.classList.contains('open')) {
|
||||
closeSidebar();
|
||||
} else {
|
||||
openSidebar();
|
||||
}
|
||||
}
|
||||
function toggleSidebar() {
|
||||
if (sidebar.classList.contains('open')) {
|
||||
closeSidebar();
|
||||
} else {
|
||||
openSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle button click
|
||||
sidebarToggle.addEventListener('click', toggleSidebar);
|
||||
// Toggle button click
|
||||
sidebarToggle.addEventListener('click', toggleSidebar);
|
||||
|
||||
// Close sidebar when clicking overlay
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
// Close sidebar when clicking overlay
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
|
||||
// Close sidebar on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
// Close sidebar on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close sidebar when navigating (nav item click on mobile)
|
||||
const navItems = sidebar.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Close sidebar when navigating (nav item click on mobile)
|
||||
const navItems = sidebar.querySelectorAll('.nav-item');
|
||||
navItems.forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Expose functions globally
|
||||
window.sidebarToggle = {
|
||||
open: openSidebar,
|
||||
close: closeSidebar,
|
||||
toggle: toggleSidebar
|
||||
};
|
||||
// Expose functions globally
|
||||
window.sidebarToggle = {
|
||||
open: openSidebar,
|
||||
close: closeSidebar,
|
||||
toggle: toggleSidebar
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize sidebar toggle when DOM is ready
|
||||
@@ -104,12 +103,12 @@ document.addEventListener('DOMContentLoaded', initSidebarToggle);
|
||||
|
||||
// Mapping of section names to their corresponding view flags
|
||||
const VIEW_FLAGS = {
|
||||
'files': 'isFilesView',
|
||||
'shared': 'isSharedView',
|
||||
'recent': 'isRecentView',
|
||||
'favorites': 'isFavoritesView',
|
||||
'trash': 'isTrashView',
|
||||
'photos': 'isPhotosView'
|
||||
files: 'isFilesView',
|
||||
shared: 'isSharedView',
|
||||
recent: 'isRecentView',
|
||||
favorites: 'isFavoritesView',
|
||||
trash: 'isTrashView',
|
||||
photos: 'isPhotosView'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -128,18 +127,17 @@ function getSectionFromNavItem(navItem) {
|
||||
* @returns {boolean} true if the section changed
|
||||
*/
|
||||
function setCurrentSection(section) {
|
||||
|
||||
if (window.app.currentSection == section) return false;
|
||||
|
||||
// Set all view flags - true for active section, false for others
|
||||
Object.entries(VIEW_FLAGS).forEach(([key, flag]) => {
|
||||
window.app[flag] = (key === section);
|
||||
window.app[flag] = key === section;
|
||||
});
|
||||
|
||||
window.app.currentSection = section;
|
||||
|
||||
// Update nav item active classes by finding matching item from DOM
|
||||
window.appElements.navItems.forEach(item => {
|
||||
window.appElements.navItems.forEach((item) => {
|
||||
const itemSection = getSectionFromNavItem(item);
|
||||
item.classList.toggle('active', itemSection === section);
|
||||
});
|
||||
@@ -164,12 +162,11 @@ function setCurrentSection(section) {
|
||||
}
|
||||
|
||||
function switchToSharedSection() {
|
||||
|
||||
if (!setCurrentSection('shared')) return;
|
||||
|
||||
// Hide breadcrumb (only shown in Files view)
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
breadcrumb?.classList.add('hidden');
|
||||
|
||||
// Hide actions-bar for shared view
|
||||
window.setActionsBarMode('hidden');
|
||||
@@ -178,14 +175,13 @@ function switchToSharedSection() {
|
||||
|
||||
//reset files view + remove any error
|
||||
window.ui.resetFilesList();
|
||||
|
||||
|
||||
// Show shared view
|
||||
if (window.sharedView) {
|
||||
window.sharedView.init();
|
||||
window.sharedView.show();
|
||||
}
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
|
||||
}
|
||||
|
||||
function switchToFilesSection() {
|
||||
@@ -196,7 +192,7 @@ function switchToFilesSection() {
|
||||
|
||||
// Show breadcrumb (only in Files view)
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.remove("hidden");
|
||||
breadcrumb?.classList.remove('hidden');
|
||||
|
||||
// show files container
|
||||
toggleFileContainer(true);
|
||||
@@ -224,14 +220,14 @@ function switchToFavoritesSection() {
|
||||
|
||||
// Hide breadcrumb (only shown in Files view)
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
breadcrumb?.classList.add('hidden');
|
||||
|
||||
// show files container
|
||||
toggleFileContainer(true);
|
||||
|
||||
// ensure correct view
|
||||
syncViewContainers();
|
||||
|
||||
|
||||
//reset files view + remove any error
|
||||
window.ui.resetFilesList();
|
||||
|
||||
@@ -242,12 +238,10 @@ function switchToFavoritesSection() {
|
||||
window.ui.showError(`
|
||||
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
||||
<p>Error loading the favorites module</p>
|
||||
`
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
}
|
||||
|
||||
function switchToRecentFilesSection() {
|
||||
@@ -258,7 +252,7 @@ function switchToRecentFilesSection() {
|
||||
|
||||
// Hide breadcrumb (only shown in Files view)
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
breadcrumb?.classList.add('hidden');
|
||||
|
||||
// show files container
|
||||
toggleFileContainer(true);
|
||||
@@ -276,8 +270,7 @@ function switchToRecentFilesSection() {
|
||||
window.ui.showError(`
|
||||
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
||||
<p>Error loading the recent module</p>
|
||||
`
|
||||
);
|
||||
`);
|
||||
}
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
}
|
||||
@@ -287,7 +280,7 @@ function switchToPhotosSection() {
|
||||
|
||||
// Hide breadcrumb
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
breadcrumb?.classList.add('hidden');
|
||||
|
||||
// Hide file containers
|
||||
toggleFileContainer(false);
|
||||
@@ -303,7 +296,6 @@ function switchToPhotosSection() {
|
||||
window.photosView.show();
|
||||
}
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
|
||||
}
|
||||
|
||||
function switchToTrashSection() {
|
||||
@@ -311,7 +303,7 @@ function switchToTrashSection() {
|
||||
|
||||
// Hide breadcrumb (only shown in Files view)
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb?.classList.add("hidden");
|
||||
breadcrumb?.classList.add('hidden');
|
||||
|
||||
// Show files containers (to be filled with trash)
|
||||
const filesList = document.getElementById('files-list');
|
||||
|
||||
@@ -11,10 +11,7 @@ async function performSearch(query, sortBy) {
|
||||
app.isSearchMode = true;
|
||||
window.ui.updateBreadcrumb(`Search: "${query}"`);
|
||||
|
||||
window.ui.showError(
|
||||
`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`
|
||||
);
|
||||
|
||||
window.ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
||||
|
||||
const options = {
|
||||
recursive: true,
|
||||
@@ -37,7 +34,6 @@ async function performSearch(query, sortBy) {
|
||||
|
||||
const searchResults = await window.search.searchFiles(query, options);
|
||||
window.search.displaySearchResults(searchResults);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
window.ui.showNotification('Error', 'Error performing search');
|
||||
|
||||
@@ -29,5 +29,4 @@ window.app = {
|
||||
viewFile: null // current file in inline view
|
||||
};
|
||||
|
||||
window.appElements = {
|
||||
};
|
||||
window.appElements = {};
|
||||
|
||||
+11
-14
@@ -8,7 +8,7 @@ async function loadTrashItems() {
|
||||
try {
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
const _tt = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
|
||||
const _tt = window.i18n && window.i18n.t ? window.i18n.t : (k) => k.split('.').pop();
|
||||
elements.filesList.innerHTML = `
|
||||
<div class="list-header trash-header">
|
||||
<div data-i18n="files.name">${_tt('files.name')}</div>
|
||||
@@ -31,10 +31,9 @@ async function loadTrashItems() {
|
||||
return;
|
||||
}
|
||||
|
||||
trashItems.forEach(item => {
|
||||
trashItems.forEach((item) => {
|
||||
addTrashItemToView(item);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading trash items:', error);
|
||||
window.ui.showNotification('Error', 'Error loading trash items');
|
||||
@@ -54,22 +53,20 @@ function addTrashItemToView(item) {
|
||||
iconClass = item.icon_class || 'fas fa-folder';
|
||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
||||
} else {
|
||||
iconClass = item.icon_class || (window.ui && window.ui.getIconClass
|
||||
? window.ui.getIconClass(item.name)
|
||||
: 'fas fa-file');
|
||||
iconSpecialClass = (window.ui && window.ui.getIconSpecialClass)
|
||||
? window.ui.getIconSpecialClass(item.name)
|
||||
: '';
|
||||
iconClass = item.icon_class || (window.ui && window.ui.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
|
||||
iconSpecialClass = window.ui && window.ui.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : '';
|
||||
const cat = item.category || '';
|
||||
typeLabel = cat
|
||||
? (window.i18n ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat)
|
||||
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
|
||||
? window.i18n
|
||||
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
||||
: cat
|
||||
: window.i18n
|
||||
? window.i18n.t('files.file_types.document')
|
||||
: 'Document';
|
||||
}
|
||||
|
||||
const isFolder = !isFile;
|
||||
const iconWrapClass = isFolder
|
||||
? 'file-icon folder-icon'
|
||||
: `file-icon ${iconSpecialClass}`.trim();
|
||||
const iconWrapClass = isFolder ? 'file-icon folder-icon' : `file-icon ${iconSpecialClass}`.trim();
|
||||
|
||||
const listElement = document.createElement('div');
|
||||
listElement.className = 'file-item trash-item';
|
||||
|
||||
+155
-144
@@ -7,7 +7,6 @@
|
||||
|
||||
// UI Module
|
||||
const ui = {
|
||||
|
||||
/** @type {HTMLDListElement | null} */
|
||||
//dragPreview,
|
||||
|
||||
@@ -285,8 +284,8 @@ const ui = {
|
||||
*/
|
||||
setupDragAndDrop() {
|
||||
// prepare area to build dragged elements
|
||||
this.dragPreview = document.createElement("div");
|
||||
this.dragPreview.className="drag-preview";
|
||||
this.dragPreview = document.createElement('div');
|
||||
this.dragPreview.className = 'drag-preview';
|
||||
document.body.appendChild(this.dragPreview);
|
||||
this.draggedItems = null;
|
||||
|
||||
@@ -294,9 +293,7 @@ const ui = {
|
||||
|
||||
const collectDroppedEntries = async (dataTransfer) => {
|
||||
const items = Array.from(dataTransfer?.items || []);
|
||||
const rootEntries = items
|
||||
.map(it => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null))
|
||||
.filter(Boolean);
|
||||
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
|
||||
|
||||
if (rootEntries.length === 0) return null;
|
||||
|
||||
@@ -356,17 +353,17 @@ const ui = {
|
||||
dropzone.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
|
||||
e._oxiHandled = true; // Mark as handled for document-level fallback
|
||||
e._oxiHandled = true; // Mark as handled for document-level fallback
|
||||
dropzone.classList.remove('active');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
// First try directory-aware extraction (Finder folder drag & drop)
|
||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||
if (droppedEntries && droppedEntries.length > 0) {
|
||||
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
|
||||
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
|
||||
if (hasFolderStructure) {
|
||||
fileOps.uploadFolderEntries(droppedEntries);
|
||||
} else {
|
||||
fileOps.uploadFiles(droppedEntries.map(x => x.file));
|
||||
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
||||
}
|
||||
setTimeout(() => {
|
||||
dropzone.style.display = 'none';
|
||||
@@ -375,9 +372,7 @@ const ui = {
|
||||
}
|
||||
|
||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
|
||||
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
|
||||
);
|
||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
|
||||
if (hasRelativePaths) {
|
||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||
} else {
|
||||
@@ -399,8 +394,7 @@ const ui = {
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', (e) => {
|
||||
if (e.clientX <= 0 || e.clientY <= 0 ||
|
||||
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
dropzone.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
if (!dropzone.classList.contains('active')) {
|
||||
@@ -421,11 +415,11 @@ const ui = {
|
||||
// First try directory-aware extraction (Finder folder drag & drop)
|
||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||
if (droppedEntries && droppedEntries.length > 0) {
|
||||
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
|
||||
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
|
||||
if (hasFolderStructure) {
|
||||
fileOps.uploadFolderEntries(droppedEntries);
|
||||
} else {
|
||||
fileOps.uploadFiles(droppedEntries.map(x => x.file));
|
||||
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
||||
}
|
||||
setTimeout(() => {
|
||||
dropzone.style.display = 'none';
|
||||
@@ -434,9 +428,7 @@ const ui = {
|
||||
}
|
||||
|
||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
|
||||
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
|
||||
);
|
||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
|
||||
if (hasRelativePaths) {
|
||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||
} else {
|
||||
@@ -514,7 +506,10 @@ const ui = {
|
||||
// TODO clarify the difference between homeIcon & this first element
|
||||
if (window.app.userHomeFolderName) {
|
||||
if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) {
|
||||
path.unshift({ name: window.app.userHomeFolderName, id: window.app.userHomeFolderId});
|
||||
path.unshift({
|
||||
name: window.app.userHomeFolderName,
|
||||
id: window.app.userHomeFolderId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,7 +542,7 @@ const ui = {
|
||||
// can drag files on this folder
|
||||
// dragover – only folders are valid drop targets
|
||||
item.addEventListener('dragover', (e) => {
|
||||
const card = e.target.closest("span");
|
||||
const card = e.target.closest('span');
|
||||
if (!card || !card.dataset.folderId) return;
|
||||
e.preventDefault();
|
||||
card.classList.add('drop-target');
|
||||
@@ -555,15 +550,15 @@ const ui = {
|
||||
|
||||
// dragleave
|
||||
item.addEventListener('dragleave', (e) => {
|
||||
console.log( "dragleave ",e );
|
||||
const card = e.target.closest("span");
|
||||
console.log('dragleave ', e);
|
||||
const card = e.target.closest('span');
|
||||
if (!card || !card.dataset.folderId) return;
|
||||
card.classList.remove('drop-target');
|
||||
});
|
||||
|
||||
// drop – only folders accept drops
|
||||
item.addEventListener('drop', async (e) => {
|
||||
const card = e.target.closest("span");
|
||||
const card = e.target.closest('span');
|
||||
if (!card) return;
|
||||
const targetFolderId = card.dataset.folderId;
|
||||
if (!targetFolderId) return;
|
||||
@@ -572,7 +567,7 @@ const ui = {
|
||||
card.classList.remove('drop-target');
|
||||
|
||||
const action = e.dataTransfer?.dropEffect;
|
||||
await self._dropToFolder( action, targetFolderId, e.dataTransfer );
|
||||
await self._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||
});
|
||||
} else {
|
||||
// Last segment: current location, not clickable
|
||||
@@ -639,8 +634,6 @@ const ui = {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Data store + event delegation (replaces per-item listeners)
|
||||
* ================================================================ */
|
||||
@@ -666,42 +659,42 @@ const ui = {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} folders
|
||||
* @returns
|
||||
*
|
||||
* @param {Object[]} folders
|
||||
* @returns
|
||||
*/
|
||||
_renderFoldersToView(folders) {
|
||||
if (!Array.isArray(folders) || folders.length === 0) return;
|
||||
const target = document.getElementById('files-list')
|
||||
const target = document.getElementById('files-list');
|
||||
if (!target) return;
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const folder of folders) {
|
||||
frag.appendChild( this._createFolderItem(folder));
|
||||
frag.appendChild(this._createFolderItem(folder));
|
||||
}
|
||||
target.appendChild(frag);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns
|
||||
*/
|
||||
_renderFilesToView(files) {
|
||||
if (!Array.isArray(files) || files.length === 0) return;
|
||||
const target = document.getElementById('files-list')
|
||||
const target = document.getElementById('files-list');
|
||||
if (!target) return;
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const file of files) {
|
||||
frag.appendChild( this._createFileItem(file))
|
||||
frag.appendChild(this._createFileItem(file));
|
||||
}
|
||||
target.appendChild(frag);
|
||||
},
|
||||
|
||||
_upsertById(arr, item) {
|
||||
if (!Array.isArray(arr) || !item || !item.id) return;
|
||||
const idx = arr.findIndex(x => x && x.id === item.id);
|
||||
const idx = arr.findIndex((x) => x && x.id === item.id);
|
||||
if (idx >= 0) {
|
||||
arr[idx] = item;
|
||||
} else {
|
||||
@@ -721,21 +714,19 @@ const ui = {
|
||||
window.multiSelect.clear();
|
||||
|
||||
if (selection.fileIds.length == 0 && selection.folderIds.length == 0) {
|
||||
|
||||
// try to use dataTransfer (direct move without selection)
|
||||
const id = dataTransfer.getData('text/plain');
|
||||
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
||||
|
||||
if (isFolder && id === targetFolderId) {
|
||||
console.log("nothing to do");
|
||||
console.log('nothing to do');
|
||||
return; //nothing to do
|
||||
}
|
||||
// append current item to selection
|
||||
if (isFolder) {
|
||||
console.log(`adding ${id} as folder`);
|
||||
selection.folderIds.push(id);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.log(`adding ${id} as file`);
|
||||
selection.fileIds.push(id);
|
||||
}
|
||||
@@ -750,15 +741,14 @@ const ui = {
|
||||
|
||||
let result;
|
||||
switch (action) {
|
||||
case "copy":
|
||||
case 'copy':
|
||||
result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId);
|
||||
break;
|
||||
|
||||
case "move":
|
||||
case 'move':
|
||||
result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
|
||||
// redraw directory
|
||||
if (result.success > 0)
|
||||
window.loadFiles();
|
||||
if (result.success > 0) window.loadFiles();
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -766,7 +756,7 @@ const ui = {
|
||||
return;
|
||||
}
|
||||
window.multiSelect.showBatchResult(action, result);
|
||||
console.log( result);
|
||||
console.log(result);
|
||||
},
|
||||
|
||||
_hydrateViewIfNeeded() {
|
||||
@@ -801,9 +791,21 @@ const ui = {
|
||||
const itemInfo = (card) => {
|
||||
if (!card) return null;
|
||||
const fileId = card.dataset.fileId;
|
||||
if (fileId) return { type: 'file', id: fileId, name: card.dataset.fileName, data: self._items.get(fileId) };
|
||||
if (fileId)
|
||||
return {
|
||||
type: 'file',
|
||||
id: fileId,
|
||||
name: card.dataset.fileName,
|
||||
data: self._items.get(fileId)
|
||||
};
|
||||
const folderId = card.dataset.folderId;
|
||||
if (folderId) return { type: 'folder', id: folderId, name: card.dataset.folderName, data: self._items.get(folderId) };
|
||||
if (folderId)
|
||||
return {
|
||||
type: 'folder',
|
||||
id: folderId,
|
||||
name: card.dataset.folderName,
|
||||
data: self._items.get(folderId)
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -815,7 +817,7 @@ const ui = {
|
||||
// WOPI editor intercept: open Office documents in the WOPI editor
|
||||
// But NOT image files - those should be previewed in the inline viewer
|
||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||
const imageExts = ['jpg','jpeg','png','gif','svg','webp','bmp','ico','heic','heif','avif','tiff'];
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
||||
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
|
||||
try {
|
||||
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
|
||||
@@ -856,13 +858,13 @@ const ui = {
|
||||
window.app.contextMenuTargetFolder = {
|
||||
id: info.id,
|
||||
name: card.dataset.folderName,
|
||||
parent_id: card.dataset.parentId || ""
|
||||
parent_id: card.dataset.parentId || ''
|
||||
};
|
||||
} else {
|
||||
window.app.contextMenuTargetFile = {
|
||||
id: info.id,
|
||||
name: card.dataset.fileName,
|
||||
folder_id: card.dataset.folderId || ""
|
||||
folder_id: card.dataset.folderId || ''
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -878,10 +880,8 @@ const ui = {
|
||||
const info = itemInfo(card);
|
||||
if (!info) return;
|
||||
setContextTarget(card, info);
|
||||
const menuId = info.type === 'folder'
|
||||
? 'folder-context-menu' : 'file-context-menu';
|
||||
showContextMenuAtElement(
|
||||
e.target.closest('.file-actions'), menuId);
|
||||
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||
showContextMenuAtElement(e.target.closest('.file-actions'), menuId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -926,7 +926,7 @@ const ui = {
|
||||
});
|
||||
|
||||
// ── shared events ──────────────────────
|
||||
|
||||
|
||||
filesList.addEventListener('contextmenu', (e) => {
|
||||
const card = e.target.closest('.file-item');
|
||||
if (!card) return;
|
||||
@@ -934,97 +934,100 @@ const ui = {
|
||||
const info = itemInfo(card);
|
||||
if (!info) return;
|
||||
setContextTarget(card, info);
|
||||
const menuId = info.type === 'folder'
|
||||
? 'folder-context-menu' : 'file-context-menu';
|
||||
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||
const menu = document.getElementById(menuId);
|
||||
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
||||
window.contextMenus.syncFavoriteOptionLabels();
|
||||
}
|
||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
|
||||
}
|
||||
menu.style.left = `${e.pageX}px`;
|
||||
menu.style.top = `${e.pageY}px`;
|
||||
menu.style.top = `${e.pageY}px`;
|
||||
menu.style.display = 'block';
|
||||
});
|
||||
|
||||
// dragstart
|
||||
filesList.addEventListener('dragstart', (e) => {
|
||||
let card = e.target.closest('.file-item');
|
||||
if (!card) { e.preventDefault(); return; }
|
||||
|
||||
if (!card) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const info = itemInfo(card);
|
||||
if (!info) { e.preventDefault(); return; }
|
||||
if (!info) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.setData('text/plain', info.id);
|
||||
if (info.type === 'folder') {
|
||||
e.dataTransfer.setData(
|
||||
'application/oxicloud-folder', 'true');
|
||||
if (info.type === 'folder') {
|
||||
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
||||
}
|
||||
// allow copy or move (handled by the browser)
|
||||
e.dataTransfer.effectAllowed = "copyMove";
|
||||
|
||||
self.draggedItems = document.createElement("div");
|
||||
self.draggedItems.className = "dragged-items";
|
||||
|
||||
e.dataTransfer.effectAllowed = 'copyMove';
|
||||
|
||||
self.draggedItems = document.createElement('div');
|
||||
self.draggedItems.className = 'dragged-items';
|
||||
|
||||
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
||||
if (selectedCardFromList.length == 0) {
|
||||
// fallback to current element
|
||||
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
||||
}
|
||||
|
||||
|
||||
let index = 0;
|
||||
const maxElements = 4;
|
||||
let lastItemDiv = null;
|
||||
|
||||
while (index < selectedCardFromList.length && index < maxElements) {
|
||||
let iconCell = document.createElement("div");
|
||||
let icon = selectedCardFromList[index].getElementsByClassName("file-icon").item(0)?.cloneNode(true);
|
||||
let iconCell = document.createElement('div');
|
||||
let icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
|
||||
if (icon) {
|
||||
iconCell.appendChild( icon);
|
||||
iconCell.querySelectorAll('img')?.forEach((img) => { img.loading="eager"; } );
|
||||
iconCell.appendChild(icon);
|
||||
iconCell.querySelectorAll('img')?.forEach((img) => {
|
||||
img.loading = 'eager';
|
||||
});
|
||||
}
|
||||
|
||||
let nameCell = document.createElement("div");
|
||||
let name = selectedCardFromList[index].getElementsByTagName("span").item(0)?.cloneNode(true);
|
||||
let nameCell = document.createElement('div');
|
||||
let name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
|
||||
if (name) {
|
||||
nameCell.appendChild(name);
|
||||
}
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.className="file-item";
|
||||
let div = document.createElement('div');
|
||||
div.className = 'file-item';
|
||||
div.appendChild(iconCell);
|
||||
div.appendChild(nameCell);
|
||||
|
||||
self.draggedItems.appendChild( div);
|
||||
self.draggedItems.appendChild(div);
|
||||
index += 1;
|
||||
lastItemDiv = div;
|
||||
}
|
||||
|
||||
// if more than 1 item, display the badge
|
||||
if (selectedCardFromList.length > 1) {
|
||||
let badge = document.createElement("span");
|
||||
badge.className="dragged-items-badge";
|
||||
badge.innerText=`${selectedCardFromList.length}`;
|
||||
self.draggedItems.appendChild( badge);
|
||||
let badge = document.createElement('span');
|
||||
badge.className = 'dragged-items-badge';
|
||||
badge.innerText = `${selectedCardFromList.length}`;
|
||||
self.draggedItems.appendChild(badge);
|
||||
}
|
||||
|
||||
// if more than maxElements display the fading
|
||||
if (selectedCardFromList.length > maxElements) {
|
||||
lastItemDiv.classList.add("fading");
|
||||
lastItemDiv.classList.add('fading');
|
||||
}
|
||||
|
||||
self.dragPreview.appendChild( self.draggedItems);
|
||||
|
||||
self.dragPreview.appendChild(self.draggedItems);
|
||||
e.dataTransfer.setDragImage(self.draggedItems, 0, 0);
|
||||
|
||||
});
|
||||
|
||||
// dragend
|
||||
filesList.addEventListener('dragend', (e) => {
|
||||
|
||||
self.dragPreview.removeChild( self.draggedItems);
|
||||
document.querySelectorAll('.drop-target')
|
||||
.forEach(el => el.classList.remove('drop-target'));
|
||||
self.dragPreview.removeChild(self.draggedItems);
|
||||
document.querySelectorAll('.drop-target').forEach((el) => el.classList.remove('drop-target'));
|
||||
});
|
||||
|
||||
// dragover – only folders are valid drop targets
|
||||
@@ -1054,9 +1057,8 @@ const ui = {
|
||||
card.classList.remove('drop-target');
|
||||
|
||||
const action = e.dataTransfer.dropEffect;
|
||||
await self._dropToFolder( action, targetFolderId, e.dataTransfer);
|
||||
await self._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/* ================================================================
|
||||
@@ -1074,7 +1076,7 @@ const ui = {
|
||||
|
||||
if (!window.favorites) return;
|
||||
|
||||
const itemId = star.dataset.itemId;
|
||||
const itemId = star.dataset.itemId;
|
||||
const itemType = star.dataset.itemType;
|
||||
const itemName = star.dataset.itemName;
|
||||
|
||||
@@ -1099,9 +1101,7 @@ const ui = {
|
||||
* Sync favorite visuals for a file/folder across grid and list views.
|
||||
*/
|
||||
setFavoriteVisualState(itemId, itemType, isFavorite) {
|
||||
const selector = itemType === 'folder'
|
||||
? `#files-list .file-item[data-folder-id="${itemId}"]`
|
||||
: `#files-list .file-item[data-file-id="${itemId}"]`;
|
||||
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
|
||||
|
||||
const card = document.querySelector(selector);
|
||||
const starBtn = card ? card.querySelector('.favorite-star') : null;
|
||||
@@ -1127,7 +1127,7 @@ const ui = {
|
||||
i.classList.add(isFavorite ? 'fas' : 'far');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const listItem = document.querySelector(selector);
|
||||
if (listItem) {
|
||||
const nameCell = listItem.querySelector('.name-cell');
|
||||
@@ -1155,9 +1155,9 @@ const ui = {
|
||||
_createFolderItem(folder) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'file-item';
|
||||
el.dataset.folderId = folder.id;
|
||||
el.dataset.folderId = folder.id;
|
||||
el.dataset.folderName = folder.name;
|
||||
el.dataset.parentId = folder.parent_id || "";
|
||||
el.dataset.parentId = folder.parent_id || '';
|
||||
|
||||
const isFav = window.favorites && window.favorites.isFavorite(folder.id, 'folder');
|
||||
const formattedDate = window.formatDateTime(folder.modified_at);
|
||||
@@ -1182,7 +1182,7 @@ const ui = {
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.app.currentPath !== "") {
|
||||
if (window.app.currentPath !== '') {
|
||||
el.setAttribute('draggable', 'true');
|
||||
}
|
||||
this._bindStarClick(el);
|
||||
@@ -1195,17 +1195,21 @@ const ui = {
|
||||
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
||||
const cat = file.category || '';
|
||||
const typeLabel = cat
|
||||
? (window.i18n ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat)
|
||||
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
|
||||
? window.i18n
|
||||
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
||||
: cat
|
||||
: window.i18n
|
||||
? window.i18n.t('files.file_types.document')
|
||||
: 'Document';
|
||||
const fileSize = file.size_formatted || window.formatFileSize(file.size);
|
||||
const formattedDate = window.formatDateTime(file.modified_at);
|
||||
const isFav = window.favorites && window.favorites.isFavorite(file.id, 'file');
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'file-item';
|
||||
el.dataset.fileId = file.id;
|
||||
el.dataset.fileId = file.id;
|
||||
el.dataset.fileName = file.name;
|
||||
el.dataset.folderId = file.folder_id || "";
|
||||
el.dataset.folderId = file.folder_id || '';
|
||||
el.setAttribute('draggable', 'true');
|
||||
|
||||
el.innerHTML = `
|
||||
@@ -1230,7 +1234,10 @@ const ui = {
|
||||
</div>
|
||||
`;
|
||||
var thumb = el.querySelector('.file-thumb');
|
||||
if (thumb) thumb.addEventListener('error', function() { this.style.display = 'none'; });
|
||||
if (thumb)
|
||||
thumb.addEventListener('error', function () {
|
||||
this.style.display = 'none';
|
||||
});
|
||||
this._bindStarClick(el);
|
||||
return el;
|
||||
},
|
||||
@@ -1241,11 +1248,11 @@ const ui = {
|
||||
|
||||
resetFilesList() {
|
||||
const filesList = document.getElementById('files-list');
|
||||
const filesContainerError=document.getElementById("files-container-error");
|
||||
const filesContainerError = document.getElementById('files-container-error');
|
||||
|
||||
if (!filesList) return;
|
||||
|
||||
filesList.innerHTML=`
|
||||
filesList.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>
|
||||
@@ -1254,12 +1261,11 @@ const ui = {
|
||||
<div data-i18n="files.modified">Modified</div>
|
||||
<div></div><!-- actions -->
|
||||
</div>`;
|
||||
|
||||
if (window.i18n && window.i18n.translateElement)
|
||||
window.i18n.translateElement( filesList);
|
||||
|
||||
filesList.classList.remove("hidden");
|
||||
filesContainerError?.classList.add("hidden");
|
||||
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesList);
|
||||
|
||||
filesList.classList.remove('hidden');
|
||||
filesContainerError?.classList.add('hidden');
|
||||
},
|
||||
|
||||
showEmptyList() {
|
||||
@@ -1267,8 +1273,7 @@ const ui = {
|
||||
<i class="fas fa-folder-open empty-state-icon"></i>
|
||||
<p data-i18n="files.no_files"></p>
|
||||
<p data-i18n="files.empty_hint"></p>
|
||||
`
|
||||
);
|
||||
`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1277,16 +1282,14 @@ const ui = {
|
||||
*
|
||||
*/
|
||||
showError(content) {
|
||||
const filesContainerError=document.getElementById("files-container-error");
|
||||
const filesList=document.getElementById("files-list");
|
||||
if (filesContainerError)
|
||||
filesContainerError.innerHTML=content;
|
||||
const filesContainerError = document.getElementById('files-container-error');
|
||||
const filesList = document.getElementById('files-list');
|
||||
if (filesContainerError) filesContainerError.innerHTML = content;
|
||||
|
||||
if (window.i18n && window.i18n.translateElement)
|
||||
window.i18n.translateElement( filesContainerError);
|
||||
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesContainerError);
|
||||
|
||||
filesContainerError?.classList.remove("hidden");
|
||||
filesList?.classList.add("hidden");
|
||||
filesContainerError?.classList.remove('hidden');
|
||||
filesList?.classList.add('hidden');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1381,7 +1384,7 @@ function toggleCardSelection(card, event) {
|
||||
*/
|
||||
function showContextMenuAtElement(triggerElement, menuId) {
|
||||
// Hide any open menus first
|
||||
document.querySelectorAll('.context-menu').forEach(m => m.style.display = 'none');
|
||||
document.querySelectorAll('.context-menu').forEach((m) => (m.style.display = 'none'));
|
||||
|
||||
const menu = document.getElementById(menuId);
|
||||
if (!menu) return;
|
||||
@@ -1403,7 +1406,7 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
||||
window.contextMenus.syncFavoriteOptionLabels();
|
||||
}
|
||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
|
||||
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
|
||||
}
|
||||
|
||||
menu.style.left = `${left}px`;
|
||||
@@ -1426,7 +1429,8 @@ function initRubberBandSelection() {
|
||||
}
|
||||
|
||||
let active = false;
|
||||
let startX = 0, startY = 0;
|
||||
let startX = 0,
|
||||
startY = 0;
|
||||
|
||||
// We listen on the whole files-container (covers grid + empty space)
|
||||
const container = document.querySelector('.files-container') || document.getElementById('files-list');
|
||||
@@ -1435,11 +1439,16 @@ function initRubberBandSelection() {
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
// Only start if clicking empty area (not on a card, button, menu, input…)
|
||||
if (e.button !== 0) return; // left click only
|
||||
if (e.target.closest('.file-item') ||
|
||||
if (
|
||||
e.target.closest('.file-item') ||
|
||||
e.target.closest('.context-menu') ||
|
||||
e.target.closest('.upload-dropdown') || e.target.closest('button') ||
|
||||
e.target.closest('input') || e.target.closest('.breadcrumb') ||
|
||||
e.target.closest('.list-header')) return;
|
||||
e.target.closest('.upload-dropdown') ||
|
||||
e.target.closest('button') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('.breadcrumb') ||
|
||||
e.target.closest('.list-header')
|
||||
)
|
||||
return;
|
||||
|
||||
active = true;
|
||||
startX = e.clientX;
|
||||
@@ -1478,13 +1487,10 @@ function initRubberBandSelection() {
|
||||
// Highlight cards that intersect with the rectangle
|
||||
const rectBounds = { left, top, right: left + width, bottom: top + height };
|
||||
|
||||
document.querySelectorAll('#files-list .file-item').forEach(card => {
|
||||
document.querySelectorAll('#files-list .file-item').forEach((card) => {
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const intersects =
|
||||
cardRect.left < rectBounds.right &&
|
||||
cardRect.right > rectBounds.left &&
|
||||
cardRect.top < rectBounds.bottom &&
|
||||
cardRect.bottom > rectBounds.top;
|
||||
cardRect.left < rectBounds.right && cardRect.right > rectBounds.left && cardRect.top < rectBounds.bottom && cardRect.bottom > rectBounds.top;
|
||||
|
||||
if (intersects) {
|
||||
card.classList.add('selected');
|
||||
@@ -1498,7 +1504,7 @@ function initRubberBandSelection() {
|
||||
card.classList.remove('selected');
|
||||
// Deselect from multiSelect module
|
||||
if (window.multiSelect) {
|
||||
const info = window.multiSelect._extractInfo(card)
|
||||
const info = window.multiSelect._extractInfo(card);
|
||||
if (info) window.multiSelect.deselect(info.id);
|
||||
}
|
||||
}
|
||||
@@ -1516,7 +1522,9 @@ function initRubberBandSelection() {
|
||||
// deselect handler doesn't immediately clear the selection.
|
||||
if (hadSelection) {
|
||||
window.__rubberBandJustFinished = true;
|
||||
requestAnimationFrame(() => { window.__rubberBandJustFinished = false; });
|
||||
requestAnimationFrame(() => {
|
||||
window.__rubberBandJustFinished = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1533,7 +1541,6 @@ window.toggleCardSelection = toggleCardSelection;
|
||||
window.showContextMenuAtElement = showContextMenuAtElement;
|
||||
window.initRubberBandSelection = initRubberBandSelection;
|
||||
|
||||
|
||||
/**
|
||||
* Show a modern confirm dialog (replaces native confirm())
|
||||
* @param {Object} options
|
||||
@@ -1573,7 +1580,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Force layout then show
|
||||
requestAnimationFrame(() => { overlay.classList.add('active'); });
|
||||
requestAnimationFrame(() => {
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
|
||||
const cleanup = (result) => {
|
||||
overlay.classList.remove('active');
|
||||
@@ -1583,7 +1592,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
||||
|
||||
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
|
||||
overlay.querySelector('.confirm-dialog-ok').addEventListener('click', () => cleanup(true));
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(false); });
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) cleanup(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
window.showConfirmDialog = showConfirmDialog;
|
||||
|
||||
+139
-47
@@ -18,24 +18,57 @@ const uiFileTypes = {
|
||||
if (!fileName) return 'fas fa-file';
|
||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||
const map = {
|
||||
pdf:'fas fa-file-pdf', doc:'fas fa-file-word', docx:'fas fa-file-word',
|
||||
txt:'fas fa-file-alt', rtf:'fas fa-file-alt', odt:'fas fa-file-alt',
|
||||
xls:'fas fa-file-excel', xlsx:'fas fa-file-excel', csv:'fas fa-file-excel', ods:'fas fa-file-excel',
|
||||
ppt:'fas fa-file-powerpoint', pptx:'fas fa-file-powerpoint', odp:'fas fa-file-powerpoint',
|
||||
jpg:'fas fa-file-image', jpeg:'fas fa-file-image', png:'fas fa-file-image',
|
||||
gif:'fas fa-file-image', svg:'fas fa-file-image', webp:'fas fa-file-image',
|
||||
bmp:'fas fa-file-image', ico:'fas fa-file-image',
|
||||
mp4:'fas fa-file-video', avi:'fas fa-file-video', mov:'fas fa-file-video',
|
||||
mkv:'fas fa-file-video', webm:'fas fa-file-video', flv:'fas fa-file-video',
|
||||
mp3:'fas fa-file-audio', wav:'fas fa-file-audio', ogg:'fas fa-file-audio',
|
||||
flac:'fas fa-file-audio', aac:'fas fa-file-audio', m4a:'fas fa-file-audio',
|
||||
zip:'fas fa-file-archive', rar:'fas fa-file-archive', '7z':'fas fa-file-archive',
|
||||
tar:'fas fa-file-archive', gz:'fas fa-file-archive',
|
||||
js:'fas fa-file-code', ts:'fas fa-file-code', py:'fas fa-file-code',
|
||||
rs:'fas fa-file-code', java:'fas fa-file-code', html:'fas fa-file-code',
|
||||
css:'fas fa-file-code', json:'fas fa-file-code', xml:'fas fa-file-code',
|
||||
sh:'fas fa-terminal', bash:'fas fa-terminal', bat:'fas fa-terminal',
|
||||
md:'fas fa-file-alt',
|
||||
pdf: 'fas fa-file-pdf',
|
||||
doc: 'fas fa-file-word',
|
||||
docx: 'fas fa-file-word',
|
||||
txt: 'fas fa-file-alt',
|
||||
rtf: 'fas fa-file-alt',
|
||||
odt: 'fas fa-file-alt',
|
||||
xls: 'fas fa-file-excel',
|
||||
xlsx: 'fas fa-file-excel',
|
||||
csv: 'fas fa-file-excel',
|
||||
ods: 'fas fa-file-excel',
|
||||
ppt: 'fas fa-file-powerpoint',
|
||||
pptx: 'fas fa-file-powerpoint',
|
||||
odp: 'fas fa-file-powerpoint',
|
||||
jpg: 'fas fa-file-image',
|
||||
jpeg: 'fas fa-file-image',
|
||||
png: 'fas fa-file-image',
|
||||
gif: 'fas fa-file-image',
|
||||
svg: 'fas fa-file-image',
|
||||
webp: 'fas fa-file-image',
|
||||
bmp: 'fas fa-file-image',
|
||||
ico: 'fas fa-file-image',
|
||||
mp4: 'fas fa-file-video',
|
||||
avi: 'fas fa-file-video',
|
||||
mov: 'fas fa-file-video',
|
||||
mkv: 'fas fa-file-video',
|
||||
webm: 'fas fa-file-video',
|
||||
flv: 'fas fa-file-video',
|
||||
mp3: 'fas fa-file-audio',
|
||||
wav: 'fas fa-file-audio',
|
||||
ogg: 'fas fa-file-audio',
|
||||
flac: 'fas fa-file-audio',
|
||||
aac: 'fas fa-file-audio',
|
||||
m4a: 'fas fa-file-audio',
|
||||
zip: 'fas fa-file-archive',
|
||||
rar: 'fas fa-file-archive',
|
||||
'7z': 'fas fa-file-archive',
|
||||
tar: 'fas fa-file-archive',
|
||||
gz: 'fas fa-file-archive',
|
||||
js: 'fas fa-file-code',
|
||||
ts: 'fas fa-file-code',
|
||||
py: 'fas fa-file-code',
|
||||
rs: 'fas fa-file-code',
|
||||
java: 'fas fa-file-code',
|
||||
html: 'fas fa-file-code',
|
||||
css: 'fas fa-file-code',
|
||||
json: 'fas fa-file-code',
|
||||
xml: 'fas fa-file-code',
|
||||
sh: 'fas fa-terminal',
|
||||
bash: 'fas fa-terminal',
|
||||
bat: 'fas fa-terminal',
|
||||
md: 'fas fa-file-alt'
|
||||
};
|
||||
return map[ext] || 'fas fa-file';
|
||||
},
|
||||
@@ -44,35 +77,94 @@ const uiFileTypes = {
|
||||
if (!fileName) return '';
|
||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||
const map = {
|
||||
pdf:'pdf-icon',
|
||||
doc:'doc-icon', docx:'doc-icon', odt:'doc-icon', rtf:'doc-icon',
|
||||
xls:'spreadsheet-icon', xlsx:'spreadsheet-icon', ods:'spreadsheet-icon', csv:'spreadsheet-icon',
|
||||
ppt:'presentation-icon', pptx:'presentation-icon', odp:'presentation-icon', key:'presentation-icon',
|
||||
jpg:'image-icon', jpeg:'image-icon', png:'image-icon', gif:'image-icon',
|
||||
svg:'image-icon', webp:'image-icon', bmp:'image-icon', ico:'image-icon',
|
||||
heic:'image-icon', heif:'image-icon', avif:'image-icon', tiff:'image-icon',
|
||||
mp4:'video-icon', avi:'video-icon', mkv:'video-icon', mov:'video-icon',
|
||||
wmv:'video-icon', flv:'video-icon', webm:'video-icon', m4v:'video-icon',
|
||||
mp3:'audio-icon', wav:'audio-icon', ogg:'audio-icon', flac:'audio-icon',
|
||||
aac:'audio-icon', wma:'audio-icon', m4a:'audio-icon', opus:'audio-icon',
|
||||
zip:'archive-icon', rar:'archive-icon', '7z':'archive-icon',
|
||||
tar:'archive-icon', gz:'archive-icon', bz2:'archive-icon', xz:'archive-icon',
|
||||
exe:'installer-icon', msi:'installer-icon', dmg:'installer-icon',
|
||||
deb:'installer-icon', rpm:'installer-icon', appimage:'installer-icon',
|
||||
py:'code-icon py-icon', rs:'code-icon rust-icon', go:'code-icon go-icon',
|
||||
js:'code-icon js-icon', jsx:'code-icon js-icon', mjs:'code-icon js-icon',
|
||||
ts:'code-icon ts-icon', tsx:'code-icon ts-icon',
|
||||
java:'code-icon java-icon', c:'code-icon c-icon', cpp:'code-icon c-icon',
|
||||
cs:'code-icon cs-icon', rb:'code-icon ruby-icon', php:'code-icon php-icon',
|
||||
swift:'code-icon swift-icon',
|
||||
html:'code-icon html-icon', htm:'code-icon html-icon',
|
||||
css:'code-icon css-icon', scss:'code-icon css-icon',
|
||||
json:'code-icon json-icon', xml:'code-icon html-icon',
|
||||
yaml:'code-icon config-icon', yml:'code-icon config-icon',
|
||||
toml:'code-icon config-icon', ini:'code-icon config-icon',
|
||||
sql:'code-icon sql-icon', vue:'code-icon js-icon', svelte:'code-icon js-icon',
|
||||
sh:'script-icon', bash:'script-icon', zsh:'script-icon', bat:'script-icon',
|
||||
md:'code-icon md-icon', txt:'doc-icon',
|
||||
pdf: 'pdf-icon',
|
||||
doc: 'doc-icon',
|
||||
docx: 'doc-icon',
|
||||
odt: 'doc-icon',
|
||||
rtf: 'doc-icon',
|
||||
xls: 'spreadsheet-icon',
|
||||
xlsx: 'spreadsheet-icon',
|
||||
ods: 'spreadsheet-icon',
|
||||
csv: 'spreadsheet-icon',
|
||||
ppt: 'presentation-icon',
|
||||
pptx: 'presentation-icon',
|
||||
odp: 'presentation-icon',
|
||||
key: 'presentation-icon',
|
||||
jpg: 'image-icon',
|
||||
jpeg: 'image-icon',
|
||||
png: 'image-icon',
|
||||
gif: 'image-icon',
|
||||
svg: 'image-icon',
|
||||
webp: 'image-icon',
|
||||
bmp: 'image-icon',
|
||||
ico: 'image-icon',
|
||||
heic: 'image-icon',
|
||||
heif: 'image-icon',
|
||||
avif: 'image-icon',
|
||||
tiff: 'image-icon',
|
||||
mp4: 'video-icon',
|
||||
avi: 'video-icon',
|
||||
mkv: 'video-icon',
|
||||
mov: 'video-icon',
|
||||
wmv: 'video-icon',
|
||||
flv: 'video-icon',
|
||||
webm: 'video-icon',
|
||||
m4v: 'video-icon',
|
||||
mp3: 'audio-icon',
|
||||
wav: 'audio-icon',
|
||||
ogg: 'audio-icon',
|
||||
flac: 'audio-icon',
|
||||
aac: 'audio-icon',
|
||||
wma: 'audio-icon',
|
||||
m4a: 'audio-icon',
|
||||
opus: 'audio-icon',
|
||||
zip: 'archive-icon',
|
||||
rar: 'archive-icon',
|
||||
'7z': 'archive-icon',
|
||||
tar: 'archive-icon',
|
||||
gz: 'archive-icon',
|
||||
bz2: 'archive-icon',
|
||||
xz: 'archive-icon',
|
||||
exe: 'installer-icon',
|
||||
msi: 'installer-icon',
|
||||
dmg: 'installer-icon',
|
||||
deb: 'installer-icon',
|
||||
rpm: 'installer-icon',
|
||||
appimage: 'installer-icon',
|
||||
py: 'code-icon py-icon',
|
||||
rs: 'code-icon rust-icon',
|
||||
go: 'code-icon go-icon',
|
||||
js: 'code-icon js-icon',
|
||||
jsx: 'code-icon js-icon',
|
||||
mjs: 'code-icon js-icon',
|
||||
ts: 'code-icon ts-icon',
|
||||
tsx: 'code-icon ts-icon',
|
||||
java: 'code-icon java-icon',
|
||||
c: 'code-icon c-icon',
|
||||
cpp: 'code-icon c-icon',
|
||||
cs: 'code-icon cs-icon',
|
||||
rb: 'code-icon ruby-icon',
|
||||
php: 'code-icon php-icon',
|
||||
swift: 'code-icon swift-icon',
|
||||
html: 'code-icon html-icon',
|
||||
htm: 'code-icon html-icon',
|
||||
css: 'code-icon css-icon',
|
||||
scss: 'code-icon css-icon',
|
||||
json: 'code-icon json-icon',
|
||||
xml: 'code-icon html-icon',
|
||||
yaml: 'code-icon config-icon',
|
||||
yml: 'code-icon config-icon',
|
||||
toml: 'code-icon config-icon',
|
||||
ini: 'code-icon config-icon',
|
||||
sql: 'code-icon sql-icon',
|
||||
vue: 'code-icon js-icon',
|
||||
svelte: 'code-icon js-icon',
|
||||
sh: 'script-icon',
|
||||
bash: 'script-icon',
|
||||
zsh: 'script-icon',
|
||||
bat: 'script-icon',
|
||||
md: 'code-icon md-icon',
|
||||
txt: 'doc-icon'
|
||||
};
|
||||
return map[ext] || '';
|
||||
}
|
||||
|
||||
@@ -16,7 +16,13 @@ const uiNotifications = {
|
||||
} else if (normalizedTitle.includes('favorite') || normalizedTitle.includes('favorit') || normalizedTitle.includes('fav')) {
|
||||
icon = 'fa-star';
|
||||
iconClass = 'success';
|
||||
} else if (normalizedTitle.includes('delete') || normalizedTitle.includes('removed') || normalizedTitle.includes('trash') || normalizedTitle.includes('rename') || normalizedTitle.includes('complete')) {
|
||||
} else if (
|
||||
normalizedTitle.includes('delete') ||
|
||||
normalizedTitle.includes('removed') ||
|
||||
normalizedTitle.includes('trash') ||
|
||||
normalizedTitle.includes('rename') ||
|
||||
normalizedTitle.includes('complete')
|
||||
) {
|
||||
icon = 'fa-check-circle';
|
||||
iconClass = 'success';
|
||||
}
|
||||
|
||||
+18
-11
@@ -31,9 +31,15 @@ function setupUserMenu() {
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
const isAdmin = userData.role === 'admin';
|
||||
if (adminBtn) { isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden'); }
|
||||
if (adminDivider) { isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden'); }
|
||||
if (roleBadge) { isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.classList.add('hidden'); }
|
||||
if (adminBtn) {
|
||||
isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden');
|
||||
}
|
||||
if (adminDivider) {
|
||||
isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden');
|
||||
}
|
||||
if (roleBadge) {
|
||||
isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,10 +102,7 @@ function setupUserMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
window.ui.showNotification(
|
||||
newIsDark ? '🌙' : '☀️',
|
||||
newIsDark ? 'Dark mode enabled' : 'Light mode enabled'
|
||||
);
|
||||
window.ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,7 +168,7 @@ function updateUserMenuData() {
|
||||
}
|
||||
|
||||
const usedBytes = userData.storage_used_bytes || 0;
|
||||
const quotaBytes = userData.storage_quota_bytes == null ? (10 * 1024 * 1024 * 1024) : userData.storage_quota_bytes;
|
||||
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||
|
||||
if (storageFill) storageFill.style.width = percentage + '%';
|
||||
@@ -199,11 +202,11 @@ function showUserProfileModal() {
|
||||
const role = userData.role || 'user';
|
||||
const initials = username.substring(0, 2).toUpperCase();
|
||||
const usedBytes = userData.storage_used_bytes || 0;
|
||||
const quotaBytes = userData.storage_quota_bytes == null ? (10 * 1024 * 1024 * 1024) : userData.storage_quota_bytes;
|
||||
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
||||
|
||||
const t = (key, fallback) => (window.i18n && window.i18n.t) ? window.i18n.t(key) || fallback : fallback;
|
||||
const t = (key, fallback) => (window.i18n && window.i18n.t ? window.i18n.t(key) || fallback : fallback);
|
||||
|
||||
const existing = document.getElementById('profile-modal-overlay');
|
||||
if (existing) existing.remove();
|
||||
@@ -268,7 +271,11 @@ async function logout() {
|
||||
// cleared before redirecting, otherwise the login page's session probe
|
||||
// will refresh the token and redirect back to the app).
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin', headers: getCsrfHeaders() });
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: getCsrfHeaders()
|
||||
});
|
||||
} catch (_) {
|
||||
// Best-effort
|
||||
}
|
||||
|
||||
+26
-28
@@ -1,28 +1,26 @@
|
||||
/**
|
||||
* CSRF double-submit cookie utility.
|
||||
*
|
||||
* Reads the `oxicloud_csrf` cookie (which is NOT HttpOnly) and provides
|
||||
* its value as the `X-CSRF-Token` header on mutating requests.
|
||||
*
|
||||
* Usage:
|
||||
* // In any fetch call that changes state:
|
||||
* fetch(url, { method: 'POST', headers: { ...getCsrfHeaders(), 'Content-Type': 'application/json' } })
|
||||
*
|
||||
* The server-side `csrf_middleware` validates that the header value matches
|
||||
* the cookie for every POST/PUT/DELETE/PATCH request authenticated via
|
||||
* HttpOnly cookies.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('oxicloud_csrf='));
|
||||
return match ? match.split('=')[1] : '';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getCsrfHeaders() {
|
||||
const token = getCsrfToken();
|
||||
return token ? { 'X-CSRF-Token': token } : {};
|
||||
}
|
||||
/**
|
||||
* CSRF double-submit cookie utility.
|
||||
*
|
||||
* Reads the `oxicloud_csrf` cookie (which is NOT HttpOnly) and provides
|
||||
* its value as the `X-CSRF-Token` header on mutating requests.
|
||||
*
|
||||
* Usage:
|
||||
* // In any fetch call that changes state:
|
||||
* fetch(url, { method: 'POST', headers: { ...getCsrfHeaders(), 'Content-Type': 'application/json' } })
|
||||
*
|
||||
* The server-side `csrf_middleware` validates that the header value matches
|
||||
* the cookie for every POST/PUT/DELETE/PATCH request authenticated via
|
||||
* HttpOnly cookies.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie.split('; ').find((row) => row.startsWith('oxicloud_csrf='));
|
||||
return match ? match.split('=')[1] : '';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getCsrfHeaders() {
|
||||
const token = getCsrfToken();
|
||||
return token ? { 'X-CSRF-Token': token } : {};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
@@ -40,24 +35,32 @@ function formatDateTime(value) {
|
||||
dateValue = new Date(value);
|
||||
}
|
||||
if (isNaN(dateValue.getTime())) return String(value);
|
||||
return dateValue.toLocaleDateString() + ' ' +
|
||||
dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return dateValue.toLocaleDateString() + ' ' + dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function formatDateShort(value) {
|
||||
if (!value) return 'N/A';
|
||||
const dateValue = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
|
||||
if (isNaN(dateValue.getTime())) return String(value);
|
||||
return dateValue.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
return dateValue.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function isTextViewable(mimeType) {
|
||||
if (!mimeType) return false;
|
||||
if (mimeType.startsWith('text/')) return true;
|
||||
const textTypes = [
|
||||
'application/json', 'application/xml', 'application/javascript',
|
||||
'application/x-sh', 'application/x-yaml', 'application/toml',
|
||||
'application/x-toml', 'application/sql',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/javascript',
|
||||
'application/x-sh',
|
||||
'application/x-yaml',
|
||||
'application/toml',
|
||||
'application/x-toml',
|
||||
'application/sql'
|
||||
];
|
||||
return textTypes.includes(mimeType);
|
||||
}
|
||||
|
||||
+31
-34
@@ -1,15 +1,12 @@
|
||||
/**
|
||||
* OxiCloud Internationalization (i18n) Module
|
||||
*
|
||||
*
|
||||
* This module provides functionality for internationalization of the OxiCloud web interface.
|
||||
* It loads translations from the server and provides functions to translate keys.
|
||||
*/
|
||||
|
||||
// Current locale code (default to browser locale if available, fallback to English)
|
||||
let currentLocale =
|
||||
(navigator.language && navigator.language.substring(0, 2)) ||
|
||||
(navigator.userLanguage && navigator.userLanguage.substring(0, 2)) ||
|
||||
'en';
|
||||
let currentLocale = (navigator.language && navigator.language.substring(0, 2)) || (navigator.userLanguage && navigator.userLanguage.substring(0, 2)) || 'en';
|
||||
|
||||
// Supported locales (languages that have locale files on the server)
|
||||
// When a locale file is not found, the system gracefully falls back to English
|
||||
@@ -33,19 +30,19 @@ async function loadTranslations(locale) {
|
||||
if (translations[locale]) {
|
||||
return translations[locale];
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Load directly from local JSON file
|
||||
const localeData = await fetch(`/locales/${locale}.json`);
|
||||
if (!localeData.ok) {
|
||||
throw new Error(`Failed to load locale file for ${locale}`);
|
||||
}
|
||||
|
||||
|
||||
translations[locale] = await localeData.json();
|
||||
return translations[locale];
|
||||
} catch (error) {
|
||||
console.error('Error loading translations:', error);
|
||||
|
||||
|
||||
// Return empty object as last resort
|
||||
translations[locale] = {};
|
||||
return translations[locale];
|
||||
@@ -62,13 +59,13 @@ function getNestedValue(obj, path) {
|
||||
// Try direct key match first
|
||||
if (obj && typeof obj === 'object' && path in obj) {
|
||||
const value = obj[path];
|
||||
return (typeof value === 'string') ? value : null;
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
|
||||
// Try standard dot notation for nested values
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
@@ -78,7 +75,7 @@ function getNestedValue(obj, path) {
|
||||
if (path.includes('_') && !path.includes('.')) {
|
||||
const [prefix, ...parts] = path.split('_');
|
||||
const suffix = parts.join('_');
|
||||
|
||||
|
||||
if (obj[prefix] && typeof obj[prefix] === 'object' && suffix in obj[prefix]) {
|
||||
return obj[prefix][suffix];
|
||||
}
|
||||
@@ -86,8 +83,8 @@ function getNestedValue(obj, path) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (typeof current === 'string') ? current : null;
|
||||
|
||||
return typeof current === 'string' ? current : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,17 +101,17 @@ function t(key, params = {}) {
|
||||
console.warn(`Translations for ${currentLocale} not loaded yet`);
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
// Special handling for shared_ and share_ prefixed keys
|
||||
if (key.startsWith('shared_') || key.startsWith('share_')) {
|
||||
const unprefixedKey = key.replace(/^(shared|share)_/, '');
|
||||
const prefixObj = key.startsWith('shared_') ? localeData.shared : localeData.share;
|
||||
|
||||
|
||||
if (prefixObj && typeof prefixObj === 'object' && unprefixedKey in prefixObj) {
|
||||
return interpolate(prefixObj[unprefixedKey], params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the translation value
|
||||
let value = getNestedValue(localeData, key);
|
||||
|
||||
@@ -154,12 +151,12 @@ function t(key, params = {}) {
|
||||
return interpolate(fallbackValue, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Key not found, return key
|
||||
console.warn(`Translation key not found: ${key}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
// Replace parameters
|
||||
return interpolate(value, params);
|
||||
}
|
||||
@@ -186,24 +183,24 @@ async function setLocale(locale) {
|
||||
console.error(`Locale not supported: ${locale}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Load translations if not loaded yet
|
||||
if (!translations[locale]) {
|
||||
await loadTranslations(locale);
|
||||
}
|
||||
|
||||
|
||||
// Update current locale
|
||||
currentLocale = locale;
|
||||
|
||||
|
||||
// Save locale preference
|
||||
localStorage.setItem('oxicloud-locale', locale);
|
||||
|
||||
|
||||
// Trigger an event for components to update
|
||||
window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } }));
|
||||
|
||||
|
||||
// Update all elements with data-i18n attribute
|
||||
translatePage();
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -217,10 +214,10 @@ async function initI18n() {
|
||||
if (savedLocale && supportedLocales.includes(savedLocale)) {
|
||||
currentLocale = savedLocale;
|
||||
}
|
||||
|
||||
|
||||
// Load translations for current locale
|
||||
await loadTranslations(currentLocale);
|
||||
|
||||
|
||||
// Preload English translations as fallback
|
||||
if (currentLocale !== 'en') {
|
||||
await loadTranslations('en');
|
||||
@@ -228,10 +225,10 @@ async function initI18n() {
|
||||
|
||||
// Mark loaded BEFORE translatePage so safeT resolves properly
|
||||
translationsLoaded = true;
|
||||
|
||||
|
||||
// Translate the page
|
||||
translatePage();
|
||||
|
||||
|
||||
console.log(`I18n initialized with locale: ${currentLocale}`);
|
||||
}
|
||||
|
||||
@@ -252,17 +249,17 @@ function translateElement(root) {
|
||||
// (e.g. admin.js) shadow the global t() function.
|
||||
const resolve = safeT;
|
||||
const el = root || document;
|
||||
el.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
el.querySelectorAll('[data-i18n]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n');
|
||||
element.textContent = resolve(key);
|
||||
});
|
||||
|
||||
el.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
||||
|
||||
el.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-placeholder');
|
||||
element.placeholder = resolve(key);
|
||||
});
|
||||
|
||||
el.querySelectorAll('[data-i18n-title]').forEach(element => {
|
||||
|
||||
el.querySelectorAll('[data-i18n-title]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-title');
|
||||
element.title = resolve(key);
|
||||
});
|
||||
|
||||
+429
-162
@@ -10,7 +10,7 @@
|
||||
* https://fontawesome.com/license/free
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
// ── Icon registry ──────────────────────────────────────────────
|
||||
// Each entry: [viewBox-width, svg-path-d]
|
||||
@@ -18,93 +18,351 @@
|
||||
// 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"],
|
||||
"ban": [512, "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM159.3 388.7L388.7 159.3c4.6-4.6 11.5-5.9 17.4-3.5c14.5 6 26.4 15.3 35.1 27c3.8 5.2 3.2 12.3-1.2 16.8L210.2 428.4c-4.4 4.4-11.6 5-16.8 1.2c-11.7-8.7-21-20.6-27-35.1c-2.5-5.9-1.1-12.8 3.5-17.4z"],
|
||||
"bars": [512, "M96 160C96 142.3 110.3 128 128 128L512 128C529.7 128 544 142.3 544 160C544 177.7 529.7 192 512 192L128 192C110.3 192 96 177.7 96 160zM96 320C96 302.3 110.3 288 128 288L512 288C529.7 288 544 302.3 544 320C544 337.7 529.7 352 512 352L128 352C110.3 352 96 337.7 96 320zM544 480C544 497.7 529.7 512 512 512L128 512C110.3 512 96 497.7 96 480C96 462.3 110.3 448 128 448L512 448C529.7 448 544 462.3 544 480z"],
|
||||
"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"],
|
||||
"crown": [576, "M309 106c11.4-7 19-19.7 19-34c0-22.1-17.9-40-40-40s-40 17.9-40 40c0 14.4 7.6 27 19 34L209.7 220.6c-9.1 18.2-32.7 23.4-48.6 10.7L72 160c5-6.7 8-15 8-24c0-22.1-17.9-40-40-40S0 113.9 0 136s17.9 40 40 40c.2 0 .5 0 .7 0L86.4 427.4c5.5 30.4 32 52.6 63 52.6l277.2 0c30.9 0 57.4-22.1 63-52.6L535.3 176c.2 0 .5 0 .7 0c22.1 0 40-17.9 40-40s-17.9-40-40-40s-40 17.9-40 40c0 9 3 17.3 8 24l-89.1 71.3c-15.9 12.7-39.5 7.5-48.6-10.7L309 106z"],
|
||||
"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"],
|
||||
"home": [640, "M341.8 72.6C329.5 61.2 310.5 61.2 298.3 72.6L74.3 280.6C64.7 289.6 61.5 303.5 66.3 315.7C71.1 327.9 82.8 336 96 336L112 336L112 512C112 547.3 140.7 576 176 576L464 576C499.3 576 528 547.3 528 512L528 336L544 336C557.2 336 569 327.9 573.8 315.7C578.6 303.5 575.4 289.5 565.8 280.6L341.8 72.6zM304 384L336 384C362.5 384 384 405.5 384 432L384 528L256 528L256 432C256 405.5 277.5 384 304 384z"],
|
||||
"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"],
|
||||
"images": [576, "M160 32c-35.3 0-64 28.7-64 64l0 224c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-224c0-35.3-28.7-64-64-64L160 32zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320l-144 0-48 0-80 0c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120L0 344c0 75.1 60.9 136 136 136l320 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-320 0c-48.6 0-88-39.4-88-88l0-224z"],
|
||||
"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"],
|
||||
"play": [384, "M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"],
|
||||
"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"],
|
||||
"star-outline": [576, "M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L439.6 319.9l24.6 145.7c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7l-137-73.2L155.2 490.8c-8 4.3-17.8 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l24.6-145.7L39.6 218.2c-6.4-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L274.3 13.5C278.3 5.2 286.7 0 295.9 0h-8zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 218.9l85.8 85.1c5.5 5.5 8.1 13.3 6.8 21L171.3 444.7l111.5-59.5c7-3.7 15.3-3.7 22.3 0l111.5 59.5-20.3-119.7c-1.3-7.7 1.2-15.5 6.8-21l85.8-85.1-118.3-17.4c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z"],
|
||||
"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"],
|
||||
'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'
|
||||
],
|
||||
ban: [
|
||||
512,
|
||||
'M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM159.3 388.7L388.7 159.3c4.6-4.6 11.5-5.9 17.4-3.5c14.5 6 26.4 15.3 35.1 27c3.8 5.2 3.2 12.3-1.2 16.8L210.2 428.4c-4.4 4.4-11.6 5-16.8 1.2c-11.7-8.7-21-20.6-27-35.1c-2.5-5.9-1.1-12.8 3.5-17.4z'
|
||||
],
|
||||
bars: [
|
||||
512,
|
||||
'M96 160C96 142.3 110.3 128 128 128L512 128C529.7 128 544 142.3 544 160C544 177.7 529.7 192 512 192L128 192C110.3 192 96 177.7 96 160zM96 320C96 302.3 110.3 288 128 288L512 288C529.7 288 544 302.3 544 320C544 337.7 529.7 352 512 352L128 352C110.3 352 96 337.7 96 320zM544 480C544 497.7 529.7 512 512 512L128 512C110.3 512 96 497.7 96 480C96 462.3 110.3 448 128 448L512 448C529.7 448 544 462.3 544 480z'
|
||||
],
|
||||
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'
|
||||
],
|
||||
crown: [
|
||||
576,
|
||||
'M309 106c11.4-7 19-19.7 19-34c0-22.1-17.9-40-40-40s-40 17.9-40 40c0 14.4 7.6 27 19 34L209.7 220.6c-9.1 18.2-32.7 23.4-48.6 10.7L72 160c5-6.7 8-15 8-24c0-22.1-17.9-40-40-40S0 113.9 0 136s17.9 40 40 40c.2 0 .5 0 .7 0L86.4 427.4c5.5 30.4 32 52.6 63 52.6l277.2 0c30.9 0 57.4-22.1 63-52.6L535.3 176c.2 0 .5 0 .7 0c22.1 0 40-17.9 40-40s-17.9-40-40-40s-40 17.9-40 40c0 9 3 17.3 8 24l-89.1 71.3c-15.9 12.7-39.5 7.5-48.6-10.7L309 106z'
|
||||
],
|
||||
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'
|
||||
],
|
||||
home: [
|
||||
640,
|
||||
'M341.8 72.6C329.5 61.2 310.5 61.2 298.3 72.6L74.3 280.6C64.7 289.6 61.5 303.5 66.3 315.7C71.1 327.9 82.8 336 96 336L112 336L112 512C112 547.3 140.7 576 176 576L464 576C499.3 576 528 547.3 528 512L528 336L544 336C557.2 336 569 327.9 573.8 315.7C578.6 303.5 575.4 289.5 565.8 280.6L341.8 72.6zM304 384L336 384C362.5 384 384 405.5 384 432L384 528L256 528L256 432C256 405.5 277.5 384 304 384z'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
images: [
|
||||
576,
|
||||
'M160 32c-35.3 0-64 28.7-64 64l0 224c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-224c0-35.3-28.7-64-64-64L160 32zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320l-144 0-48 0-80 0c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120L0 344c0 75.1 60.9 136 136 136l320 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-320 0c-48.6 0-88-39.4-88-88l0-224z'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
play: [
|
||||
384,
|
||||
'M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
'star-outline': [
|
||||
576,
|
||||
'M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L439.6 319.9l24.6 145.7c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7l-137-73.2L155.2 490.8c-8 4.3-17.8 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l24.6-145.7L39.6 218.2c-6.4-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L274.3 13.5C278.3 5.2 286.7 0 295.9 0h-8zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 218.9l85.8 85.1c5.5 5.5 8.1 13.3 6.8 21L171.3 444.7l111.5-59.5c7-3.7 15.3-3.7 22.3 0l111.5 59.5-20.3-119.7c-1.3-7.7 1.2-15.5 6.8-21l85.8-85.1-118.3-17.4c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z'
|
||||
],
|
||||
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'
|
||||
]
|
||||
};
|
||||
|
||||
// Icon CSS is now in /css/components/icons.css (loaded via main.css).
|
||||
@@ -118,11 +376,11 @@ const _ICONS = {
|
||||
* @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>`;
|
||||
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>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,61 +393,67 @@ function oxiIcon(name, extraClass) {
|
||||
* @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+/);
|
||||
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 = [];
|
||||
// Find the icon name (fa-xxx)
|
||||
let iconName = null;
|
||||
let isSpin = false;
|
||||
const extraClasses = [];
|
||||
|
||||
let isRegular = false; // far = outline variant
|
||||
let isRegular = false; // far = outline variant
|
||||
|
||||
for (const cls of classes) {
|
||||
if (cls === "fa-spin") { isSpin = true; continue; }
|
||||
if (cls === "far") { isRegular = true; continue; }
|
||||
if (cls.startsWith("fa-") && cls !== "fa") {
|
||||
iconName = cls.substring(3); // strip "fa-"
|
||||
} else if (cls !== "fas" && cls !== "fab" && cls !== "fa") {
|
||||
extraClasses.push(cls);
|
||||
}
|
||||
for (const cls of classes) {
|
||||
if (cls === 'fa-spin') {
|
||||
isSpin = true;
|
||||
continue;
|
||||
}
|
||||
if (cls === 'far') {
|
||||
isRegular = true;
|
||||
continue;
|
||||
}
|
||||
if (cls.startsWith('fa-') && cls !== 'fa') {
|
||||
iconName = cls.substring(3); // strip "fa-"
|
||||
} else if (cls !== 'fas' && cls !== 'fab' && cls !== 'fa') {
|
||||
extraClasses.push(cls);
|
||||
}
|
||||
}
|
||||
|
||||
// Use outline variant if available and element uses "far"
|
||||
if (isRegular && _ICONS[iconName + '-outline']) {
|
||||
iconName = iconName + '-outline';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Preserve id attribute if present
|
||||
if (el.id) svg.id = el.id;
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('fill', 'currentColor');
|
||||
path.setAttribute('d', d);
|
||||
svg.appendChild(path);
|
||||
|
||||
el.replaceWith(svg);
|
||||
}
|
||||
|
||||
// Use outline variant if available and element uses "far"
|
||||
if (isRegular && _ICONS[iconName + "-outline"]) {
|
||||
iconName = iconName + "-outline";
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Preserve id attribute if present
|
||||
if (el.id) svg.id = el.id;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("fill", "currentColor");
|
||||
path.setAttribute("d", d);
|
||||
svg.appendChild(path);
|
||||
|
||||
el.replaceWith(svg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Expose globally ────────────────────────────────────────────
|
||||
@@ -201,24 +465,27 @@ window.OxiIcons = _ICONS;
|
||||
// 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(); };
|
||||
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;
|
||||
}
|
||||
// Initial sweep once the DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
} else {
|
||||
scan();
|
||||
}
|
||||
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||
|
||||
// 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 });
|
||||
})();
|
||||
|
||||
@@ -5,18 +5,13 @@
|
||||
|
||||
// Locale files that actually exist (have full translations)
|
||||
// Keep this list in sync when adding new locale JSON files
|
||||
const AVAILABLE_LOCALES = new Set([
|
||||
'en', 'es', 'zh', 'fa', 'fr', 'de', 'pt', 'it', 'nl',
|
||||
'hi', 'ar', 'ru', 'ja', 'ko'
|
||||
]);
|
||||
const AVAILABLE_LOCALES = new Set(['en', 'es', 'zh', 'fa', 'fr', 'de', 'pt', 'it', 'nl', 'hi', 'ar', 'ru', 'ja', 'ko']);
|
||||
|
||||
// Language codes, names, and flag emojis
|
||||
// Only returns languages that have a real locale file
|
||||
function getAvailableLanguages() {
|
||||
if (typeof ALL_LANGUAGES !== 'undefined') {
|
||||
return ALL_LANGUAGES
|
||||
.filter(l => AVAILABLE_LOCALES.has(l.code))
|
||||
.map(l => ({ code: l.code, name: l.nativeName, flag: l.flag }));
|
||||
return ALL_LANGUAGES.filter((l) => AVAILABLE_LOCALES.has(l.code)).map((l) => ({ code: l.code, name: l.nativeName, flag: l.flag }));
|
||||
}
|
||||
return [
|
||||
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
||||
@@ -37,10 +32,10 @@ const rtlLanguages = ['fa', 'ar'];
|
||||
// Update HTML lang attribute and dir for RTL languages
|
||||
function updateHtmlAttributes(langCode) {
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
|
||||
// Set lang attribute
|
||||
htmlElement.setAttribute('lang', langCode);
|
||||
|
||||
|
||||
// Set dir attribute for RTL languages
|
||||
if (rtlLanguages.includes(langCode)) {
|
||||
htmlElement.setAttribute('dir', 'rtl');
|
||||
@@ -62,18 +57,18 @@ function createLanguageSelector(containerId = 'language-selector') {
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
|
||||
// Ensure container has the right class
|
||||
container.className = 'language-selector';
|
||||
|
||||
|
||||
// Get current language
|
||||
const languages = getAvailableLanguages();
|
||||
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
|
||||
const currentLang = languages.find(l => l.code === currentLocale) || languages[0];
|
||||
|
||||
const currentLang = languages.find((l) => l.code === currentLocale) || languages[0];
|
||||
|
||||
// Set initial HTML attributes
|
||||
updateHtmlAttributes(currentLocale);
|
||||
|
||||
|
||||
// Create toggle button
|
||||
const toggle = document.createElement('div');
|
||||
toggle.className = 'language-selector-toggle';
|
||||
@@ -86,14 +81,14 @@ function createLanguageSelector(containerId = 'language-selector') {
|
||||
<span class="lang-code">${currentLang.code.toUpperCase()}</span>
|
||||
<i class="fas fa-chevron-down dropdown-arrow"></i>
|
||||
`;
|
||||
|
||||
|
||||
// Create dropdown menu
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'language-selector-dropdown';
|
||||
dropdown.setAttribute('role', 'listbox');
|
||||
|
||||
|
||||
// Add language options
|
||||
languages.forEach(lang => {
|
||||
languages.forEach((lang) => {
|
||||
const option = document.createElement('div');
|
||||
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
||||
option.setAttribute('role', 'option');
|
||||
@@ -104,26 +99,26 @@ function createLanguageSelector(containerId = 'language-selector') {
|
||||
<span class="lang-name">${lang.name}</span>
|
||||
<i class="fas fa-check lang-check"></i>
|
||||
`;
|
||||
|
||||
|
||||
option.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
await selectLanguage(lang.code, container);
|
||||
});
|
||||
|
||||
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
|
||||
// Clear and build container
|
||||
container.innerHTML = '';
|
||||
container.appendChild(toggle);
|
||||
container.appendChild(dropdown);
|
||||
|
||||
|
||||
// Toggle dropdown on click
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleDropdown(container);
|
||||
});
|
||||
|
||||
|
||||
// Keyboard support
|
||||
toggle.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -133,19 +128,19 @@ function createLanguageSelector(containerId = 'language-selector') {
|
||||
closeDropdown(container);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!container.contains(e.target)) {
|
||||
closeDropdown(container);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Listen for locale changes from i18n system
|
||||
window.addEventListener('localeChanged', (e) => {
|
||||
updateSelectedLanguage(e.detail.locale, container);
|
||||
});
|
||||
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -191,10 +186,10 @@ async function selectLanguage(langCode, container) {
|
||||
if (window.i18n) {
|
||||
await window.i18n.setLocale(langCode);
|
||||
}
|
||||
|
||||
|
||||
// Update HTML lang attribute and dir for RTL languages
|
||||
updateHtmlAttributes(langCode);
|
||||
|
||||
|
||||
updateSelectedLanguage(langCode, container);
|
||||
closeDropdown(container);
|
||||
}
|
||||
@@ -204,17 +199,17 @@ async function selectLanguage(langCode, container) {
|
||||
*/
|
||||
function updateSelectedLanguage(langCode, container) {
|
||||
const languages = getAvailableLanguages();
|
||||
const lang = languages.find(l => l.code === langCode) || languages[0];
|
||||
|
||||
const lang = languages.find((l) => l.code === langCode) || languages[0];
|
||||
|
||||
// Update toggle button text
|
||||
const langCodeSpan = container.querySelector('.lang-code');
|
||||
if (langCodeSpan) {
|
||||
langCodeSpan.textContent = lang.code.toUpperCase();
|
||||
}
|
||||
|
||||
|
||||
// Update active state on options
|
||||
const options = container.querySelectorAll('.language-option');
|
||||
options.forEach(option => {
|
||||
options.forEach((option) => {
|
||||
const isActive = option.getAttribute('data-lang') === langCode;
|
||||
option.classList.toggle('active', isActive);
|
||||
option.setAttribute('aria-selected', isActive);
|
||||
@@ -224,4 +219,4 @@ function updateSelectedLanguage(langCode, container) {
|
||||
// Create language selector when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
createLanguageSelector();
|
||||
});
|
||||
});
|
||||
|
||||
+235
-243
@@ -1,243 +1,235 @@
|
||||
/**
|
||||
* Modal System for OxiCloud
|
||||
* Provides modern, styled modals to replace browser prompts/alerts
|
||||
*/
|
||||
|
||||
const Modal = {
|
||||
// Modal element references
|
||||
overlay: null,
|
||||
container: null,
|
||||
icon: null,
|
||||
title: null,
|
||||
label: null,
|
||||
input: null,
|
||||
cancelBtn: null,
|
||||
confirmBtn: null,
|
||||
closeBtn: null,
|
||||
|
||||
// Current callback
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
|
||||
// Rename mode: select only name without extension
|
||||
_selectNameOnly: false,
|
||||
|
||||
/**
|
||||
* Initialize modal system
|
||||
*/
|
||||
init() {
|
||||
this.overlay = document.getElementById('input-modal');
|
||||
if (!this.overlay) {
|
||||
console.warn('Modal overlay not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
this.title = document.getElementById('modal-title');
|
||||
this.label = document.getElementById('modal-label');
|
||||
this.input = document.getElementById('modal-input');
|
||||
this.cancelBtn = document.getElementById('modal-cancel-btn');
|
||||
this.confirmBtn = document.getElementById('modal-confirm-btn');
|
||||
this.closeBtn = document.getElementById('modal-close-btn');
|
||||
|
||||
// Event listeners
|
||||
this.cancelBtn.addEventListener('click', () => this.close(false));
|
||||
this.closeBtn.addEventListener('click', () => this.close(false));
|
||||
this.confirmBtn.addEventListener('click', () => this.confirm());
|
||||
|
||||
// Close on overlay click
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Enter and Escape keys
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.confirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show input modal (replacement for prompt())
|
||||
* @param {Object} options - Modal configuration
|
||||
* @param {string} options.title - Modal title
|
||||
* @param {string} options.label - Input label
|
||||
* @param {string} options.placeholder - Input placeholder
|
||||
* @param {string} options.value - Initial input value
|
||||
* @param {string} options.icon - Font Awesome icon class (e.g., 'fa-folder-plus')
|
||||
* @param {string} options.confirmText - Confirm button text
|
||||
* @param {string} options.cancelText - Cancel button text
|
||||
* @returns {Promise<string|null>} - Resolves with input value or null if cancelled
|
||||
*/
|
||||
prompt(options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const {
|
||||
title = 'Input',
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = '',
|
||||
icon = 'fa-keyboard',
|
||||
confirmText = null,
|
||||
cancelText = null
|
||||
} = options;
|
||||
|
||||
// Set modal content - update the icon
|
||||
const iconContainer = document.querySelector('.modal-icon');
|
||||
if (iconContainer) {
|
||||
// Replace contents with a fresh <i> that icons.js will convert
|
||||
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
// Let icons.js convert it to SVG
|
||||
if (window.replaceIconsInElement) {
|
||||
window.replaceIconsInElement(iconContainer);
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
}
|
||||
}
|
||||
this.title.textContent = title;
|
||||
this.label.textContent = label;
|
||||
this.input.placeholder = placeholder;
|
||||
this.input.value = value;
|
||||
|
||||
// Set button text (use i18n if available)
|
||||
if (confirmText) {
|
||||
this.confirmBtn.textContent = confirmText;
|
||||
} else if (window.i18n) {
|
||||
this.confirmBtn.textContent = window.i18n.t('actions.confirm');
|
||||
}
|
||||
|
||||
if (cancelText) {
|
||||
this.cancelBtn.textContent = cancelText;
|
||||
} else if (window.i18n) {
|
||||
this.cancelBtn.textContent = window.i18n.t('actions.cancel');
|
||||
}
|
||||
|
||||
// Set callbacks
|
||||
this.onConfirm = () => {
|
||||
const inputValue = this.input.value.trim();
|
||||
resolve(inputValue || null);
|
||||
};
|
||||
this.onCancel = () => resolve(null);
|
||||
|
||||
// Show modal
|
||||
this.open();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for creating new folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptNewFolder() {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.new_folder_title') || 'New folder',
|
||||
label: t('dialogs.folder_name') || 'Folder name',
|
||||
placeholder: t('dialogs.folder_placeholder') || 'My folder',
|
||||
icon: 'fa-folder-plus',
|
||||
confirmText: t('actions.create') || 'Create'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for renaming
|
||||
* @param {string} currentName - Current name of file/folder
|
||||
* @param {boolean} isFolder - Whether it's a folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptRename(currentName, isFolder = false) {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
// For files, we want to select only the name part (without extension)
|
||||
this._selectNameOnly = !isFolder;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.rename_title') || 'Renombrar',
|
||||
label: t('dialogs.new_name') || 'Nuevo nombre',
|
||||
placeholder: '',
|
||||
value: currentName,
|
||||
icon: isFolder ? 'fa-folder' : 'fa-file',
|
||||
confirmText: t('actions.rename') || 'Renombrar'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the modal
|
||||
*/
|
||||
open() {
|
||||
if (!this.overlay) return;
|
||||
|
||||
// Show overlay
|
||||
this.overlay.style.display = 'flex';
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
this.overlay.classList.add('active');
|
||||
});
|
||||
|
||||
// Focus input after animation
|
||||
setTimeout(() => {
|
||||
this.input.focus();
|
||||
|
||||
if (this._selectNameOnly) {
|
||||
// Select only the filename, excluding the extension
|
||||
const value = this.input.value;
|
||||
const lastDot = value.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
this.input.setSelectionRange(0, lastDot);
|
||||
} else {
|
||||
this.input.select();
|
||||
}
|
||||
this._selectNameOnly = false;
|
||||
} else {
|
||||
this.input.select();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
* @param {boolean} confirmed - Whether the action was confirmed
|
||||
*/
|
||||
close(confirmed = false) {
|
||||
if (!this.overlay) return;
|
||||
|
||||
this.overlay.classList.remove('active');
|
||||
|
||||
setTimeout(() => {
|
||||
this.overlay.style.display = 'none';
|
||||
|
||||
if (!confirmed && this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
// Clear callbacks
|
||||
this.onConfirm = null;
|
||||
this.onCancel = null;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm the action
|
||||
*/
|
||||
confirm() {
|
||||
if (this.onConfirm) {
|
||||
this.onConfirm();
|
||||
}
|
||||
this.close(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Modal.init();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
window.Modal = Modal;
|
||||
/**
|
||||
* Modal System for OxiCloud
|
||||
* Provides modern, styled modals to replace browser prompts/alerts
|
||||
*/
|
||||
|
||||
const Modal = {
|
||||
// Modal element references
|
||||
overlay: null,
|
||||
container: null,
|
||||
icon: null,
|
||||
title: null,
|
||||
label: null,
|
||||
input: null,
|
||||
cancelBtn: null,
|
||||
confirmBtn: null,
|
||||
closeBtn: null,
|
||||
|
||||
// Current callback
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
|
||||
// Rename mode: select only name without extension
|
||||
_selectNameOnly: false,
|
||||
|
||||
/**
|
||||
* Initialize modal system
|
||||
*/
|
||||
init() {
|
||||
this.overlay = document.getElementById('input-modal');
|
||||
if (!this.overlay) {
|
||||
console.warn('Modal overlay not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
this.title = document.getElementById('modal-title');
|
||||
this.label = document.getElementById('modal-label');
|
||||
this.input = document.getElementById('modal-input');
|
||||
this.cancelBtn = document.getElementById('modal-cancel-btn');
|
||||
this.confirmBtn = document.getElementById('modal-confirm-btn');
|
||||
this.closeBtn = document.getElementById('modal-close-btn');
|
||||
|
||||
// Event listeners
|
||||
this.cancelBtn.addEventListener('click', () => this.close(false));
|
||||
this.closeBtn.addEventListener('click', () => this.close(false));
|
||||
this.confirmBtn.addEventListener('click', () => this.confirm());
|
||||
|
||||
// Close on overlay click
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Enter and Escape keys
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.confirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show input modal (replacement for prompt())
|
||||
* @param {Object} options - Modal configuration
|
||||
* @param {string} options.title - Modal title
|
||||
* @param {string} options.label - Input label
|
||||
* @param {string} options.placeholder - Input placeholder
|
||||
* @param {string} options.value - Initial input value
|
||||
* @param {string} options.icon - Font Awesome icon class (e.g., 'fa-folder-plus')
|
||||
* @param {string} options.confirmText - Confirm button text
|
||||
* @param {string} options.cancelText - Cancel button text
|
||||
* @returns {Promise<string|null>} - Resolves with input value or null if cancelled
|
||||
*/
|
||||
prompt(options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { title = 'Input', label = '', placeholder = '', value = '', icon = 'fa-keyboard', confirmText = null, cancelText = null } = options;
|
||||
|
||||
// Set modal content - update the icon
|
||||
const iconContainer = document.querySelector('.modal-icon');
|
||||
if (iconContainer) {
|
||||
// Replace contents with a fresh <i> that icons.js will convert
|
||||
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
// Let icons.js convert it to SVG
|
||||
if (window.replaceIconsInElement) {
|
||||
window.replaceIconsInElement(iconContainer);
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
}
|
||||
}
|
||||
this.title.textContent = title;
|
||||
this.label.textContent = label;
|
||||
this.input.placeholder = placeholder;
|
||||
this.input.value = value;
|
||||
|
||||
// Set button text (use i18n if available)
|
||||
if (confirmText) {
|
||||
this.confirmBtn.textContent = confirmText;
|
||||
} else if (window.i18n) {
|
||||
this.confirmBtn.textContent = window.i18n.t('actions.confirm');
|
||||
}
|
||||
|
||||
if (cancelText) {
|
||||
this.cancelBtn.textContent = cancelText;
|
||||
} else if (window.i18n) {
|
||||
this.cancelBtn.textContent = window.i18n.t('actions.cancel');
|
||||
}
|
||||
|
||||
// Set callbacks
|
||||
this.onConfirm = () => {
|
||||
const inputValue = this.input.value.trim();
|
||||
resolve(inputValue || null);
|
||||
};
|
||||
this.onCancel = () => resolve(null);
|
||||
|
||||
// Show modal
|
||||
this.open();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for creating new folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptNewFolder() {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.new_folder_title') || 'New folder',
|
||||
label: t('dialogs.folder_name') || 'Folder name',
|
||||
placeholder: t('dialogs.folder_placeholder') || 'My folder',
|
||||
icon: 'fa-folder-plus',
|
||||
confirmText: t('actions.create') || 'Create'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for renaming
|
||||
* @param {string} currentName - Current name of file/folder
|
||||
* @param {boolean} isFolder - Whether it's a folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptRename(currentName, isFolder = false) {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
// For files, we want to select only the name part (without extension)
|
||||
this._selectNameOnly = !isFolder;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.rename_title') || 'Renombrar',
|
||||
label: t('dialogs.new_name') || 'Nuevo nombre',
|
||||
placeholder: '',
|
||||
value: currentName,
|
||||
icon: isFolder ? 'fa-folder' : 'fa-file',
|
||||
confirmText: t('actions.rename') || 'Renombrar'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the modal
|
||||
*/
|
||||
open() {
|
||||
if (!this.overlay) return;
|
||||
|
||||
// Show overlay
|
||||
this.overlay.style.display = 'flex';
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
this.overlay.classList.add('active');
|
||||
});
|
||||
|
||||
// Focus input after animation
|
||||
setTimeout(() => {
|
||||
this.input.focus();
|
||||
|
||||
if (this._selectNameOnly) {
|
||||
// Select only the filename, excluding the extension
|
||||
const value = this.input.value;
|
||||
const lastDot = value.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
this.input.setSelectionRange(0, lastDot);
|
||||
} else {
|
||||
this.input.select();
|
||||
}
|
||||
this._selectNameOnly = false;
|
||||
} else {
|
||||
this.input.select();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
* @param {boolean} confirmed - Whether the action was confirmed
|
||||
*/
|
||||
close(confirmed = false) {
|
||||
if (!this.overlay) return;
|
||||
|
||||
this.overlay.classList.remove('active');
|
||||
|
||||
setTimeout(() => {
|
||||
this.overlay.style.display = 'none';
|
||||
|
||||
if (!confirmed && this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
// Clear callbacks
|
||||
this.onConfirm = null;
|
||||
this.onCancel = null;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm the action
|
||||
*/
|
||||
confirm() {
|
||||
if (this.onConfirm) {
|
||||
this.onConfirm();
|
||||
}
|
||||
this.close(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Modal.init();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
window.Modal = Modal;
|
||||
|
||||
+355
-359
@@ -1,359 +1,355 @@
|
||||
/**
|
||||
* OxiCloud – Notification Bell Module
|
||||
*
|
||||
* Centralised notification system that renders items inside the bell dropdown
|
||||
* in the top-bar. Upload progress, quota errors, and general messages all
|
||||
* go through this module.
|
||||
*
|
||||
* Public API (on window.notifications):
|
||||
* addUploadBatch(totalFiles) → batchId
|
||||
* updateFile(batchId, fileName, pct, status)
|
||||
* finishBatch(batchId, successCount, totalFiles)
|
||||
* addNotification({ icon, iconClass, title, text })
|
||||
* clear()
|
||||
*/
|
||||
|
||||
const notifications = (() => {
|
||||
'use strict';
|
||||
|
||||
/* ── state ──────────────────────────────────────────────── */
|
||||
let _badgeCount = 0;
|
||||
let _batchSeq = 0;
|
||||
const _batches = {}; // batchId → { el, files:{}, totalFiles }
|
||||
|
||||
/* ── DOM refs (resolved lazily) ─────────────────────────── */
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
/* ── bell toggle ────────────────────────────────────────── */
|
||||
function _initBell() {
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
const wrapper = $('notif-wrapper');
|
||||
const clearBtn = $('notif-clear-btn');
|
||||
|
||||
if (!bellBtn) return;
|
||||
|
||||
bellBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const open = wrapper.classList.toggle('open');
|
||||
bellBtn.classList.toggle('active', open);
|
||||
|
||||
// Close user-menu if it's open
|
||||
const um = $('user-menu-wrapper');
|
||||
if (um) um.classList.remove('open');
|
||||
|
||||
if (open) _clearBadge();
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
// Clear all
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
const wrapper = $('notif-wrapper');
|
||||
wrapper.classList.remove('open');
|
||||
bellBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
/* ── badge helpers ──────────────────────────────────────── */
|
||||
function _incrementBadge() {
|
||||
_badgeCount++;
|
||||
_renderBadge();
|
||||
_ringBell();
|
||||
}
|
||||
function _clearBadge() {
|
||||
_badgeCount = 0;
|
||||
_renderBadge();
|
||||
}
|
||||
function _renderBadge() {
|
||||
const badge = $('notif-badge');
|
||||
if (!badge) return;
|
||||
if (_badgeCount > 0) {
|
||||
badge.classList.remove("hidden");
|
||||
badge.textContent = _badgeCount > 99 ? '99+' : _badgeCount;
|
||||
} else {
|
||||
badge.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
function _ringBell() {
|
||||
const btn = $('notif-bell-btn');
|
||||
if (!btn) return;
|
||||
btn.classList.remove('ring');
|
||||
// Force reflow so the animation restarts
|
||||
void btn.offsetWidth;
|
||||
btn.classList.add('ring');
|
||||
}
|
||||
|
||||
/* ── empty state ────────────────────────────────────────── */
|
||||
function _showEmptyIfNeeded() {
|
||||
const body = $('notif-panel-body');
|
||||
const empty = $('notif-empty');
|
||||
if (!body || !empty) return;
|
||||
// Any real items?
|
||||
const hasItems = body.querySelector('.notif-item') !== null;
|
||||
empty.style.display = hasItems ? 'none' : '';
|
||||
}
|
||||
|
||||
/* ── generic notification ───────────────────────────────── */
|
||||
function addNotification({ icon = 'fa-info-circle', iconClass = 'upload', title = '', text = '' }) {
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'notif-item';
|
||||
item.innerHTML = `
|
||||
<div class="notif-item-icon ${iconClass}"><i class="fas ${icon}"></i></div>
|
||||
<div class="notif-item-body">
|
||||
<div class="notif-item-title">${_esc(title)}</div>
|
||||
<div class="notif-item-text" title="${_esc(text)}">${_esc(text)}</div>
|
||||
<div class="notif-item-time">${_timeAgo()}</div>
|
||||
</div>
|
||||
`;
|
||||
// Insert at top
|
||||
body.insertBefore(item, body.firstChild);
|
||||
|
||||
// If panel is closed, bump badge
|
||||
const wrapper = $('notif-wrapper');
|
||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
||||
_incrementBadge();
|
||||
}
|
||||
_showEmptyIfNeeded();
|
||||
}
|
||||
|
||||
/* ── upload batch API ───────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Start tracking a new upload batch. Returns a batchId string.
|
||||
* Always uses compact folder-level display: one progress bar + counter.
|
||||
* @param {number} totalFiles
|
||||
* @param {string} [folderName] root folder name (for folder uploads)
|
||||
*/
|
||||
function addUploadBatch(totalFiles, folderName) {
|
||||
const batchId = 'batch-' + (++_batchSeq);
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return batchId;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'notif-item';
|
||||
item.id = batchId;
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const uploadingText = folderName
|
||||
? `📁 ${t('upload.uploading')} ${_esc(folderName)}…`
|
||||
: t('upload.uploading');
|
||||
const filesLabel = t('upload.files');
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div>
|
||||
<div class="notif-item-body">
|
||||
<div class="notif-item-title">${uploadingText}</div>
|
||||
<div class="notif-upload-current" id="${batchId}-current"></div>
|
||||
<div class="notif-upload-progress">
|
||||
<div class="notif-upload-bar"><div class="notif-upload-fill" id="${batchId}-fill"></div></div>
|
||||
<div class="notif-upload-detail">
|
||||
<span class="notif-upload-pct" id="${batchId}-pct">0%</span>
|
||||
<span class="notif-upload-stats" id="${batchId}-stats">0 / ${totalFiles} ${filesLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notif-item-time">${_timeAgo()}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at top
|
||||
body.insertBefore(item, body.firstChild);
|
||||
|
||||
_batches[batchId] = {
|
||||
el: item,
|
||||
totalFiles,
|
||||
completed: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
lastLabelUpdateTs: 0,
|
||||
lastLabelFile: ''
|
||||
};
|
||||
_showEmptyIfNeeded();
|
||||
|
||||
// Auto open
|
||||
const wrapper = $('notif-wrapper');
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
if (wrapper && !wrapper.classList.contains('open')) {
|
||||
wrapper.classList.add('open');
|
||||
if (bellBtn) bellBtn.classList.add('active');
|
||||
}
|
||||
|
||||
return batchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current-file label inside a batch.
|
||||
* This does NOT create any DOM rows — just a single text update.
|
||||
* @param {string} batchId
|
||||
* @param {string} fileName
|
||||
* @param {number} pct 0-100
|
||||
* @param {'uploading'|'done'|'error'} status
|
||||
*/
|
||||
function updateFile(batchId, fileName, pct, status) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
|
||||
if (status === 'error') batch.errorCount = (batch.errorCount || 0) + 1;
|
||||
|
||||
// Update the current-file label AND progress bar during upload
|
||||
if (status === 'uploading') {
|
||||
const now = Date.now();
|
||||
const fileChanged = batch.lastLabelFile !== fileName;
|
||||
// Throttle DOM updates to avoid reflow storms (every 300ms or on file change)
|
||||
const shouldUpdate = fileChanged || now - (batch.lastLabelUpdateTs || 0) >= 300 || pct >= 100;
|
||||
if (!shouldUpdate) return;
|
||||
|
||||
// Show just the file name being uploaded (truncate long paths)
|
||||
const curEl = $(batchId + '-current');
|
||||
if (curEl) {
|
||||
const shortName = fileName.length > 50
|
||||
? '…' + fileName.slice(-49)
|
||||
: fileName;
|
||||
curEl.textContent = shortName;
|
||||
}
|
||||
batch.lastLabelFile = fileName;
|
||||
batch.lastLabelUpdateTs = now;
|
||||
|
||||
// Update progress bar with per-file granularity:
|
||||
// overall% = (completed_files + current_file_fraction) / total_files
|
||||
const overallPct = Math.round(
|
||||
((batch.completed + (pct / 100)) / batch.totalFiles) * 100
|
||||
);
|
||||
const fillEl = $(batchId + '-fill');
|
||||
const pctEl = $(batchId + '-pct');
|
||||
if (fillEl) fillEl.style.width = overallPct + '%';
|
||||
if (pctEl) pctEl.textContent = overallPct + '%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a file as completed within a batch (updates overall bar).
|
||||
* DOM updates are throttled to every 5 files to avoid reflow starvation.
|
||||
*/
|
||||
function fileCompleted(batchId, success) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
batch.completed++;
|
||||
if (success) batch.successCount++;
|
||||
|
||||
// Throttle DOM updates: every 5 files, or on the very last file
|
||||
const isLast = batch.completed >= batch.totalFiles;
|
||||
if (!isLast && batch.completed % 5 !== 0) return;
|
||||
|
||||
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
||||
const fillEl = $(batchId + '-fill');
|
||||
const pctEl = $(batchId + '-pct');
|
||||
const statsEl = $(batchId + '-stats');
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const filesLabel = t('upload.files');
|
||||
|
||||
if (fillEl) fillEl.style.width = pctVal + '%';
|
||||
if (pctEl) pctEl.textContent = pctVal + '%';
|
||||
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalise a batch – update icon and title.
|
||||
*/
|
||||
function finishBatch(batchId, successCount, totalFiles) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
|
||||
const fillEl = $(batchId + '-fill');
|
||||
if (fillEl) {
|
||||
fillEl.style.width = '100%';
|
||||
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
||||
}
|
||||
|
||||
const titleEl = batch.el.querySelector('.notif-item-title');
|
||||
const iconEl = batch.el.querySelector('.notif-item-icon');
|
||||
|
||||
// Clear the current-file label
|
||||
const curEl = $(batchId + '-current');
|
||||
if (curEl) curEl.textContent = '';
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const filesLabel = t('upload.files');
|
||||
const completeText = t('upload.complete', { count: successCount, total: totalFiles });
|
||||
if (titleEl) titleEl.textContent = completeText;
|
||||
|
||||
if (iconEl) {
|
||||
if (successCount === totalFiles) {
|
||||
iconEl.className = 'notif-item-icon success';
|
||||
iconEl.innerHTML = '<i class="fas fa-check-circle"></i>';
|
||||
} else {
|
||||
iconEl.className = 'notif-item-icon error';
|
||||
iconEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
// If the panel is closed, bump badge
|
||||
const wrapper = $('notif-wrapper');
|
||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
||||
_incrementBadge();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── clear all ──────────────────────────────────────────── */
|
||||
function clear() {
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return;
|
||||
// Remove all notif-items
|
||||
body.querySelectorAll('.notif-item').forEach(el => el.remove());
|
||||
_clearBadge();
|
||||
_showEmptyIfNeeded();
|
||||
// automatically close notification center on clear
|
||||
setTimeout(
|
||||
()=> { close(); },
|
||||
600
|
||||
);
|
||||
}
|
||||
|
||||
/* ── util ───────────────────────────────────────────────── */
|
||||
function _esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
function _timeAgo() {
|
||||
const now = new Date();
|
||||
const h = String(now.getHours()).padStart(2, '0');
|
||||
const m = String(now.getMinutes()).padStart(2, '0');
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
/* ── init on DOM ready ──────────────────────────────────── */
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', _initBell);
|
||||
} else {
|
||||
_initBell();
|
||||
}
|
||||
|
||||
/* ── public API ─────────────────────────────────────────── */
|
||||
return {
|
||||
addUploadBatch,
|
||||
updateFile,
|
||||
fileCompleted,
|
||||
finishBatch,
|
||||
addNotification,
|
||||
clear,
|
||||
};
|
||||
})();
|
||||
|
||||
window.notifications = notifications;
|
||||
/**
|
||||
* OxiCloud – Notification Bell Module
|
||||
*
|
||||
* Centralised notification system that renders items inside the bell dropdown
|
||||
* in the top-bar. Upload progress, quota errors, and general messages all
|
||||
* go through this module.
|
||||
*
|
||||
* Public API (on window.notifications):
|
||||
* addUploadBatch(totalFiles) → batchId
|
||||
* updateFile(batchId, fileName, pct, status)
|
||||
* finishBatch(batchId, successCount, totalFiles)
|
||||
* addNotification({ icon, iconClass, title, text })
|
||||
* clear()
|
||||
*/
|
||||
|
||||
const notifications = (() => {
|
||||
'use strict';
|
||||
|
||||
/* ── state ──────────────────────────────────────────────── */
|
||||
let _badgeCount = 0;
|
||||
let _batchSeq = 0;
|
||||
const _batches = {}; // batchId → { el, files:{}, totalFiles }
|
||||
|
||||
/* ── DOM refs (resolved lazily) ─────────────────────────── */
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
/* ── bell toggle ────────────────────────────────────────── */
|
||||
function _initBell() {
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
const wrapper = $('notif-wrapper');
|
||||
const clearBtn = $('notif-clear-btn');
|
||||
|
||||
if (!bellBtn) return;
|
||||
|
||||
bellBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const open = wrapper.classList.toggle('open');
|
||||
bellBtn.classList.toggle('active', open);
|
||||
|
||||
// Close user-menu if it's open
|
||||
const um = $('user-menu-wrapper');
|
||||
if (um) um.classList.remove('open');
|
||||
|
||||
if (open) _clearBadge();
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
// Clear all
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
const wrapper = $('notif-wrapper');
|
||||
wrapper.classList.remove('open');
|
||||
bellBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
/* ── badge helpers ──────────────────────────────────────── */
|
||||
function _incrementBadge() {
|
||||
_badgeCount++;
|
||||
_renderBadge();
|
||||
_ringBell();
|
||||
}
|
||||
function _clearBadge() {
|
||||
_badgeCount = 0;
|
||||
_renderBadge();
|
||||
}
|
||||
function _renderBadge() {
|
||||
const badge = $('notif-badge');
|
||||
if (!badge) return;
|
||||
if (_badgeCount > 0) {
|
||||
badge.classList.remove('hidden');
|
||||
badge.textContent = _badgeCount > 99 ? '99+' : _badgeCount;
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
function _ringBell() {
|
||||
const btn = $('notif-bell-btn');
|
||||
if (!btn) return;
|
||||
btn.classList.remove('ring');
|
||||
// Force reflow so the animation restarts
|
||||
void btn.offsetWidth;
|
||||
btn.classList.add('ring');
|
||||
}
|
||||
|
||||
/* ── empty state ────────────────────────────────────────── */
|
||||
function _showEmptyIfNeeded() {
|
||||
const body = $('notif-panel-body');
|
||||
const empty = $('notif-empty');
|
||||
if (!body || !empty) return;
|
||||
// Any real items?
|
||||
const hasItems = body.querySelector('.notif-item') !== null;
|
||||
empty.style.display = hasItems ? 'none' : '';
|
||||
}
|
||||
|
||||
/* ── generic notification ───────────────────────────────── */
|
||||
function addNotification({ icon = 'fa-info-circle', iconClass = 'upload', title = '', text = '' }) {
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'notif-item';
|
||||
item.innerHTML = `
|
||||
<div class="notif-item-icon ${iconClass}"><i class="fas ${icon}"></i></div>
|
||||
<div class="notif-item-body">
|
||||
<div class="notif-item-title">${_esc(title)}</div>
|
||||
<div class="notif-item-text" title="${_esc(text)}">${_esc(text)}</div>
|
||||
<div class="notif-item-time">${_timeAgo()}</div>
|
||||
</div>
|
||||
`;
|
||||
// Insert at top
|
||||
body.insertBefore(item, body.firstChild);
|
||||
|
||||
// If panel is closed, bump badge
|
||||
const wrapper = $('notif-wrapper');
|
||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
||||
_incrementBadge();
|
||||
}
|
||||
_showEmptyIfNeeded();
|
||||
}
|
||||
|
||||
/* ── upload batch API ───────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Start tracking a new upload batch. Returns a batchId string.
|
||||
* Always uses compact folder-level display: one progress bar + counter.
|
||||
* @param {number} totalFiles
|
||||
* @param {string} [folderName] root folder name (for folder uploads)
|
||||
*/
|
||||
function addUploadBatch(totalFiles, folderName) {
|
||||
const batchId = 'batch-' + ++_batchSeq;
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return batchId;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'notif-item';
|
||||
item.id = batchId;
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
|
||||
const filesLabel = t('upload.files');
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div>
|
||||
<div class="notif-item-body">
|
||||
<div class="notif-item-title">${uploadingText}</div>
|
||||
<div class="notif-upload-current" id="${batchId}-current"></div>
|
||||
<div class="notif-upload-progress">
|
||||
<div class="notif-upload-bar"><div class="notif-upload-fill" id="${batchId}-fill"></div></div>
|
||||
<div class="notif-upload-detail">
|
||||
<span class="notif-upload-pct" id="${batchId}-pct">0%</span>
|
||||
<span class="notif-upload-stats" id="${batchId}-stats">0 / ${totalFiles} ${filesLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notif-item-time">${_timeAgo()}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at top
|
||||
body.insertBefore(item, body.firstChild);
|
||||
|
||||
_batches[batchId] = {
|
||||
el: item,
|
||||
totalFiles,
|
||||
completed: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
lastLabelUpdateTs: 0,
|
||||
lastLabelFile: ''
|
||||
};
|
||||
_showEmptyIfNeeded();
|
||||
|
||||
// Auto open
|
||||
const wrapper = $('notif-wrapper');
|
||||
const bellBtn = $('notif-bell-btn');
|
||||
if (wrapper && !wrapper.classList.contains('open')) {
|
||||
wrapper.classList.add('open');
|
||||
if (bellBtn) bellBtn.classList.add('active');
|
||||
}
|
||||
|
||||
return batchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current-file label inside a batch.
|
||||
* This does NOT create any DOM rows — just a single text update.
|
||||
* @param {string} batchId
|
||||
* @param {string} fileName
|
||||
* @param {number} pct 0-100
|
||||
* @param {'uploading'|'done'|'error'} status
|
||||
*/
|
||||
function updateFile(batchId, fileName, pct, status) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
|
||||
if (status === 'error') batch.errorCount = (batch.errorCount || 0) + 1;
|
||||
|
||||
// Update the current-file label AND progress bar during upload
|
||||
if (status === 'uploading') {
|
||||
const now = Date.now();
|
||||
const fileChanged = batch.lastLabelFile !== fileName;
|
||||
// Throttle DOM updates to avoid reflow storms (every 300ms or on file change)
|
||||
const shouldUpdate = fileChanged || now - (batch.lastLabelUpdateTs || 0) >= 300 || pct >= 100;
|
||||
if (!shouldUpdate) return;
|
||||
|
||||
// Show just the file name being uploaded (truncate long paths)
|
||||
const curEl = $(batchId + '-current');
|
||||
if (curEl) {
|
||||
const shortName = fileName.length > 50 ? '…' + fileName.slice(-49) : fileName;
|
||||
curEl.textContent = shortName;
|
||||
}
|
||||
batch.lastLabelFile = fileName;
|
||||
batch.lastLabelUpdateTs = now;
|
||||
|
||||
// Update progress bar with per-file granularity:
|
||||
// overall% = (completed_files + current_file_fraction) / total_files
|
||||
const overallPct = Math.round(((batch.completed + pct / 100) / batch.totalFiles) * 100);
|
||||
const fillEl = $(batchId + '-fill');
|
||||
const pctEl = $(batchId + '-pct');
|
||||
if (fillEl) fillEl.style.width = overallPct + '%';
|
||||
if (pctEl) pctEl.textContent = overallPct + '%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a file as completed within a batch (updates overall bar).
|
||||
* DOM updates are throttled to every 5 files to avoid reflow starvation.
|
||||
*/
|
||||
function fileCompleted(batchId, success) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
batch.completed++;
|
||||
if (success) batch.successCount++;
|
||||
|
||||
// Throttle DOM updates: every 5 files, or on the very last file
|
||||
const isLast = batch.completed >= batch.totalFiles;
|
||||
if (!isLast && batch.completed % 5 !== 0) return;
|
||||
|
||||
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
||||
const fillEl = $(batchId + '-fill');
|
||||
const pctEl = $(batchId + '-pct');
|
||||
const statsEl = $(batchId + '-stats');
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const filesLabel = t('upload.files');
|
||||
|
||||
if (fillEl) fillEl.style.width = pctVal + '%';
|
||||
if (pctEl) pctEl.textContent = pctVal + '%';
|
||||
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalise a batch – update icon and title.
|
||||
*/
|
||||
function finishBatch(batchId, successCount, totalFiles) {
|
||||
const batch = _batches[batchId];
|
||||
if (!batch) return;
|
||||
|
||||
const fillEl = $(batchId + '-fill');
|
||||
if (fillEl) {
|
||||
fillEl.style.width = '100%';
|
||||
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
||||
}
|
||||
|
||||
const titleEl = batch.el.querySelector('.notif-item-title');
|
||||
const iconEl = batch.el.querySelector('.notif-item-icon');
|
||||
|
||||
// Clear the current-file label
|
||||
const curEl = $(batchId + '-current');
|
||||
if (curEl) curEl.textContent = '';
|
||||
|
||||
const t = window.i18n?.t || ((k) => k);
|
||||
const filesLabel = t('upload.files');
|
||||
const completeText = t('upload.complete', {
|
||||
count: successCount,
|
||||
total: totalFiles
|
||||
});
|
||||
if (titleEl) titleEl.textContent = completeText;
|
||||
|
||||
if (iconEl) {
|
||||
if (successCount === totalFiles) {
|
||||
iconEl.className = 'notif-item-icon success';
|
||||
iconEl.innerHTML = '<i class="fas fa-check-circle"></i>';
|
||||
} else {
|
||||
iconEl.className = 'notif-item-icon error';
|
||||
iconEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
// If the panel is closed, bump badge
|
||||
const wrapper = $('notif-wrapper');
|
||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
||||
_incrementBadge();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── clear all ──────────────────────────────────────────── */
|
||||
function clear() {
|
||||
const body = $('notif-panel-body');
|
||||
if (!body) return;
|
||||
// Remove all notif-items
|
||||
body.querySelectorAll('.notif-item').forEach((el) => el.remove());
|
||||
_clearBadge();
|
||||
_showEmptyIfNeeded();
|
||||
// automatically close notification center on clear
|
||||
setTimeout(() => {
|
||||
close();
|
||||
}, 600);
|
||||
}
|
||||
|
||||
/* ── util ───────────────────────────────────────────────── */
|
||||
function _esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
function _timeAgo() {
|
||||
const now = new Date();
|
||||
const h = String(now.getHours()).padStart(2, '0');
|
||||
const m = String(now.getMinutes()).padStart(2, '0');
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
/* ── init on DOM ready ──────────────────────────────────── */
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', _initBell);
|
||||
} else {
|
||||
_initBell();
|
||||
}
|
||||
|
||||
/* ── public API ─────────────────────────────────────────── */
|
||||
return {
|
||||
addUploadBatch,
|
||||
updateFile,
|
||||
fileCompleted,
|
||||
finishBatch,
|
||||
addNotification,
|
||||
clear
|
||||
};
|
||||
})();
|
||||
|
||||
window.notifications = notifications;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Service Worker registration — runs after page load.
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(function () { /* registered */ })
|
||||
.catch(function (err) { console.log('Service Worker registration failed:', err); });
|
||||
});
|
||||
}
|
||||
// Service Worker registration — runs after page load.
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then(function () {
|
||||
/* registered */
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log('Service Worker registration failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Apply saved theme immediately (render-blocking) to prevent FOUC.
|
||||
// This file MUST be loaded without "defer" or "async".
|
||||
if(localStorage.getItem('oxicloud_theme')==='dark')document.documentElement.setAttribute('data-theme','dark');
|
||||
// Apply saved theme immediately (render-blocking) to prevent FOUC.
|
||||
// This file MUST be loaded without "defer" or "async".
|
||||
if (localStorage.getItem('oxicloud_theme') === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
|
||||
|
||||
+490
-290
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,9 @@ const contextMenus = {
|
||||
if (!label) return;
|
||||
label.textContent = window.i18n
|
||||
? window.i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite')
|
||||
: (isFavorite ? 'Remove from favorites' : 'Add to favorites');
|
||||
: isFavorite
|
||||
? 'Remove from favorites'
|
||||
: 'Add to favorites';
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -26,10 +28,7 @@ const contextMenus = {
|
||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
||||
// Don't show WOPI editor for image files - they should use inline preview
|
||||
const isImage = targetFile && targetFile.mime_type && targetFile.mime_type.startsWith('image/');
|
||||
const show = targetFile &&
|
||||
!isImage &&
|
||||
window.wopiEditor &&
|
||||
await window.wopiEditor.canEdit(targetFile.name);
|
||||
const show = targetFile && !isImage && window.wopiEditor && (await window.wopiEditor.canEdit(targetFile.name));
|
||||
|
||||
wopiEdit.classList.toggle('hidden', !show);
|
||||
wopiEditTab.classList.toggle('hidden', !show);
|
||||
@@ -59,10 +58,7 @@ const contextMenus = {
|
||||
// Folder context menu options
|
||||
document.getElementById('download-folder-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFolder) {
|
||||
window.fileOps.downloadFolder(
|
||||
window.app.contextMenuTargetFolder.id,
|
||||
window.app.contextMenuTargetFolder.name
|
||||
);
|
||||
window.fileOps.downloadFolder(window.app.contextMenuTargetFolder.id, window.app.contextMenuTargetFolder.name);
|
||||
}
|
||||
window.ui.closeContextMenu();
|
||||
});
|
||||
@@ -80,12 +76,7 @@ const contextMenus = {
|
||||
}
|
||||
} else {
|
||||
// Add to favorites
|
||||
const ok = await window.favorites.addToFavorites(
|
||||
folder.id,
|
||||
folder.name,
|
||||
'folder',
|
||||
folder.parent_id
|
||||
);
|
||||
const ok = await window.favorites.addToFavorites(folder.id, folder.name, 'folder', folder.parent_id);
|
||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||
window.ui.setFavoriteVisualState(folder.id, 'folder', true);
|
||||
}
|
||||
@@ -130,9 +121,11 @@ const contextMenus = {
|
||||
if (window.app.contextMenuTargetFile) {
|
||||
// Capture reference before context menu cleanup nullifies it
|
||||
const file = window.app.contextMenuTargetFile;
|
||||
fetch(`/api/files/${file.id}?metadata=true`, { credentials: 'same-origin' })
|
||||
.then(response => response.json())
|
||||
.then(fileDetails => {
|
||||
fetch(`/api/files/${file.id}?metadata=true`, {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((fileDetails) => {
|
||||
// Check if viewable file type (images, PDFs, text files)
|
||||
if (window.ui && window.ui.isViewableFile(fileDetails)) {
|
||||
// Open with inline viewer
|
||||
@@ -147,7 +140,7 @@ const contextMenus = {
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error fetching file details:', error);
|
||||
// On error, fallback to download
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
@@ -174,10 +167,7 @@ const contextMenus = {
|
||||
|
||||
document.getElementById('download-file-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFile) {
|
||||
window.fileOps.downloadFile(
|
||||
window.app.contextMenuTargetFile.id,
|
||||
window.app.contextMenuTargetFile.name
|
||||
);
|
||||
window.fileOps.downloadFile(window.app.contextMenuTargetFile.id, window.app.contextMenuTargetFile.name);
|
||||
}
|
||||
window.ui.closeFileContextMenu();
|
||||
});
|
||||
@@ -195,12 +185,7 @@ const contextMenus = {
|
||||
}
|
||||
} else {
|
||||
// Add to favorites
|
||||
const ok = await window.favorites.addToFavorites(
|
||||
file.id,
|
||||
file.name,
|
||||
'file',
|
||||
file.folder_id
|
||||
);
|
||||
const ok = await window.favorites.addToFavorites(file.id, file.name, 'file', file.folder_id);
|
||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||
window.ui.setFavoriteVisualState(file.id, 'file', true);
|
||||
}
|
||||
@@ -285,33 +270,27 @@ const contextMenus = {
|
||||
const targetId = window.app.selectedTargetFolderId;
|
||||
const items = window.app.batchMoveItems || [];
|
||||
|
||||
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
|
||||
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
|
||||
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
||||
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||
|
||||
let result = await window.fileOps.batchCopy(fileIds, folderIds, targetId);
|
||||
|
||||
let result = await window.fileOps.batchCopy( fileIds, folderIds, targetId);
|
||||
|
||||
this.closeMoveDialog();
|
||||
window.multiSelect.clear();
|
||||
window.loadFiles();
|
||||
|
||||
window.multiSelect.showBatchResult( "copy", result);
|
||||
window.multiSelect.showBatchResult('copy', result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single item copy
|
||||
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
||||
const success = await window.fileOps.copyFile(
|
||||
window.app.contextMenuTargetFile.id,
|
||||
window.app.selectedTargetFolderId
|
||||
);
|
||||
const success = await window.fileOps.copyFile(window.app.contextMenuTargetFile.id, window.app.selectedTargetFolderId);
|
||||
if (success) {
|
||||
this.closeMoveDialog();
|
||||
}
|
||||
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
||||
const success = await window.fileOps.copyFolder(
|
||||
window.app.contextMenuTargetFolder.id,
|
||||
window.app.selectedTargetFolderId
|
||||
);
|
||||
const success = await window.fileOps.copyFolder(window.app.contextMenuTargetFolder.id, window.app.selectedTargetFolderId);
|
||||
if (success) {
|
||||
this.closeMoveDialog();
|
||||
}
|
||||
@@ -324,32 +303,26 @@ const contextMenus = {
|
||||
const targetId = window.app.selectedTargetFolderId;
|
||||
const items = window.app.batchMoveItems || [];
|
||||
|
||||
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
|
||||
const folderIds = items.filter(i => i.type === 'folder' && i.id !== targetId).map(i => i.id);
|
||||
|
||||
let result = await window.fileOps.batchMove( fileIds, folderIds, targetId);
|
||||
|
||||
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
||||
const folderIds = items.filter((i) => i.type === 'folder' && i.id !== targetId).map((i) => i.id);
|
||||
|
||||
let result = await window.fileOps.batchMove(fileIds, folderIds, targetId);
|
||||
|
||||
this.closeMoveDialog();
|
||||
window.multiSelect.clear();
|
||||
window.loadFiles();
|
||||
window.multiSelect.showBatchResult( "move", result);
|
||||
|
||||
window.multiSelect.showBatchResult('move', result);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
||||
const success = await window.fileOps.moveFile(
|
||||
window.app.contextMenuTargetFile.id,
|
||||
window.app.selectedTargetFolderId
|
||||
);
|
||||
const success = await window.fileOps.moveFile(window.app.contextMenuTargetFile.id, window.app.selectedTargetFolderId);
|
||||
if (success) {
|
||||
this.closeMoveDialog();
|
||||
}
|
||||
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
||||
const success = await window.fileOps.moveFolder(
|
||||
window.app.contextMenuTargetFolder.id,
|
||||
window.app.selectedTargetFolderId
|
||||
);
|
||||
const success = await window.fileOps.moveFolder(window.app.contextMenuTargetFolder.id, window.app.selectedTargetFolderId);
|
||||
if (success) {
|
||||
this.closeMoveDialog();
|
||||
}
|
||||
@@ -416,7 +389,7 @@ const contextMenus = {
|
||||
window.app.moveDialogMode = mode;
|
||||
|
||||
// Reset selection
|
||||
window.app.selectedTargetFolderId = "";
|
||||
window.app.selectedTargetFolderId = '';
|
||||
|
||||
// Ensure we have the home folder ID BEFORE calculating startFolderId
|
||||
if (!window.app.userHomeFolderId) {
|
||||
@@ -442,7 +415,16 @@ const contextMenus = {
|
||||
startFolderId = window.app.userHomeFolderId || null;
|
||||
}
|
||||
|
||||
console.log('[Move Dialog] showMoveDialog - item:', item, 'mode:', mode, 'startFolderId:', startFolderId, 'userHomeFolderId:', window.app.userHomeFolderId);
|
||||
console.log(
|
||||
'[Move Dialog] showMoveDialog - item:',
|
||||
item,
|
||||
'mode:',
|
||||
mode,
|
||||
'startFolderId:',
|
||||
startFolderId,
|
||||
'userHomeFolderId:',
|
||||
window.app.userHomeFolderId
|
||||
);
|
||||
|
||||
// Store the item being moved and navigation state
|
||||
window.app.moveDialogItemId = item.id;
|
||||
@@ -460,9 +442,14 @@ const contextMenus = {
|
||||
|
||||
// Update dialog title (preserve icon)
|
||||
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
||||
const titleText = mode === 'file' ?
|
||||
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') :
|
||||
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder');
|
||||
const titleText =
|
||||
mode === 'file'
|
||||
? window.i18n
|
||||
? window.i18n.t('dialogs.move_file')
|
||||
: 'Move file'
|
||||
: window.i18n
|
||||
? window.i18n.t('dialogs.move_folder')
|
||||
: 'Move folder';
|
||||
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
||||
|
||||
// Load folders for the starting location
|
||||
@@ -525,7 +512,6 @@ const contextMenus = {
|
||||
*/
|
||||
async loadMoveDialogFolders(parentFolderId) {
|
||||
try {
|
||||
|
||||
// Ensure we have the home folder ID before proceeding
|
||||
if (!window.app.userHomeFolderId) {
|
||||
await window.resolveHomeFolder();
|
||||
@@ -555,7 +541,7 @@ const contextMenus = {
|
||||
|
||||
// The contents endpoint returns an array of child folders
|
||||
// The fallback /api/folders returns root folders (home folder itself)
|
||||
const folders = Array.isArray(data) ? data : (data.folders || []);
|
||||
const folders = Array.isArray(data) ? data : data.folders || [];
|
||||
console.log('[Move Dialog] Loaded folders:', folders.length, 'folders:', folders);
|
||||
|
||||
const folderSelectContainer = document.getElementById('folder-select-container');
|
||||
@@ -582,7 +568,7 @@ const contextMenus = {
|
||||
<span>${window.i18n ? window.i18n.t('dialogs.select_this_folder') : 'Select this folder'}</span>
|
||||
`;
|
||||
currentFolderOption.addEventListener('click', () => {
|
||||
document.querySelectorAll('.folder-select-item').forEach(item => {
|
||||
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
currentFolderOption.classList.add('selected');
|
||||
@@ -606,9 +592,7 @@ const contextMenus = {
|
||||
if (currentBreadcrumb.length > 0) {
|
||||
// Remove current folder from breadcrumb
|
||||
currentBreadcrumb.pop();
|
||||
const parentFolder = currentBreadcrumb.length > 0
|
||||
? currentBreadcrumb[currentBreadcrumb.length - 1]
|
||||
: null;
|
||||
const parentFolder = currentBreadcrumb.length > 0 ? currentBreadcrumb[currentBreadcrumb.length - 1] : null;
|
||||
window.app.moveDialogBreadcrumb = currentBreadcrumb;
|
||||
window.app.moveDialogCurrentFolderId = parentFolder ? parentFolder.id : null;
|
||||
this.loadMoveDialogFolders(parentFolder ? parentFolder.id : null);
|
||||
@@ -623,7 +607,7 @@ const contextMenus = {
|
||||
}
|
||||
|
||||
// Add subfolders (clicking navigates INTO the folder)
|
||||
folders.forEach(folder => {
|
||||
folders.forEach((folder) => {
|
||||
// Skip the item being moved (to prevent moving a folder into itself)
|
||||
if (mode === 'folder' && folder.id === itemId) {
|
||||
return;
|
||||
@@ -661,11 +645,11 @@ const contextMenus = {
|
||||
<span>${window.i18n ? window.i18n.t('dialogs.move_to_home') : 'Move to Home folder'}</span>
|
||||
`;
|
||||
homeOption.addEventListener('click', () => {
|
||||
document.querySelectorAll('.folder-select-item').forEach(item => {
|
||||
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
homeOption.classList.add('selected');
|
||||
window.app.selectedTargetFolderId = ''; // Empty means root/home
|
||||
window.app.selectedTargetFolderId = ''; // Empty means root/home
|
||||
});
|
||||
folderSelectContainer.appendChild(homeOption);
|
||||
} else if (folders.length === 0) {
|
||||
@@ -780,147 +764,150 @@ const contextMenus = {
|
||||
*/
|
||||
async showShareDialog(item, itemType) {
|
||||
try {
|
||||
const shareDialog = document.getElementById('share-dialog');
|
||||
if (!shareDialog) {
|
||||
console.error('Share dialog element not found in DOM');
|
||||
window.ui.showNotification('Error', 'Share dialog not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update dialog title — use the <span> inside header to preserve <i> icon
|
||||
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
||||
if (dialogHeader) {
|
||||
const headerSpan = dialogHeader.querySelector('span');
|
||||
const titleText = itemType === 'file' ?
|
||||
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') :
|
||||
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder');
|
||||
if (headerSpan) {
|
||||
headerSpan.textContent = titleText;
|
||||
} else {
|
||||
dialogHeader.textContent = titleText;
|
||||
const shareDialog = document.getElementById('share-dialog');
|
||||
if (!shareDialog) {
|
||||
console.error('Share dialog element not found in DOM');
|
||||
window.ui.showNotification('Error', 'Share dialog not available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const itemName = document.getElementById('shared-item-name');
|
||||
if (itemName) itemName.textContent = item.name;
|
||||
|
||||
// Reset form
|
||||
const pwField = document.getElementById('share-password');
|
||||
const expField = document.getElementById('share-expiration');
|
||||
if (pwField) pwField.value = '';
|
||||
if (expField) expField.value = '';
|
||||
const permRead = document.getElementById('share-permission-read');
|
||||
const permWrite = document.getElementById('share-permission-write');
|
||||
const permReshare = document.getElementById('share-permission-reshare');
|
||||
if (permRead) permRead.checked = true;
|
||||
if (permWrite) permWrite.checked = false;
|
||||
if (permReshare) permReshare.checked = false;
|
||||
|
||||
// Store the current item and type for use when creating the share
|
||||
window.app.shareDialogItem = item;
|
||||
window.app.shareDialogItemType = itemType;
|
||||
|
||||
// Check if item already has shares (async API call)
|
||||
const existingShares = await window.fileSharing.getSharedLinksForItem(item.id, itemType);
|
||||
const existingSharesContainer = document.getElementById('existing-shares-container');
|
||||
|
||||
// Clear existing shares container
|
||||
existingSharesContainer.innerHTML = '';
|
||||
|
||||
if (existingShares.length > 0) {
|
||||
document.getElementById('existing-shares-section').classList.remove('hidden');
|
||||
|
||||
// Create elements for each existing share
|
||||
existingShares.forEach(share => {
|
||||
const shareEl = document.createElement('div');
|
||||
shareEl.className = 'existing-share-item';
|
||||
|
||||
const expiresText = share.expires_at ?
|
||||
`Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
|
||||
'No expiration';
|
||||
|
||||
// Share URL
|
||||
const urlDiv = document.createElement('div');
|
||||
urlDiv.className = 'share-url';
|
||||
urlDiv.textContent = share.url;
|
||||
shareEl.appendChild(urlDiv);
|
||||
|
||||
// Share info
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'share-info';
|
||||
if (share.has_password) {
|
||||
const protectedSpan = document.createElement('span');
|
||||
protectedSpan.className = 'share-protected';
|
||||
protectedSpan.innerHTML = '<i class="fas fa-lock"></i> Password protected';
|
||||
infoDiv.appendChild(protectedSpan);
|
||||
// Update dialog title — use the <span> inside header to preserve <i> icon
|
||||
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
||||
if (dialogHeader) {
|
||||
const headerSpan = dialogHeader.querySelector('span');
|
||||
const titleText =
|
||||
itemType === 'file'
|
||||
? window.i18n
|
||||
? window.i18n.t('dialogs.share_file')
|
||||
: 'Share file'
|
||||
: window.i18n
|
||||
? window.i18n.t('dialogs.share_folder')
|
||||
: 'Share folder';
|
||||
if (headerSpan) {
|
||||
headerSpan.textContent = titleText;
|
||||
} else {
|
||||
dialogHeader.textContent = titleText;
|
||||
}
|
||||
const expirationSpan = document.createElement('span');
|
||||
expirationSpan.className = 'share-expiration';
|
||||
expirationSpan.textContent = expiresText;
|
||||
infoDiv.appendChild(expirationSpan);
|
||||
shareEl.appendChild(infoDiv);
|
||||
}
|
||||
|
||||
// Share actions
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'share-actions';
|
||||
const itemName = document.getElementById('shared-item-name');
|
||||
if (itemName) itemName.textContent = item.name;
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'btn btn-small copy-link-btn';
|
||||
copyBtn.dataset.shareUrl = share.url;
|
||||
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
// Reset form
|
||||
const pwField = document.getElementById('share-password');
|
||||
const expField = document.getElementById('share-expiration');
|
||||
if (pwField) pwField.value = '';
|
||||
if (expField) expField.value = '';
|
||||
const permRead = document.getElementById('share-permission-read');
|
||||
const permWrite = document.getElementById('share-permission-write');
|
||||
const permReshare = document.getElementById('share-permission-reshare');
|
||||
if (permRead) permRead.checked = true;
|
||||
if (permWrite) permWrite.checked = false;
|
||||
if (permReshare) permReshare.checked = false;
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn btn-small btn-danger delete-link-btn';
|
||||
deleteBtn.dataset.shareId = share.id;
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
// Store the current item and type for use when creating the share
|
||||
window.app.shareDialogItem = item;
|
||||
window.app.shareDialogItemType = itemType;
|
||||
|
||||
shareEl.appendChild(actionsDiv);
|
||||
// Check if item already has shares (async API call)
|
||||
const existingShares = await window.fileSharing.getSharedLinksForItem(item.id, itemType);
|
||||
const existingSharesContainer = document.getElementById('existing-shares-container');
|
||||
|
||||
existingSharesContainer.appendChild(shareEl);
|
||||
});
|
||||
// Clear existing shares container
|
||||
existingSharesContainer.innerHTML = '';
|
||||
|
||||
// Add event listeners for copy and delete buttons
|
||||
document.querySelectorAll('.copy-link-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const url = btn.getAttribute('data-share-url');
|
||||
window.fileSharing.copyLinkToClipboard(url);
|
||||
if (existingShares.length > 0) {
|
||||
document.getElementById('existing-shares-section').classList.remove('hidden');
|
||||
|
||||
// Create elements for each existing share
|
||||
existingShares.forEach((share) => {
|
||||
const shareEl = document.createElement('div');
|
||||
shareEl.className = 'existing-share-item';
|
||||
|
||||
const expiresText = share.expires_at ? `Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` : 'No expiration';
|
||||
|
||||
// Share URL
|
||||
const urlDiv = document.createElement('div');
|
||||
urlDiv.className = 'share-url';
|
||||
urlDiv.textContent = share.url;
|
||||
shareEl.appendChild(urlDiv);
|
||||
|
||||
// Share info
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'share-info';
|
||||
if (share.has_password) {
|
||||
const protectedSpan = document.createElement('span');
|
||||
protectedSpan.className = 'share-protected';
|
||||
protectedSpan.innerHTML = '<i class="fas fa-lock"></i> Password protected';
|
||||
infoDiv.appendChild(protectedSpan);
|
||||
}
|
||||
const expirationSpan = document.createElement('span');
|
||||
expirationSpan.className = 'share-expiration';
|
||||
expirationSpan.textContent = expiresText;
|
||||
infoDiv.appendChild(expirationSpan);
|
||||
shareEl.appendChild(infoDiv);
|
||||
|
||||
// Share actions
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'share-actions';
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'btn btn-small copy-link-btn';
|
||||
copyBtn.dataset.shareUrl = share.url;
|
||||
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn btn-small btn-danger delete-link-btn';
|
||||
deleteBtn.dataset.shareId = share.id;
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
|
||||
shareEl.appendChild(actionsDiv);
|
||||
|
||||
existingSharesContainer.appendChild(shareEl);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-link-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const shareId = btn.getAttribute('data-share-id');
|
||||
|
||||
showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
|
||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||
}).then(async (confirmed) => {
|
||||
if (confirmed) {
|
||||
await window.fileSharing.removeSharedLink(shareId);
|
||||
btn.closest('.existing-share-item').remove();
|
||||
if (existingSharesContainer.children.length === 0) {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
// Add event listeners for copy and delete buttons
|
||||
document.querySelectorAll('.copy-link-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const url = btn.getAttribute('data-share-url');
|
||||
window.fileSharing.copyLinkToClipboard(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Hide new-share section from previous use
|
||||
const newShareSection = document.getElementById('new-share-section');
|
||||
if (newShareSection) newShareSection.classList.add('hidden');
|
||||
document.querySelectorAll('.delete-link-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const shareId = btn.getAttribute('data-share-id');
|
||||
|
||||
// Show dialog
|
||||
shareDialog.style.display = 'flex';
|
||||
console.log('Share dialog opened for', itemType, item.name);
|
||||
showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
|
||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||
}).then(async (confirmed) => {
|
||||
if (confirmed) {
|
||||
await window.fileSharing.removeSharedLink(shareId);
|
||||
btn.closest('.existing-share-item').remove();
|
||||
if (existingSharesContainer.children.length === 0) {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Hide new-share section from previous use
|
||||
const newShareSection = document.getElementById('new-share-section');
|
||||
if (newShareSection) newShareSection.classList.add('hidden');
|
||||
|
||||
// Show dialog
|
||||
shareDialog.style.display = 'flex';
|
||||
console.log('Share dialog opened for', itemType, item.name);
|
||||
} catch (error) {
|
||||
console.error('Error opening share dialog:', error);
|
||||
window.ui.showNotification('Error', 'Could not open share dialog');
|
||||
@@ -961,7 +948,10 @@ const contextMenus = {
|
||||
};
|
||||
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...getCsrfHeaders()
|
||||
};
|
||||
|
||||
const response = await fetch('/api/shares', {
|
||||
method: 'POST',
|
||||
@@ -990,7 +980,6 @@ const contextMenus = {
|
||||
window.i18n ? window.i18n.t('notifications.link_created') : 'Link created',
|
||||
window.i18n ? window.i18n.t('notifications.share_success') : 'Shared link created successfully'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating shared link:', error);
|
||||
window.ui.showNotification('Error', error.message || 'Could not create shared link');
|
||||
|
||||
@@ -14,18 +14,15 @@ function getAuthHeaders() {
|
||||
|
||||
// File Operations Module
|
||||
const fileOps = {
|
||||
|
||||
// ========================================================================
|
||||
// Upload progress — notification bell integration
|
||||
// ========================================================================
|
||||
_currentBatchId: null,
|
||||
_isUploading: false, // Guard against concurrent upload calls
|
||||
_isUploading: false, // Guard against concurrent upload calls
|
||||
|
||||
/** Start a new upload batch in the notification bell */
|
||||
_initUploadToast(totalFiles, folderName) {
|
||||
this._currentBatchId = window.notifications
|
||||
? window.notifications.addUploadBatch(totalFiles, folderName)
|
||||
: null;
|
||||
this._currentBatchId = window.notifications ? window.notifications.addUploadBatch(totalFiles, folderName) : null;
|
||||
},
|
||||
|
||||
/** Finalise the batch in the notification bell */
|
||||
@@ -100,7 +97,9 @@ const fileOps = {
|
||||
const resetStallTimer = () => {
|
||||
if (stallTimer) clearTimeout(stallTimer);
|
||||
stallTimer = setTimeout(() => {
|
||||
try { xhr.abort(); } catch (_) {}
|
||||
try {
|
||||
xhr.abort();
|
||||
} catch (_) {}
|
||||
safeUpdateFile(0, 'error');
|
||||
finalize({
|
||||
ok: false,
|
||||
@@ -112,7 +111,9 @@ const fileOps = {
|
||||
|
||||
resetStallTimer();
|
||||
hardTimer = setTimeout(() => {
|
||||
try { xhr.abort(); } catch (_) {}
|
||||
try {
|
||||
xhr.abort();
|
||||
} catch (_) {}
|
||||
safeUpdateFile(0, 'error');
|
||||
finalize({
|
||||
ok: false,
|
||||
@@ -144,7 +145,9 @@ const fileOps = {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
safeUpdateFile(100, 'done');
|
||||
let data = null;
|
||||
try { data = JSON.parse(xhr.responseText); } catch (_) {}
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText);
|
||||
} catch (_) {}
|
||||
finalize({ ok: true, data });
|
||||
} else {
|
||||
safeUpdateFile(0, 'error');
|
||||
@@ -167,12 +170,20 @@ const fileOps = {
|
||||
|
||||
xhr.addEventListener('abort', () => {
|
||||
safeUpdateFile(0, 'error');
|
||||
finalize({ ok: false, isTimeout: true, errorMsg: `Upload aborted/stalled: ${fileName}` });
|
||||
finalize({
|
||||
ok: false,
|
||||
isTimeout: true,
|
||||
errorMsg: `Upload aborted/stalled: ${fileName}`
|
||||
});
|
||||
});
|
||||
|
||||
xhr.addEventListener('timeout', () => {
|
||||
safeUpdateFile(0, 'error');
|
||||
finalize({ ok: false, isTimeout: true, errorMsg: `Timeout after ${Math.round(timeoutMs / 1000)}s` });
|
||||
finalize({
|
||||
ok: false,
|
||||
isTimeout: true,
|
||||
errorMsg: `Timeout after ${Math.round(timeoutMs / 1000)}s`
|
||||
});
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/files/upload');
|
||||
@@ -218,18 +229,20 @@ const fileOps = {
|
||||
|
||||
// Read body as text first (always consume the response fully)
|
||||
let rawText = '';
|
||||
try { rawText = await response.text(); } catch (_) {}
|
||||
try {
|
||||
rawText = await response.text();
|
||||
} catch (_) {}
|
||||
|
||||
let body = null;
|
||||
try { body = JSON.parse(rawText); } catch (_) {}
|
||||
try {
|
||||
body = JSON.parse(rawText);
|
||||
} catch (_) {}
|
||||
|
||||
if (response.ok) {
|
||||
return { ok: true, data: body };
|
||||
}
|
||||
|
||||
const errorMsg = body && typeof body === 'object'
|
||||
? (body.error || null)
|
||||
: (rawText || null);
|
||||
const errorMsg = body && typeof body === 'object' ? body.error || null : rawText || null;
|
||||
const isQuotaError = (body && typeof body === 'object' && body.error_type === 'QuotaExceeded') || response.status === 507;
|
||||
return { ok: false, errorMsg, isQuotaError };
|
||||
} catch (e) {
|
||||
@@ -237,9 +250,7 @@ const fileOps = {
|
||||
return {
|
||||
ok: false,
|
||||
isTimeout,
|
||||
errorMsg: isTimeout
|
||||
? `Timeout after ${Math.round(timeoutMs / 1000)}s`
|
||||
: `Fetch upload failed: ${e?.message || 'network error'}`
|
||||
errorMsg: isTimeout ? `Timeout after ${Math.round(timeoutMs / 1000)}s` : `Fetch upload failed: ${e?.message || 'network error'}`
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -269,8 +280,12 @@ const fileOps = {
|
||||
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
||||
const progressBar = document.querySelector('.progress-fill');
|
||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||
if (progressBar) { progressBar.style.width = '0%'; }
|
||||
if (uploadProgressDiv) {
|
||||
uploadProgressDiv.style.display = 'block';
|
||||
}
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '0%';
|
||||
}
|
||||
|
||||
// Filter out unreadable entries (typically dropped folders/placeholders)
|
||||
const readableFiles = [];
|
||||
@@ -321,7 +336,8 @@ const fileOps = {
|
||||
formData.append('file', file);
|
||||
|
||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
||||
file: file.name, size: file.size
|
||||
file: file.name,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
// Scale stall timeout with file size:
|
||||
@@ -334,7 +350,7 @@ const fileOps = {
|
||||
|
||||
// Legacy dropzone bar
|
||||
if (progressBar) {
|
||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||
progressBar.style.width = (uploadedCount / totalFiles) * 100 + '%';
|
||||
}
|
||||
// Notify bell of per-file completion
|
||||
if (window.notifications && batchId) {
|
||||
@@ -378,7 +394,9 @@ const fileOps = {
|
||||
|
||||
// Refresh storage usage display
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try { await window.refreshUserData(); } catch (_) {}
|
||||
try {
|
||||
await window.refreshUserData();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -425,8 +443,12 @@ const fileOps = {
|
||||
|
||||
const progressBar = document.querySelector('.progress-fill');
|
||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||
if (progressBar) { progressBar.style.width = '0%'; }
|
||||
if (uploadProgressDiv) {
|
||||
uploadProgressDiv.style.display = 'block';
|
||||
}
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '0%';
|
||||
}
|
||||
|
||||
try {
|
||||
// Filter unreadable entries
|
||||
@@ -460,9 +482,7 @@ const fileOps = {
|
||||
}
|
||||
}
|
||||
|
||||
const sortedPaths = [...folderPaths].sort((a, b) =>
|
||||
a.split('/').length - b.split('/').length
|
||||
);
|
||||
const sortedPaths = [...folderPaths].sort((a, b) => a.split('/').length - b.split('/').length);
|
||||
|
||||
// Create folders first
|
||||
for (const folderPath of sortedPaths) {
|
||||
@@ -498,16 +518,23 @@ const fileOps = {
|
||||
}
|
||||
|
||||
// Detect root folder(s) from entry paths
|
||||
const rootFolderNames = [...new Set(validEntries.map((entry) => {
|
||||
const rel = entry.relativePath || entry.file.name;
|
||||
return rel.split('/')[0] || '';
|
||||
}).filter(Boolean))];
|
||||
const rootFolderNames = [
|
||||
...new Set(
|
||||
validEntries
|
||||
.map((entry) => {
|
||||
const rel = entry.relativePath || entry.file.name;
|
||||
return rel.split('/')[0] || '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
];
|
||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
||||
const rootFolderLabel = rootFolderNames.length <= 1
|
||||
? (rootFolderNames[0] || '')
|
||||
: (locale.startsWith('es')
|
||||
? `${rootFolderNames.length} carpetas`
|
||||
: `${rootFolderNames.length} folders`);
|
||||
const rootFolderLabel =
|
||||
rootFolderNames.length <= 1
|
||||
? rootFolderNames[0] || ''
|
||||
: locale.startsWith('es')
|
||||
? `${rootFolderNames.length} carpetas`
|
||||
: `${rootFolderNames.length} folders`;
|
||||
|
||||
// Upload files — pass folder name for folder-level progress display
|
||||
this._initUploadToast(totalFiles, rootFolderLabel);
|
||||
@@ -522,8 +549,8 @@ const fileOps = {
|
||||
// so all files reaching fetch() are regular. Keep-alive
|
||||
// reuses TCP connections across workers for speed.
|
||||
const CONCURRENCY = 10;
|
||||
const TIMEOUT_MS = 10000; // 10s for normal files
|
||||
const TIMEOUT_MS_ZERO = 3000; // 3s for 0-byte files
|
||||
const TIMEOUT_MS = 10000; // 10s for normal files
|
||||
const TIMEOUT_MS_ZERO = 3000; // 3s for 0-byte files
|
||||
|
||||
const uploadOneFile = async (idx) => {
|
||||
if (quotaStop) return;
|
||||
@@ -542,13 +569,12 @@ const fileOps = {
|
||||
// but block on open(). Pre-read only 0-byte files into
|
||||
// memory; files with size>0 are always regular files and
|
||||
// go straight to FormData (zero extra memory copy).
|
||||
let uploadFile = file; // default: use original File
|
||||
let uploadFile = file; // default: use original File
|
||||
if (file.size === 0) {
|
||||
try {
|
||||
const buf = await Promise.race([
|
||||
file.arrayBuffer(),
|
||||
new Promise((_, rej) =>
|
||||
setTimeout(() => rej(new Error('read-timeout')), 2000))
|
||||
new Promise((_, rej) => setTimeout(() => rej(new Error('read-timeout')), 2000))
|
||||
]);
|
||||
uploadFile = new Blob([buf], {
|
||||
type: file.type || 'application/octet-stream'
|
||||
@@ -558,7 +584,9 @@ const fileOps = {
|
||||
uploadedCount++;
|
||||
successCount++;
|
||||
if (window.notifications && batchId) {
|
||||
try { window.notifications.fileCompleted(batchId, true); } catch (_) {}
|
||||
try {
|
||||
window.notifications.fileCompleted(batchId, true);
|
||||
} catch (_) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -585,10 +613,12 @@ const fileOps = {
|
||||
uploadedCount++;
|
||||
|
||||
if (window.notifications && batchId) {
|
||||
try { window.notifications.fileCompleted(batchId, result.ok); } catch (_) {}
|
||||
try {
|
||||
window.notifications.fileCompleted(batchId, result.ok);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (progressBar && uploadedCount % 10 === 0) {
|
||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||
progressBar.style.width = (uploadedCount / totalFiles) * 100 + '%';
|
||||
}
|
||||
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
||||
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
||||
@@ -627,7 +657,9 @@ const fileOps = {
|
||||
this._finishUploadToast(successCount, totalFiles);
|
||||
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try { await window.refreshUserData(); } catch (_) {}
|
||||
try {
|
||||
await window.refreshUserData();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -651,7 +683,7 @@ const fileOps = {
|
||||
async createFolder(name) {
|
||||
try {
|
||||
console.log('Creating folder with name:', name);
|
||||
|
||||
|
||||
// Send the actual request to the backend to create the folder
|
||||
const response = await fetch('/api/folders', {
|
||||
method: 'POST',
|
||||
@@ -670,11 +702,11 @@ const fileOps = {
|
||||
// Get the created folder from the backend
|
||||
const folder = await response.json();
|
||||
console.log('Folder created successfully:', folder);
|
||||
|
||||
|
||||
// Optimistic UI: add folder card directly from server response
|
||||
// — no reload needed since the backend already confirmed creation.
|
||||
window.ui.addFolderToView(folder);
|
||||
|
||||
|
||||
window.ui.showNotification('Folder created', `"${name}" created successfully`);
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
@@ -702,7 +734,7 @@ const fileOps = {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder_id: targetFolderId === "" ? null : targetFolderId
|
||||
folder_id: targetFolderId === '' ? null : targetFolderId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -744,7 +776,7 @@ const fileOps = {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parent_id: targetFolderId === "" ? null : targetFolderId
|
||||
parent_id: targetFolderId === '' ? null : targetFolderId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -785,9 +817,9 @@ const fileOps = {
|
||||
* @returns {Promise<BatchResult>} - Success status
|
||||
*/
|
||||
async batchMove(fileIds, folderIds, targetFolderId) {
|
||||
|
||||
// TODO ensure not moving a folder into itself
|
||||
let success = 0, errors = 0;
|
||||
let success = 0,
|
||||
errors = 0;
|
||||
|
||||
try {
|
||||
// Batch move files in a single request
|
||||
@@ -795,7 +827,10 @@ const fileOps = {
|
||||
const res = await fetch('/api/batch/files/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId })
|
||||
body: JSON.stringify({
|
||||
file_ids: fileIds,
|
||||
target_folder_id: targetFolderId
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
@@ -807,14 +842,16 @@ const fileOps = {
|
||||
const res = await fetch('/api/batch/folders/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_ids: folderIds, target_folder_id: targetFolderId })
|
||||
body: JSON.stringify({
|
||||
folder_ids: folderIds,
|
||||
target_folder_id: targetFolderId
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.error('Batch move error:', err);
|
||||
errors++;
|
||||
}
|
||||
@@ -841,7 +878,7 @@ const fileOps = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_ids: [fileId],
|
||||
target_folder_id: targetFolderId === "" ? null : targetFolderId
|
||||
target_folder_id: targetFolderId === '' ? null : targetFolderId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -890,17 +927,20 @@ const fileOps = {
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async batchCopy(fileIds, folderIds, targetFolderId) {
|
||||
|
||||
// FIXME ensure not moving a folder into itself
|
||||
|
||||
let success = 0, errors = 0;
|
||||
let success = 0,
|
||||
errors = 0;
|
||||
try {
|
||||
// Batch copy files
|
||||
if (fileIds.length > 0) {
|
||||
const res = await fetch('/api/batch/files/copy', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId })
|
||||
body: JSON.stringify({
|
||||
file_ids: fileIds,
|
||||
target_folder_id: targetFolderId
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
@@ -1027,11 +1067,13 @@ const fileOps = {
|
||||
async deleteFile(fileId, fileName) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_delete_file', { name: fileName })
|
||||
: `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
|
||||
try {
|
||||
// Use the trash API endpoint
|
||||
const response = await fetch(`/api/trash/files/${fileId}`, {
|
||||
@@ -1049,7 +1091,7 @@ const fileOps = {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
|
||||
if (fallbackResponse.ok) {
|
||||
window.loadFiles();
|
||||
window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
||||
@@ -1075,11 +1117,13 @@ const fileOps = {
|
||||
async deleteFolder(folderId, folderName) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
||||
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
|
||||
try {
|
||||
// Use the trash API endpoint
|
||||
const response = await fetch(`/api/trash/folders/${folderId}`, {
|
||||
@@ -1102,7 +1146,7 @@ const fileOps = {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
|
||||
if (fallbackResponse.ok) {
|
||||
// If we're inside the folder we just deleted, go back up
|
||||
if (window.app.currentPath === folderId) {
|
||||
@@ -1123,7 +1167,7 @@ const fileOps = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get trash items
|
||||
* @returns {Promise<Array>} - List of trash items
|
||||
@@ -1133,7 +1177,7 @@ const fileOps = {
|
||||
const response = await fetch('/api/trash', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
@@ -1145,7 +1189,7 @@ const fileOps = {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Restore an item from trash
|
||||
* @param {string} trashId - Trash item ID
|
||||
@@ -1161,7 +1205,7 @@ const fileOps = {
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Item restored', 'Item restored successfully');
|
||||
return true;
|
||||
@@ -1175,7 +1219,7 @@ const fileOps = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Permanently delete a trash item
|
||||
* @param {string} trashId - Trash item ID
|
||||
@@ -1184,17 +1228,19 @@ const fileOps = {
|
||||
async deletePermanently(trashId) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
||||
message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_permanent_delete_msg')
|
||||
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/trash/${trashId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Item deleted', 'Item permanently deleted');
|
||||
return true;
|
||||
@@ -1208,7 +1254,7 @@ const fileOps = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Empty the trash
|
||||
* @returns {Promise<boolean>} - Operation success
|
||||
@@ -1216,17 +1262,19 @@ const fileOps = {
|
||||
async emptyTrash() {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
||||
message: window.i18n ? window.i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash',
|
||||
message: window.i18n
|
||||
? window.i18n.t('trash.empty_confirm')
|
||||
: 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/trash/empty', {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
||||
return true;
|
||||
@@ -1240,7 +1288,7 @@ const fileOps = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Download a file
|
||||
* @param {string} fileId - File ID
|
||||
@@ -1269,7 +1317,7 @@ const fileOps = {
|
||||
window.ui.showNotification('Error', 'Error downloading the file');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Download a folder as ZIP
|
||||
* @param {string} folderId - Folder ID
|
||||
@@ -1279,7 +1327,7 @@ const fileOps = {
|
||||
try {
|
||||
// Show notification to user
|
||||
window.ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||
|
||||
|
||||
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
@@ -4,29 +4,29 @@
|
||||
*/
|
||||
|
||||
class InlineViewer {
|
||||
constructor() {
|
||||
this.setupViewer();
|
||||
this.currentFile = null;
|
||||
}
|
||||
|
||||
setupViewer() {
|
||||
// Create the viewer modal if it doesn't exist
|
||||
if (document.getElementById('inline-viewer-modal')) {
|
||||
return;
|
||||
constructor() {
|
||||
this.setupViewer();
|
||||
this.currentFile = null;
|
||||
}
|
||||
|
||||
// Verify document.body exists
|
||||
if (!document.body) {
|
||||
console.warn('Document body not available yet for inline viewer, will retry later');
|
||||
setTimeout(() => this.setupViewer(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal container
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'inline-viewer-modal';
|
||||
modal.className = 'inline-viewer-modal';
|
||||
modal.innerHTML = `
|
||||
|
||||
setupViewer() {
|
||||
// Create the viewer modal if it doesn't exist
|
||||
if (document.getElementById('inline-viewer-modal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify document.body exists
|
||||
if (!document.body) {
|
||||
console.warn('Document body not available yet for inline viewer, will retry later');
|
||||
setTimeout(() => this.setupViewer(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal container
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'inline-viewer-modal';
|
||||
modal.className = 'inline-viewer-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="inline-viewer-content">
|
||||
<div class="inline-viewer-header">
|
||||
<div class="inline-viewer-title">File Viewer</div>
|
||||
@@ -43,224 +43,222 @@ class InlineViewer {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to document
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners
|
||||
modal.querySelector('.inline-viewer-close').addEventListener('click', () => {
|
||||
this.closeViewer();
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-download').addEventListener('click', () => {
|
||||
if (this.currentFile) {
|
||||
this.downloadFile(this.currentFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Add zoom controls for images
|
||||
modal.querySelector('.inline-viewer-zoom-in').addEventListener('click', () => {
|
||||
this.zoomImage(1.2);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-out').addEventListener('click', () => {
|
||||
this.zoomImage(0.8);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-reset').addEventListener('click', () => {
|
||||
this.resetZoom();
|
||||
});
|
||||
|
||||
// Close on ESC key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Inline viewer initialized');
|
||||
}
|
||||
|
||||
async openFile(file) {
|
||||
console.log('Opening file:', file);
|
||||
|
||||
// WOPI editor intercept: open Office documents in the WOPI editor
|
||||
// But NOT image files - those should be previewed in the inline viewer
|
||||
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||
const imageExts = ['jpg','jpeg','png','gif','svg','webp','bmp','ico','heic','heif','avif','tiff'];
|
||||
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
|
||||
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
|
||||
try {
|
||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('WOPI editor failed, falling back to inline viewer:', e);
|
||||
// Add to document
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners
|
||||
modal.querySelector('.inline-viewer-close').addEventListener('click', () => {
|
||||
this.closeViewer();
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-download').addEventListener('click', () => {
|
||||
if (this.currentFile) {
|
||||
this.downloadFile(this.currentFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Add zoom controls for images
|
||||
modal.querySelector('.inline-viewer-zoom-in').addEventListener('click', () => {
|
||||
this.zoomImage(1.2);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-out').addEventListener('click', () => {
|
||||
this.zoomImage(0.8);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-reset').addEventListener('click', () => {
|
||||
this.resetZoom();
|
||||
});
|
||||
|
||||
// Close on ESC key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Inline viewer initialized');
|
||||
}
|
||||
|
||||
async openFile(file) {
|
||||
console.log('Opening file:', file);
|
||||
|
||||
// WOPI editor intercept: open Office documents in the WOPI editor
|
||||
// But NOT image files - those should be previewed in the inline viewer
|
||||
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
||||
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
|
||||
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||
try {
|
||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('WOPI editor failed, falling back to inline viewer:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.currentFile = file;
|
||||
this.currentFile = file;
|
||||
|
||||
// Get container
|
||||
const modal = document.getElementById('inline-viewer-modal');
|
||||
const container = modal.querySelector('.inline-viewer-container');
|
||||
const title = modal.querySelector('.inline-viewer-title');
|
||||
// Get container
|
||||
const modal = document.getElementById('inline-viewer-modal');
|
||||
const container = modal.querySelector('.inline-viewer-container');
|
||||
const title = modal.querySelector('.inline-viewer-title');
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Set title
|
||||
title.textContent = file.name;
|
||||
// Set title
|
||||
title.textContent = file.name;
|
||||
|
||||
// Set controls visibility
|
||||
const controls = modal.querySelector('.inline-viewer-controls');
|
||||
// Set controls visibility
|
||||
const controls = modal.querySelector('.inline-viewer-controls');
|
||||
|
||||
// Show viewer based on file type
|
||||
if (isImage) {
|
||||
// Show zoom controls
|
||||
controls.style.display = 'flex';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create image viewer using a blob URL
|
||||
this.createBlobUrlViewer(file, 'image', container, loader);
|
||||
}
|
||||
else if (file.mime_type && file.mime_type === 'application/pdf') {
|
||||
// Hide zoom controls for PDFs
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create PDF viewer using object tag with blob URL
|
||||
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
||||
}
|
||||
else if (file.mime_type && this.isTextViewable(file.mime_type)) {
|
||||
// Hide zoom controls for text files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create text viewer using authenticated fetch
|
||||
this.createTextViewer(file, container, loader);
|
||||
}
|
||||
else if (file.mime_type && file.mime_type.startsWith('audio/')) {
|
||||
// Hide zoom controls for audio
|
||||
controls.style.display = 'none';
|
||||
// Show viewer based on file type
|
||||
if (isImage) {
|
||||
// Show zoom controls
|
||||
controls.style.display = 'flex';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create audio player
|
||||
this.createMediaViewer(file, 'audio', container, loader);
|
||||
}
|
||||
else if (file.mime_type && file.mime_type.startsWith('video/')) {
|
||||
// Hide zoom controls for video
|
||||
controls.style.display = 'none';
|
||||
// Create image viewer using a blob URL
|
||||
this.createBlobUrlViewer(file, 'image', container, loader);
|
||||
} else if (file.mime_type && file.mime_type === 'application/pdf') {
|
||||
// Hide zoom controls for PDFs
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create video player
|
||||
this.createMediaViewer(file, 'video', container, loader);
|
||||
}
|
||||
else {
|
||||
// Hide zoom controls for unsupported files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show unsupported file message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
// Create PDF viewer using object tag with blob URL
|
||||
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
||||
} else if (file.mime_type && this.isTextViewable(file.mime_type)) {
|
||||
// Hide zoom controls for text files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create text viewer using authenticated fetch
|
||||
this.createTextViewer(file, container, loader);
|
||||
} else if (file.mime_type && file.mime_type.startsWith('audio/')) {
|
||||
// Hide zoom controls for audio
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create audio player
|
||||
this.createMediaViewer(file, 'audio', container, loader);
|
||||
} else if (file.mime_type && file.mime_type.startsWith('video/')) {
|
||||
// Hide zoom controls for video
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create video player
|
||||
this.createMediaViewer(file, 'video', container, loader);
|
||||
} else {
|
||||
// Hide zoom controls for unsupported files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show unsupported file message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-file"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>This file type cannot be previewed.</p>
|
||||
<p>Click "Download" to get the file.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
// Check if a MIME type is text-viewable — delegates to window.isTextViewable
|
||||
isTextViewable(mimeType) {
|
||||
return window.isTextViewable ? window.isTextViewable(mimeType) : false;
|
||||
}
|
||||
|
||||
// Creates a text viewer using authenticated fetch
|
||||
async createTextViewer(file, container, loader) {
|
||||
try {
|
||||
console.log('Creating text viewer for:', file.name);
|
||||
|
||||
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
// Create text viewer element
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'inline-viewer-text-content';
|
||||
pre.textContent = text;
|
||||
container.appendChild(pre);
|
||||
|
||||
console.log('Text viewer created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating text viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a viewer using a Blob URL to avoid content-disposition header
|
||||
async createBlobUrlViewer(file, type, container, loader) {
|
||||
try {
|
||||
console.log('Creating blob URL viewer for:', file.name, 'type:', type);
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
// Update loader to show progress bar for large files
|
||||
let progressBar = null;
|
||||
let progressText = null;
|
||||
if (loader && file.size > 10 * 1024 * 1024) { // Show progress for files > 10MB
|
||||
loader.innerHTML = `
|
||||
// Show modal
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
// Check if a MIME type is text-viewable — delegates to window.isTextViewable
|
||||
isTextViewable(mimeType) {
|
||||
return window.isTextViewable ? window.isTextViewable(mimeType) : false;
|
||||
}
|
||||
|
||||
// Creates a text viewer using authenticated fetch
|
||||
async createTextViewer(file, container, loader) {
|
||||
try {
|
||||
console.log('Creating text viewer for:', file.name);
|
||||
|
||||
const response = await fetch(`/api/files/${file.id}?inline=true`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
// Create text viewer element
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'inline-viewer-text-content';
|
||||
pre.textContent = text;
|
||||
container.appendChild(pre);
|
||||
|
||||
console.log('Text viewer created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating text viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a viewer using a Blob URL to avoid content-disposition header
|
||||
async createBlobUrlViewer(file, type, container, loader) {
|
||||
try {
|
||||
console.log('Creating blob URL viewer for:', file.name, 'type:', type);
|
||||
|
||||
// Update loader to show progress bar for large files
|
||||
let progressBar = null;
|
||||
let progressText = null;
|
||||
if (loader && file.size > 10 * 1024 * 1024) {
|
||||
// Show progress for files > 10MB
|
||||
loader.innerHTML = `
|
||||
<div class="inline-viewer-progress">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<div class="inline-viewer-progress-bar">
|
||||
@@ -269,226 +267,225 @@ class InlineViewer {
|
||||
<div class="inline-viewer-progress-text">0%</div>
|
||||
</div>
|
||||
`;
|
||||
progressBar = loader.querySelector('.inline-viewer-progress-fill');
|
||||
progressText = loader.querySelector('.inline-viewer-progress-text');
|
||||
}
|
||||
progressBar = loader.querySelector('.inline-viewer-progress-fill');
|
||||
progressText = loader.querySelector('.inline-viewer-progress-text');
|
||||
}
|
||||
|
||||
// Use XMLHttpRequest instead of fetch to get better control over the response
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `/api/files/${file.id}?inline=true`, true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.withCredentials = true;
|
||||
// Use XMLHttpRequest instead of fetch to get better control over the response
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `/api/files/${file.id}?inline=true`, true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.withCredentials = true;
|
||||
|
||||
// Track download progress - use double precision for large file support (>2GB)
|
||||
xhr.onprogress = (e) => {
|
||||
if (e.lengthComputable && progressBar && progressText) {
|
||||
// Use direct division to avoid 32-bit overflow issues
|
||||
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
||||
const progress = e.loaded / e.total;
|
||||
const pct = Math.round(progress * 100);
|
||||
progressBar.style.width = pct + '%';
|
||||
progressText.textContent = pct + '%';
|
||||
// Track download progress - use double precision for large file support (>2GB)
|
||||
xhr.onprogress = (e) => {
|
||||
if (e.lengthComputable && progressBar && progressText) {
|
||||
// Use direct division to avoid 32-bit overflow issues
|
||||
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
||||
const progress = e.loaded / e.total;
|
||||
const pct = Math.round(progress * 100);
|
||||
progressBar.style.width = pct + '%';
|
||||
progressText.textContent = pct + '%';
|
||||
}
|
||||
};
|
||||
|
||||
// Create a promise to handle the XHR
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
xhr.onload = function () {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
resolve(this.response);
|
||||
} else {
|
||||
reject(new Error(`Error fetching file: ${this.status} ${this.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
reject(new Error('Network error'));
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
// Create blob URL from response
|
||||
const blob = response;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
console.log('Created blob URL:', blobUrl.substring(0, 30) + '...');
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
console.log('Creating image viewer');
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
img.className = 'inline-viewer-image';
|
||||
img.src = blobUrl;
|
||||
img.alt = file.name;
|
||||
container.appendChild(img);
|
||||
|
||||
// Add loading indicator until image loads
|
||||
img.style.opacity = 0;
|
||||
img.onload = () => {
|
||||
console.log('Image loaded successfully');
|
||||
img.style.opacity = 1;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
container.removeChild(img);
|
||||
this.showErrorMessage(container);
|
||||
};
|
||||
} else if (type === 'pdf') {
|
||||
console.log('Creating PDF viewer');
|
||||
|
||||
// Create iframe for PDF (more reliable than object tag)
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'inline-viewer-pdf';
|
||||
iframe.src = blobUrl;
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
container.appendChild(iframe);
|
||||
|
||||
// Monitor iframe for loading issues
|
||||
setTimeout(() => {
|
||||
if (!iframe.contentDocument || iframe.contentDocument.body.innerHTML === '') {
|
||||
console.warn('PDF viewer might be having issues, adding fallback');
|
||||
|
||||
// Add fallback embed
|
||||
const embed = document.createElement('embed');
|
||||
embed.className = 'inline-viewer-pdf-fallback';
|
||||
embed.type = 'application/pdf';
|
||||
embed.src = blobUrl;
|
||||
container.appendChild(embed);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Store blob URL for cleaning up later
|
||||
this.currentBlobUrl = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('Error creating blob URL viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a promise to handle the XHR
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
xhr.onload = function() {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
resolve(this.response);
|
||||
} else {
|
||||
reject(new Error(`Error fetching file: ${this.status} ${this.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject(new Error('Network error'));
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
// Create blob URL from response
|
||||
const blob = response;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
console.log('Created blob URL:', blobUrl.substring(0, 30) + '...');
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
console.log('Creating image viewer');
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
img.className = 'inline-viewer-image';
|
||||
img.src = blobUrl;
|
||||
img.alt = file.name;
|
||||
container.appendChild(img);
|
||||
|
||||
// Add loading indicator until image loads
|
||||
img.style.opacity = 0;
|
||||
img.onload = () => {
|
||||
console.log('Image loaded successfully');
|
||||
img.style.opacity = 1;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
container.removeChild(img);
|
||||
this.showErrorMessage(container);
|
||||
};
|
||||
}
|
||||
else if (type === 'pdf') {
|
||||
console.log('Creating PDF viewer');
|
||||
|
||||
// Create iframe for PDF (more reliable than object tag)
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'inline-viewer-pdf';
|
||||
iframe.src = blobUrl;
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
container.appendChild(iframe);
|
||||
|
||||
// Monitor iframe for loading issues
|
||||
setTimeout(() => {
|
||||
if (!iframe.contentDocument ||
|
||||
iframe.contentDocument.body.innerHTML === '') {
|
||||
console.warn('PDF viewer might be having issues, adding fallback');
|
||||
|
||||
// Add fallback embed
|
||||
const embed = document.createElement('embed');
|
||||
embed.className = 'inline-viewer-pdf-fallback';
|
||||
embed.type = 'application/pdf';
|
||||
embed.src = blobUrl;
|
||||
container.appendChild(embed);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Store blob URL for cleaning up later
|
||||
this.currentBlobUrl = blobUrl;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating blob URL viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an audio or video player using blob URL (authenticated fetch)
|
||||
async createMediaViewer(file, mediaType, container, loader) {
|
||||
try {
|
||||
console.log(`Creating ${mediaType} player for:`, file.name);
|
||||
|
||||
// Fetch file (cookie auto-sent)
|
||||
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
|
||||
// Creates an audio or video player using blob URL (authenticated fetch)
|
||||
async createMediaViewer(file, mediaType, container, loader) {
|
||||
try {
|
||||
console.log(`Creating ${mediaType} player for:`, file.name);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
// Fetch file (cookie auto-sent)
|
||||
const response = await fetch(`/api/files/${file.id}?inline=true`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
// Wrapper with icon + player
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'inline-viewer-audio-wrapper';
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'inline-viewer-audio-icon';
|
||||
icon.innerHTML = '<i class="fas fa-music"></i>';
|
||||
wrapper.appendChild(icon);
|
||||
if (mediaType === 'audio') {
|
||||
// Wrapper with icon + player
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'inline-viewer-audio-wrapper';
|
||||
|
||||
const nameEl = document.createElement('div');
|
||||
nameEl.className = 'inline-viewer-audio-name';
|
||||
nameEl.textContent = file.name;
|
||||
wrapper.appendChild(nameEl);
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'inline-viewer-audio-icon';
|
||||
icon.innerHTML = '<i class="fas fa-music"></i>';
|
||||
wrapper.appendChild(icon);
|
||||
|
||||
const audio = document.createElement('audio');
|
||||
audio.className = 'inline-viewer-audio';
|
||||
audio.controls = true;
|
||||
audio.preload = 'metadata';
|
||||
audio.src = blobUrl;
|
||||
wrapper.appendChild(audio);
|
||||
const nameEl = document.createElement('div');
|
||||
nameEl.className = 'inline-viewer-audio-name';
|
||||
nameEl.textContent = file.name;
|
||||
wrapper.appendChild(nameEl);
|
||||
|
||||
// Fallback message for unsupported codecs
|
||||
audio.addEventListener('error', () => {
|
||||
console.warn('Audio playback error — codec may not be supported');
|
||||
wrapper.innerHTML = '';
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'inline-viewer-message';
|
||||
msg.innerHTML = `
|
||||
const audio = document.createElement('audio');
|
||||
audio.className = 'inline-viewer-audio';
|
||||
audio.controls = true;
|
||||
audio.preload = 'metadata';
|
||||
audio.src = blobUrl;
|
||||
wrapper.appendChild(audio);
|
||||
|
||||
// Fallback message for unsupported codecs
|
||||
audio.addEventListener('error', () => {
|
||||
console.warn('Audio playback error — codec may not be supported');
|
||||
wrapper.innerHTML = '';
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'inline-viewer-message';
|
||||
msg.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-exclamation-circle"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Your browser cannot play this audio format.</p>
|
||||
<p>Click "Download" to save the file.</p>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(msg);
|
||||
});
|
||||
wrapper.appendChild(msg);
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
} else {
|
||||
const video = document.createElement('video');
|
||||
video.className = 'inline-viewer-video';
|
||||
video.controls = true;
|
||||
video.preload = 'metadata';
|
||||
video.src = blobUrl;
|
||||
video.setAttribute('playsinline', 'true');
|
||||
container.appendChild(wrapper);
|
||||
} else {
|
||||
const video = document.createElement('video');
|
||||
video.className = 'inline-viewer-video';
|
||||
video.controls = true;
|
||||
video.preload = 'metadata';
|
||||
video.src = blobUrl;
|
||||
video.setAttribute('playsinline', 'true');
|
||||
|
||||
// Fallback message for unsupported codecs
|
||||
video.addEventListener('error', () => {
|
||||
console.warn('Video playback error — codec may not be supported');
|
||||
if (video.parentNode) {
|
||||
video.parentNode.removeChild(video);
|
||||
}
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'inline-viewer-message';
|
||||
msg.innerHTML = `
|
||||
// Fallback message for unsupported codecs
|
||||
video.addEventListener('error', () => {
|
||||
console.warn('Video playback error — codec may not be supported');
|
||||
if (video.parentNode) {
|
||||
video.parentNode.removeChild(video);
|
||||
}
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'inline-viewer-message';
|
||||
msg.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-exclamation-circle"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Your browser cannot play this video format.</p>
|
||||
<p>Click "Download" to save the file.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(msg);
|
||||
});
|
||||
container.appendChild(msg);
|
||||
});
|
||||
|
||||
container.appendChild(video);
|
||||
}
|
||||
container.appendChild(video);
|
||||
}
|
||||
|
||||
// Store blob URL for cleanup on close
|
||||
this.currentBlobUrl = blobUrl;
|
||||
} catch (error) {
|
||||
console.error(`Error creating ${mediaType} viewer:`, error);
|
||||
// Store blob URL for cleanup on close
|
||||
this.currentBlobUrl = blobUrl;
|
||||
} catch (error) {
|
||||
console.error(`Error creating ${mediaType} viewer:`, error);
|
||||
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to show error message
|
||||
showErrorMessage(container) {
|
||||
// Show error message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
// Helper to show error message
|
||||
showErrorMessage(container) {
|
||||
// Show error message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Error loading the file.</p>
|
||||
@@ -578,19 +575,19 @@ class InlineViewer {
|
||||
|
||||
// Initialize viewer when document is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if it's already initialized
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Initializing inline viewer on DOMContentLoaded');
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}
|
||||
// Check if it's already initialized
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Initializing inline viewer on DOMContentLoaded');
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback initialization for cases where DOMContentLoaded already fired
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Fallback initialization for inline viewer');
|
||||
setTimeout(() => {
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Fallback initialization for inline viewer');
|
||||
setTimeout(() => {
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,13 @@ const search = {
|
||||
} catch (error) {
|
||||
console.error('Error performing search:', error);
|
||||
window.ui.showNotification('Error', 'Error performing search');
|
||||
return { files: [], folders: [], total_count: 0, query_time_ms: 0, sort_by: 'relevance' };
|
||||
return {
|
||||
files: [],
|
||||
folders: [],
|
||||
total_count: 0,
|
||||
query_time_ms: 0,
|
||||
sort_by: 'relevance'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -107,10 +113,8 @@ const search = {
|
||||
const filesList = document.getElementById('files-list');
|
||||
|
||||
// Search results header with query time and sort controls
|
||||
const totalCount = results.total_count || (results.files.length + results.folders.length);
|
||||
const queryTimeText = results.query_time_ms !== undefined
|
||||
? ` <span class="search-time">(${results.query_time_ms}ms)</span>`
|
||||
: '';
|
||||
const totalCount = results.total_count || results.files.length + results.folders.length;
|
||||
const queryTimeText = results.query_time_ms !== undefined ? ` <span class="search-time">(${results.query_time_ms}ms)</span>` : '';
|
||||
|
||||
const searchHeader = document.createElement('div');
|
||||
searchHeader.className = 'search-results-header';
|
||||
@@ -169,12 +173,12 @@ const search = {
|
||||
}
|
||||
|
||||
// Render folders (server-provided enriched data)
|
||||
results.folders.forEach(folder => {
|
||||
results.folders.forEach((folder) => {
|
||||
window.ui.addFolderToView(folder);
|
||||
});
|
||||
|
||||
// Render files (server-provided enriched data)
|
||||
results.files.forEach(file => {
|
||||
results.files.forEach((file) => {
|
||||
window.ui.addFileToView(file);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -39,8 +39,7 @@ class WopiEditor {
|
||||
action = action || 'edit';
|
||||
try {
|
||||
var data = await this._getEditorUrlWithFallback(fileId, fileName, action);
|
||||
var hostUrl = '/wopi/edit/' + encodeURIComponent(fileId)
|
||||
+ '?access_token=' + encodeURIComponent(data.access_token);
|
||||
var hostUrl = '/wopi/edit/' + encodeURIComponent(fileId) + '?access_token=' + encodeURIComponent(data.access_token);
|
||||
window.open(hostUrl, '_blank');
|
||||
} catch (error) {
|
||||
console.error('Failed to open WOPI editor in tab:', error);
|
||||
@@ -54,10 +53,9 @@ class WopiEditor {
|
||||
* Fetch editor URL and WOPI token from the backend.
|
||||
*/
|
||||
async _getEditorUrl(fileId, action) {
|
||||
var response = await fetch(
|
||||
'/api/wopi/editor-url?file_id=' + encodeURIComponent(fileId) + '&action=' + encodeURIComponent(action),
|
||||
{ credentials: 'same-origin' }
|
||||
);
|
||||
var response = await fetch('/api/wopi/editor-url?file_id=' + encodeURIComponent(fileId) + '&action=' + encodeURIComponent(action), {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!response.ok) {
|
||||
var text = await response.text();
|
||||
throw new Error('Editor URL request failed: ' + response.status + ' ' + text);
|
||||
@@ -101,7 +99,8 @@ class WopiEditor {
|
||||
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;background:#fff;';
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.style.cssText = 'height:40px;background:#333;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-family:sans-serif;font-size:14px;';
|
||||
header.style.cssText =
|
||||
'height:40px;background:#333;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-family:sans-serif;font-size:14px;';
|
||||
|
||||
var title = document.createElement('span');
|
||||
title.textContent = fileName;
|
||||
@@ -138,7 +137,8 @@ class WopiEditor {
|
||||
// Loading spinner (removed once the editor signals ready)
|
||||
var spinner = document.createElement('div');
|
||||
spinner.id = 'wopi-loading-spinner';
|
||||
spinner.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:#f5f5f5;z-index:1;';
|
||||
spinner.style.cssText =
|
||||
'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:#f5f5f5;z-index:1;';
|
||||
spinner.innerHTML = '<i class="fas fa-spinner fa-spin empty-state-icon spinner"></i>';
|
||||
frameHolder.appendChild(spinner);
|
||||
|
||||
@@ -149,8 +149,7 @@ class WopiEditor {
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
// Fix 9: allow clipboard access for copy/paste inside the editor
|
||||
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
||||
iframe.setAttribute('sandbox',
|
||||
'allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox');
|
||||
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox');
|
||||
frameHolder.appendChild(iframe);
|
||||
|
||||
modal.appendChild(header);
|
||||
@@ -159,13 +158,13 @@ class WopiEditor {
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// ESC key handler
|
||||
this._escHandler = function(e) {
|
||||
this._escHandler = function (e) {
|
||||
if (e.key === 'Escape') this.closeEditor();
|
||||
}.bind(this);
|
||||
document.addEventListener('keydown', this._escHandler);
|
||||
|
||||
// Fix 7: Listen for postMessage from the editor iframe
|
||||
this._messageHandler = function(e) {
|
||||
this._messageHandler = function (e) {
|
||||
var data;
|
||||
try {
|
||||
data = JSON.parse(e.data);
|
||||
@@ -238,11 +237,7 @@ class WopiEditor {
|
||||
// Ignore — fall through to hardcoded list
|
||||
}
|
||||
// Fallback hardcoded list
|
||||
this._supportedExtensions = [
|
||||
'docx', 'doc', 'odt', 'rtf', 'txt',
|
||||
'xlsx', 'xls', 'ods', 'csv',
|
||||
'pptx', 'ppt', 'odp',
|
||||
];
|
||||
this._supportedExtensions = ['docx', 'doc', 'odt', 'rtf', 'txt', 'xlsx', 'xls', 'ods', 'csv', 'pptx', 'ppt', 'odp'];
|
||||
return this._supportedExtensions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ const favorites = {
|
||||
window.ui.updateBreadcrumb('');
|
||||
|
||||
if (this._cache.size === 0) {
|
||||
window.ui.showError( `
|
||||
window.ui.showError(`
|
||||
<i class="fas fa-star empty-state-icon"></i>
|
||||
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
||||
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
||||
|
||||
@@ -202,9 +202,7 @@ const photosView = {
|
||||
for (const file of files) tilesHtml += this._renderTile(file);
|
||||
|
||||
// Does this date-group already exist in the DOM?
|
||||
const existingHeader = this._container.querySelector(
|
||||
`.photos-day-header[data-group="${CSS.escape(label)}"]`
|
||||
);
|
||||
const existingHeader = this._container.querySelector(`.photos-day-header[data-group="${CSS.escape(label)}"]`);
|
||||
|
||||
if (existingHeader) {
|
||||
// Append tiles to existing grid and update count badge
|
||||
@@ -232,8 +230,7 @@ const photosView = {
|
||||
_renderTile(file) {
|
||||
const isVideo = file.mime_type && file.mime_type.startsWith('video/');
|
||||
const selected = this.selected.has(file.id) ? ' selected' : '';
|
||||
const cachedThumb = isVideo && this._videoThumbCache.has(file.id)
|
||||
? this._videoThumbCache.get(file.id) : null;
|
||||
const cachedThumb = isVideo && this._videoThumbCache.has(file.id) ? this._videoThumbCache.get(file.id) : null;
|
||||
const thumbUrl = cachedThumb || `/api/files/${file.id}/thumbnail/preview`;
|
||||
let h = `<div class="photo-tile${selected}" data-id="${this._escAttr(file.id)}" data-mime="${this._escAttr(file.mime_type)}">`;
|
||||
h += `<div class="photo-check"><i class="fas fa-check"></i></div>`;
|
||||
@@ -248,9 +245,12 @@ const photosView = {
|
||||
this._destroyObserver();
|
||||
const sentinel = this._container?.querySelector('.photos-sentinel');
|
||||
if (sentinel && !this.exhausted) {
|
||||
this._observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) this._loadPage();
|
||||
}, { rootMargin: '400px' });
|
||||
this._observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) this._loadPage();
|
||||
},
|
||||
{ rootMargin: '400px' }
|
||||
);
|
||||
this._observer.observe(sentinel);
|
||||
}
|
||||
},
|
||||
@@ -266,9 +266,7 @@ const photosView = {
|
||||
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
||||
_setupVideoThumbnails(startIndex = 0) {
|
||||
const tiles = this._container.querySelectorAll('.photo-tile[data-mime^="video/"]');
|
||||
const newIds = startIndex > 0
|
||||
? new Set(this.items.slice(startIndex).map(f => f.id))
|
||||
: null;
|
||||
const newIds = startIndex > 0 ? new Set(this.items.slice(startIndex).map((f) => f.id)) : null;
|
||||
|
||||
for (const tile of tiles) {
|
||||
const fileId = tile.dataset.id;
|
||||
@@ -278,9 +276,13 @@ const photosView = {
|
||||
const img = tile.querySelector('img');
|
||||
if (!img) continue;
|
||||
|
||||
img.addEventListener('error', () => {
|
||||
this._enqueueVideoThumbnail(tile, img);
|
||||
}, { once: true });
|
||||
img.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
this._enqueueVideoThumbnail(tile, img);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -315,78 +317,96 @@ const photosView = {
|
||||
// Auth is handled via HttpOnly cookie — direct URL works
|
||||
video.src = `/api/files/${fileId}`;
|
||||
|
||||
video.addEventListener('loadeddata', () => {
|
||||
// Seek to 25 % of duration, clamped between 0.5 s and 5 s
|
||||
video.currentTime = Math.min(5, Math.max(0.5, video.duration * 0.25));
|
||||
}, { once: true });
|
||||
video.addEventListener(
|
||||
'loadeddata',
|
||||
() => {
|
||||
// Seek to 25 % of duration, clamped between 0.5 s and 5 s
|
||||
video.currentTime = Math.min(5, Math.max(0.5, video.duration * 0.25));
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
video.addEventListener('seeked', () => {
|
||||
// Pre-scale to thumbnail size in the browser — saves ~22× RAM,
|
||||
// ~15× bandwidth, and lets the server skip resize entirely.
|
||||
const MAX_THUMB = 400; // must match ThumbnailSize::Preview
|
||||
const scale = Math.min(MAX_THUMB / video.videoWidth,
|
||||
MAX_THUMB / video.videoHeight, 1);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(video.videoWidth * scale);
|
||||
canvas.height = Math.round(video.videoHeight * scale);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
video.addEventListener(
|
||||
'seeked',
|
||||
() => {
|
||||
// Pre-scale to thumbnail size in the browser — saves ~22× RAM,
|
||||
// ~15× bandwidth, and lets the server skip resize entirely.
|
||||
const MAX_THUMB = 400; // must match ThumbnailSize::Preview
|
||||
const scale = Math.min(MAX_THUMB / video.videoWidth, MAX_THUMB / video.videoHeight, 1);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(video.videoWidth * scale);
|
||||
canvas.height = Math.round(video.videoHeight * scale);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// JPEG: explicit quality control, universally supported,
|
||||
// and server stores as-is when dimensions fit (zero re-encode).
|
||||
const mimeType = 'image/jpeg';
|
||||
// JPEG: explicit quality control, universally supported,
|
||||
// and server stores as-is when dimensions fit (zero re-encode).
|
||||
const mimeType = 'image/jpeg';
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
this._drainDecodeQueue();
|
||||
return;
|
||||
}
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
this._drainDecodeQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show immediately in the tile
|
||||
const url = URL.createObjectURL(blob);
|
||||
img.src = url;
|
||||
// Cache locally so re-renders are instant
|
||||
this._videoThumbCache.set(fileId, url);
|
||||
// Show immediately in the tile
|
||||
const url = URL.createObjectURL(blob);
|
||||
img.src = url;
|
||||
// Cache locally so re-renders are instant
|
||||
this._videoThumbCache.set(fileId, url);
|
||||
|
||||
// Upload to server for permanent caching
|
||||
const token = localStorage.getItem('token')
|
||||
|| sessionStorage.getItem('token');
|
||||
const headers = { 'Content-Type': blob.type, ...getCsrfHeaders() };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
// Upload to server for permanent caching
|
||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
const headers = { 'Content-Type': blob.type, ...getCsrfHeaders() };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
body: blob,
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
// Switch from blob URL to server URL so the blob
|
||||
// can be garbage-collected and future loads use
|
||||
// the permanently cached JPEG from the server.
|
||||
const serverUrl = `/api/files/${fileId}/thumbnail/preview?v=1`;
|
||||
this._videoThumbCache.set(fileId, serverUrl);
|
||||
}
|
||||
}).catch(() => { /* best-effort */ });
|
||||
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
body: blob
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.ok) {
|
||||
// Switch from blob URL to server URL so the blob
|
||||
// can be garbage-collected and future loads use
|
||||
// the permanently cached JPEG from the server.
|
||||
const serverUrl = `/api/files/${fileId}/thumbnail/preview?v=1`;
|
||||
this._videoThumbCache.set(fileId, serverUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* best-effort */
|
||||
});
|
||||
|
||||
// Release video resources
|
||||
// Release video resources
|
||||
video.src = '';
|
||||
video.load();
|
||||
this._drainDecodeQueue();
|
||||
},
|
||||
mimeType,
|
||||
0.8
|
||||
);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
// If the video can't be loaded at all, keep the generic play badge
|
||||
video.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
video.src = '';
|
||||
video.load();
|
||||
this._drainDecodeQueue();
|
||||
}, mimeType, 0.80);
|
||||
}, { once: true });
|
||||
|
||||
// If the video can't be loaded at all, keep the generic play badge
|
||||
video.addEventListener('error', () => {
|
||||
video.src = '';
|
||||
video.load();
|
||||
this._drainDecodeQueue();
|
||||
}, { once: true });
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
},
|
||||
|
||||
/** Render the group mode toolbar */
|
||||
_renderToolbar() {
|
||||
const t = (k, d) => window.i18n ? window.i18n.t(k) : d;
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
const modes = [
|
||||
['daily', t('photos.view_daily', 'Day')],
|
||||
['monthly', t('photos.view_monthly', 'Month')],
|
||||
@@ -403,7 +423,7 @@ const photosView = {
|
||||
|
||||
/** Render empty state */
|
||||
_renderEmpty() {
|
||||
const t = (k, d) => window.i18n ? window.i18n.t(k) : d;
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
this._container.innerHTML = `
|
||||
<div class="photos-empty">
|
||||
<i class="fas fa-images"></i>
|
||||
@@ -422,10 +442,16 @@ const photosView = {
|
||||
if (this.groupMode === 'yearly') {
|
||||
key = String(d.getFullYear());
|
||||
} else if (this.groupMode === 'monthly') {
|
||||
key = d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' });
|
||||
key = d.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
});
|
||||
} else {
|
||||
key = d.toLocaleDateString(undefined, {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
@@ -456,7 +482,7 @@ const photosView = {
|
||||
}
|
||||
|
||||
// Otherwise open lightbox
|
||||
const idx = this.items.findIndex(f => f.id === id);
|
||||
const idx = this.items.findIndex((f) => f.id === id);
|
||||
if (idx >= 0 && window.photosLightbox) {
|
||||
window.photosLightbox.open(this.items, idx);
|
||||
}
|
||||
@@ -490,7 +516,7 @@ const photosView = {
|
||||
document.body.appendChild(bar);
|
||||
}
|
||||
|
||||
const t = (k, d) => window.i18n ? window.i18n.t(k) : d;
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
const count = this.selected.size;
|
||||
bar.innerHTML = `
|
||||
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
|
||||
@@ -501,7 +527,7 @@ const photosView = {
|
||||
|
||||
bar.querySelector('#photos-sel-clear').onclick = () => {
|
||||
this.selected.clear();
|
||||
this._container.querySelectorAll('.photo-tile.selected').forEach(t => t.classList.remove('selected'));
|
||||
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => t.classList.remove('selected'));
|
||||
this._hideSelectionBar();
|
||||
};
|
||||
|
||||
@@ -518,7 +544,7 @@ const photosView = {
|
||||
console.error('Delete failed:', fid, err);
|
||||
}
|
||||
}
|
||||
this.items = this.items.filter(f => !this.selected.has(f.id));
|
||||
this.items = this.items.filter((f) => !this.selected.has(f.id));
|
||||
this.selected.clear();
|
||||
this._hideSelectionBar();
|
||||
this._renderedCount = 0;
|
||||
@@ -571,7 +597,9 @@ const photosView = {
|
||||
},
|
||||
|
||||
_escAttr(s) {
|
||||
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
||||
return String(s || '')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -122,8 +122,11 @@ const photosLightbox = {
|
||||
// Format date
|
||||
const ts = (item.sort_date || item.created_at) * 1000;
|
||||
const dateStr = new Date(ts).toLocaleDateString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
meta.textContent = `${dateStr} · ${item.size_formatted || ''}`;
|
||||
|
||||
@@ -233,7 +236,7 @@ const photosLightbox = {
|
||||
});
|
||||
// Remove from photosView items too
|
||||
if (window.photosView) {
|
||||
window.photosView.items = window.photosView.items.filter(f => f.id !== item.id);
|
||||
window.photosView.items = window.photosView.items.filter((f) => f.id !== item.id);
|
||||
}
|
||||
this.items.splice(this.index, 1);
|
||||
if (this.items.length === 0) {
|
||||
@@ -274,7 +277,9 @@ const photosLightbox = {
|
||||
},
|
||||
|
||||
_escAttr(s) {
|
||||
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
||||
return String(s || '')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,10 +25,12 @@ const fileSharing = {
|
||||
item_name: options.name || null,
|
||||
item_type: itemType,
|
||||
password: options.password || null,
|
||||
expires_at: options.expirationDate
|
||||
? Math.floor(new Date(options.expirationDate).getTime() / 1000)
|
||||
: null,
|
||||
permissions: options.permissions || { read: true, write: false, reshare: false }
|
||||
expires_at: options.expirationDate ? Math.floor(new Date(options.expirationDate).getTime() / 1000) : null,
|
||||
permissions: options.permissions || {
|
||||
read: true,
|
||||
write: false,
|
||||
reshare: false
|
||||
}
|
||||
};
|
||||
|
||||
const res = await fetch('/api/shares', {
|
||||
@@ -71,13 +73,16 @@ const fileSharing = {
|
||||
*/
|
||||
async getSharedLinksForItem(itemId, itemType) {
|
||||
try {
|
||||
const params = new URLSearchParams({ item_id: itemId, 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 || []);
|
||||
return Array.isArray(data) ? data : data.items || [];
|
||||
} catch (error) {
|
||||
console.error('Error getting shared links for item:', error);
|
||||
return [];
|
||||
@@ -184,7 +189,7 @@ const fileSharing = {
|
||||
*/
|
||||
init() {
|
||||
console.log('File sharing module initialized (API-backed)');
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
document.querySelectorAll('.nav-item').forEach((item) => {
|
||||
const span = item.querySelector('span');
|
||||
if (span && span.getAttribute('data-i18n') === 'nav.shared') {
|
||||
item.addEventListener('click', () => {
|
||||
@@ -204,4 +209,4 @@ window.fileSharing = fileSharing;
|
||||
window.getSharedLinks = () => fileSharing.getSharedLinks();
|
||||
window.updateSharedLink = (id, data) => fileSharing.updateSharedLink(id, data);
|
||||
window.removeSharedLink = (id) => fileSharing.removeSharedLink(id);
|
||||
window.sendShareNotification = (url, email, msg) => fileSharing.sendShareNotification(url, email, msg);
|
||||
window.sendShareNotification = (url, email, msg) => fileSharing.sendShareNotification(url, email, msg);
|
||||
|
||||
+649
-391
File diff suppressed because it is too large
Load Diff
@@ -1,109 +1,117 @@
|
||||
// device-verify.js — Extracted from inline <script> in device-verify.html
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = window.location.origin;
|
||||
var codeInput = document.getElementById('user-code');
|
||||
var deviceInfo = document.getElementById('device-info');
|
||||
var actionButtons = document.getElementById('action-buttons');
|
||||
var errorText = document.getElementById('error-text');
|
||||
var btnApprove = document.getElementById('btn-approve');
|
||||
var btnDeny = document.getElementById('btn-deny');
|
||||
var debounceTimer = null;
|
||||
var currentCode = '';
|
||||
|
||||
// Pre-fill from URL query param (?code=ABCD-1234)
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('code')) {
|
||||
codeInput.value = params.get('code');
|
||||
lookupCode(params.get('code'));
|
||||
}
|
||||
|
||||
// Auto-insert hyphen and lookup on input
|
||||
codeInput.addEventListener('input', function (e) {
|
||||
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9\-]/g, '');
|
||||
// Auto-insert hyphen after 4 chars
|
||||
if (val.length === 4 && val.indexOf('-') === -1) {
|
||||
val = val + '-';
|
||||
}
|
||||
e.target.value = val;
|
||||
errorText.classList.add('hidden');
|
||||
|
||||
// Debounce lookup
|
||||
clearTimeout(debounceTimer);
|
||||
if (val.length >= 9) {
|
||||
debounceTimer = setTimeout(function () { lookupCode(val); }, 300);
|
||||
} else {
|
||||
deviceInfo.classList.add('hidden');
|
||||
actionButtons.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Wire up approve / deny buttons (replaces inline onclick)
|
||||
btnApprove.addEventListener('click', function () { handleAction('approve'); });
|
||||
btnDeny.addEventListener('click', function () { handleAction('deny'); });
|
||||
|
||||
async function lookupCode(code) {
|
||||
try {
|
||||
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (resp.status === 401) {
|
||||
showError('You must be logged in to authorize a device. Please log in first.');
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error('Lookup failed');
|
||||
var data = await resp.json();
|
||||
|
||||
if (data.valid) {
|
||||
currentCode = code;
|
||||
document.getElementById('info-client').textContent = data.client_name || 'Unknown';
|
||||
document.getElementById('info-scopes').textContent = data.scopes || 'all';
|
||||
deviceInfo.classList.remove('hidden');
|
||||
actionButtons.classList.remove('hidden');
|
||||
errorText.classList.add('hidden');
|
||||
} else {
|
||||
deviceInfo.classList.add('hidden');
|
||||
actionButtons.classList.add('hidden');
|
||||
showError('Code not found or expired. Please check and try again.');
|
||||
}
|
||||
} catch (_err) {
|
||||
showError('Failed to verify code. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(action) {
|
||||
btnApprove.disabled = true;
|
||||
btnDeny.disabled = true;
|
||||
|
||||
try {
|
||||
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
||||
body: JSON.stringify({ user_code: currentCode, action: action })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
var err = await resp.json().catch(function () { return {}; });
|
||||
throw new Error(err.message || 'Action failed');
|
||||
}
|
||||
|
||||
document.getElementById('step-code').classList.add('hidden');
|
||||
if (action === 'approve') {
|
||||
document.getElementById('status-success').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('status-denied').classList.remove('hidden');
|
||||
}
|
||||
} catch (err) {
|
||||
btnApprove.disabled = false;
|
||||
btnDeny.disabled = false;
|
||||
showError(err.message || 'Failed to process action.');
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorText.textContent = msg;
|
||||
errorText.classList.remove('hidden');
|
||||
}
|
||||
})();
|
||||
// device-verify.js — Extracted from inline <script> in device-verify.html
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = window.location.origin;
|
||||
var codeInput = document.getElementById('user-code');
|
||||
var deviceInfo = document.getElementById('device-info');
|
||||
var actionButtons = document.getElementById('action-buttons');
|
||||
var errorText = document.getElementById('error-text');
|
||||
var btnApprove = document.getElementById('btn-approve');
|
||||
var btnDeny = document.getElementById('btn-deny');
|
||||
var debounceTimer = null;
|
||||
var currentCode = '';
|
||||
|
||||
// Pre-fill from URL query param (?code=ABCD-1234)
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('code')) {
|
||||
codeInput.value = params.get('code');
|
||||
lookupCode(params.get('code'));
|
||||
}
|
||||
|
||||
// Auto-insert hyphen and lookup on input
|
||||
codeInput.addEventListener('input', function (e) {
|
||||
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9\-]/g, '');
|
||||
// Auto-insert hyphen after 4 chars
|
||||
if (val.length === 4 && val.indexOf('-') === -1) {
|
||||
val = val + '-';
|
||||
}
|
||||
e.target.value = val;
|
||||
errorText.classList.add('hidden');
|
||||
|
||||
// Debounce lookup
|
||||
clearTimeout(debounceTimer);
|
||||
if (val.length >= 9) {
|
||||
debounceTimer = setTimeout(function () {
|
||||
lookupCode(val);
|
||||
}, 300);
|
||||
} else {
|
||||
deviceInfo.classList.add('hidden');
|
||||
actionButtons.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Wire up approve / deny buttons (replaces inline onclick)
|
||||
btnApprove.addEventListener('click', function () {
|
||||
handleAction('approve');
|
||||
});
|
||||
btnDeny.addEventListener('click', function () {
|
||||
handleAction('deny');
|
||||
});
|
||||
|
||||
async function lookupCode(code) {
|
||||
try {
|
||||
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (resp.status === 401) {
|
||||
showError('You must be logged in to authorize a device. Please log in first.');
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error('Lookup failed');
|
||||
var data = await resp.json();
|
||||
|
||||
if (data.valid) {
|
||||
currentCode = code;
|
||||
document.getElementById('info-client').textContent = data.client_name || 'Unknown';
|
||||
document.getElementById('info-scopes').textContent = data.scopes || 'all';
|
||||
deviceInfo.classList.remove('hidden');
|
||||
actionButtons.classList.remove('hidden');
|
||||
errorText.classList.add('hidden');
|
||||
} else {
|
||||
deviceInfo.classList.add('hidden');
|
||||
actionButtons.classList.add('hidden');
|
||||
showError('Code not found or expired. Please check and try again.');
|
||||
}
|
||||
} catch (_err) {
|
||||
showError('Failed to verify code. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(action) {
|
||||
btnApprove.disabled = true;
|
||||
btnDeny.disabled = true;
|
||||
|
||||
try {
|
||||
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
||||
body: JSON.stringify({ user_code: currentCode, action: action })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
var err = await resp.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
throw new Error(err.message || 'Action failed');
|
||||
}
|
||||
|
||||
document.getElementById('step-code').classList.add('hidden');
|
||||
if (action === 'approve') {
|
||||
document.getElementById('status-success').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('status-denied').classList.remove('hidden');
|
||||
}
|
||||
} catch (err) {
|
||||
btnApprove.disabled = false;
|
||||
btnDeny.disabled = false;
|
||||
showError(err.message || 'Failed to process action.');
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorText.textContent = msg;
|
||||
errorText.classList.remove('hidden');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -6,28 +6,36 @@ var errorTitle = document.getElementById('error-title');
|
||||
var errorMessage = document.getElementById('error-message');
|
||||
var errorAction = document.getElementById('error-action');
|
||||
|
||||
switch(errorType) {
|
||||
switch (errorType) {
|
||||
case 'invalid-credentials':
|
||||
errorTitle.textContent = 'Login Failed';
|
||||
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
||||
errorAction.textContent = 'Try Again';
|
||||
errorAction.addEventListener('click', function() { history.back(); });
|
||||
errorAction.addEventListener('click', function () {
|
||||
history.back();
|
||||
});
|
||||
break;
|
||||
case 'session-expired':
|
||||
errorTitle.textContent = 'Session Expired';
|
||||
errorMessage.textContent = 'Your session has expired. Please try again.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
errorAction.addEventListener('click', function () {
|
||||
window.close();
|
||||
});
|
||||
break;
|
||||
case 'not-found':
|
||||
errorTitle.textContent = 'Not Found';
|
||||
errorMessage.textContent = 'The requested page was not found.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
errorAction.addEventListener('click', function () {
|
||||
window.close();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
errorTitle.textContent = 'Error';
|
||||
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
errorAction.addEventListener('click', function () {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ if (!/^[0-9a-fA-F]+$/.test(token)) {
|
||||
document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
|
||||
|
||||
// Check if OIDC is available and configure SSO button
|
||||
(async function() {
|
||||
(async function () {
|
||||
try {
|
||||
var resp = await fetch('/api/auth/oidc/providers');
|
||||
if (!resp.ok) return;
|
||||
@@ -29,10 +29,10 @@ document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
|
||||
}
|
||||
|
||||
// SSO button redirects to the OIDC flow for this NC token
|
||||
btn.addEventListener('click', function() {
|
||||
btn.addEventListener('click', function () {
|
||||
window.location.href = '/login/v2/flow/' + token + '/oidc';
|
||||
});
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// OIDC not available — silently keep password-only mode
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
document.getElementById('close-window-btn').addEventListener('click', function() {
|
||||
document.getElementById('close-window-btn').addEventListener('click', function () {
|
||||
window.close();
|
||||
});
|
||||
// Auto-close after 3 seconds
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
window.close();
|
||||
}, 3000);
|
||||
|
||||
+283
-247
@@ -2,144 +2,163 @@ const API = '/api';
|
||||
|
||||
/* ── i18n helper — falls back to key if i18n not ready ── */
|
||||
function t(key, params) {
|
||||
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
|
||||
return key.split('.').pop().replace(/_/g, ' ');
|
||||
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
|
||||
return key.split('.').pop().replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024,
|
||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr) return t('profile.never');
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const secs = Math.floor((now - d) / 1000);
|
||||
if (secs < 60) return t('profile.just_now');
|
||||
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs/60) });
|
||||
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs/3600) });
|
||||
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs/86400) });
|
||||
return d.toLocaleDateString();
|
||||
if (!dateStr) return t('profile.never');
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const secs = Math.floor((now - d) / 1000);
|
||||
if (secs < 60) return t('profile.just_now');
|
||||
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs / 60) });
|
||||
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs / 3600) });
|
||||
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs / 86400) });
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/me', { headers: headers(), credentials: 'same-origin' });
|
||||
if (!resp.ok) { showError(); return; }
|
||||
const user = await resp.json();
|
||||
|
||||
const initials = (user.username || '?').substring(0, 2).toUpperCase();
|
||||
document.getElementById('p-avatar').textContent = initials;
|
||||
document.getElementById('p-username').textContent = user.username;
|
||||
document.getElementById('p-email').textContent = user.email || '';
|
||||
|
||||
const badge = document.getElementById('p-role-badge');
|
||||
if (user.role === 'admin') {
|
||||
badge.className = 'role-badge role-badge-admin';
|
||||
badge.innerHTML = '<i class="fas fa-shield-alt"></i> ' + t('profile.role_admin');
|
||||
} else {
|
||||
badge.className = 'role-badge role-badge-user';
|
||||
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_user');
|
||||
}
|
||||
|
||||
document.getElementById('p-detail-username').textContent = user.username;
|
||||
document.getElementById('p-detail-email').textContent = user.email || '—';
|
||||
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? t('profile.role_admin') : t('profile.role_user');
|
||||
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
|
||||
|
||||
const used = user.storage_used_bytes || 0;
|
||||
const quota = user.storage_quota_bytes || 0;
|
||||
const pct = quota > 0 ? Math.min(Math.round((used / quota) * 100), 100) : 0;
|
||||
|
||||
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
||||
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
||||
document.getElementById('p-storage-pct').textContent = quota > 0 ? pct + '%' : '—';
|
||||
|
||||
const bar = document.getElementById('p-storage-bar');
|
||||
bar.style.width = pct + '%';
|
||||
bar.className = 'storage-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green');
|
||||
document.getElementById('p-storage-text').textContent = formatBytes(used) + ' / ' + (quota > 0 ? formatBytes(quota) : t('profile.unlimited'));
|
||||
|
||||
if (user.auth_provider && user.auth_provider !== 'local') {
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
loadAppPasswords();
|
||||
|
||||
try {
|
||||
const oidcResp = await fetch(API + '/auth/oidc/providers', { credentials: 'same-origin' });
|
||||
if (oidcResp.ok) {
|
||||
const oidcInfo = await oidcResp.json();
|
||||
if (!oidcInfo.password_login_enabled) {
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
const resp = await fetch(API + '/auth/me', {
|
||||
headers: headers(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (oidcErr) {
|
||||
}
|
||||
const user = await resp.json();
|
||||
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
document.getElementById('main-content').classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError();
|
||||
}
|
||||
const initials = (user.username || '?').substring(0, 2).toUpperCase();
|
||||
document.getElementById('p-avatar').textContent = initials;
|
||||
document.getElementById('p-username').textContent = user.username;
|
||||
document.getElementById('p-email').textContent = user.email || '';
|
||||
|
||||
const badge = document.getElementById('p-role-badge');
|
||||
if (user.role === 'admin') {
|
||||
badge.className = 'role-badge role-badge-admin';
|
||||
badge.innerHTML = '<i class="fas fa-shield-alt"></i> ' + t('profile.role_admin');
|
||||
} else {
|
||||
badge.className = 'role-badge role-badge-user';
|
||||
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_user');
|
||||
}
|
||||
|
||||
document.getElementById('p-detail-username').textContent = user.username;
|
||||
document.getElementById('p-detail-email').textContent = user.email || '—';
|
||||
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? t('profile.role_admin') : t('profile.role_user');
|
||||
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
|
||||
|
||||
const used = user.storage_used_bytes || 0;
|
||||
const quota = user.storage_quota_bytes || 0;
|
||||
const pct = quota > 0 ? Math.min(Math.round((used / quota) * 100), 100) : 0;
|
||||
|
||||
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
||||
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
||||
document.getElementById('p-storage-pct').textContent = quota > 0 ? pct + '%' : '—';
|
||||
|
||||
const bar = document.getElementById('p-storage-bar');
|
||||
bar.style.width = pct + '%';
|
||||
bar.className = 'storage-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green');
|
||||
document.getElementById('p-storage-text').textContent = formatBytes(used) + ' / ' + (quota > 0 ? formatBytes(quota) : t('profile.unlimited'));
|
||||
|
||||
if (user.auth_provider && user.auth_provider !== 'local') {
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
loadAppPasswords();
|
||||
|
||||
try {
|
||||
const oidcResp = await fetch(API + '/auth/oidc/providers', {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (oidcResp.ok) {
|
||||
const oidcInfo = await oidcResp.json();
|
||||
if (!oidcInfo.password_login_enabled) {
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
} catch (oidcErr) {}
|
||||
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
document.getElementById('main-content').classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
function showError() {
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
document.getElementById('auth-error').classList.remove('hidden');
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
document.getElementById('auth-error').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function changePassword(e) {
|
||||
e.preventDefault();
|
||||
const currentPw = document.getElementById('current-password').value;
|
||||
const newPw = document.getElementById('new-password').value;
|
||||
const confirmPw = document.getElementById('confirm-password').value;
|
||||
const statusEl = document.getElementById('pw-status');
|
||||
e.preventDefault();
|
||||
const currentPw = document.getElementById('current-password').value;
|
||||
const newPw = document.getElementById('new-password').value;
|
||||
const confirmPw = document.getElementById('confirm-password').value;
|
||||
const statusEl = document.getElementById('pw-status');
|
||||
|
||||
if (newPw !== confirmPw) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newPw.length < 8) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.password_too_short')) + '</div>';
|
||||
return false;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('pw-submit');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
|
||||
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/change-password', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ current_password: currentPw, new_password: newPw })
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
statusEl.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + escapeHtml(t('profile.password_updated')) + '</div>';
|
||||
document.getElementById('password-form').reset();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(err.message || t('profile.password_change_failed')) + '</div>';
|
||||
if (newPw !== confirmPw) {
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_network', { message: err.message })) + '</div>';
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
|
||||
return false;
|
||||
if (newPw.length < 8) {
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.password_too_short')) + '</div>';
|
||||
return false;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('pw-submit');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
|
||||
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/change-password', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
current_password: currentPw,
|
||||
new_password: newPw
|
||||
})
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
statusEl.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + escapeHtml(t('profile.password_updated')) + '</div>';
|
||||
document.getElementById('password-form').reset();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||
escapeHtml(err.message || t('profile.password_change_failed')) +
|
||||
'</div>';
|
||||
}
|
||||
} catch (err) {
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||
escapeHtml(t('profile.error_network', { message: err.message })) +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── App Passwords ──
|
||||
@@ -147,164 +166,177 @@ async function changePassword(e) {
|
||||
const AUTO_LABELS = ['Nextcloud', 'Nextcloud (OIDC)'];
|
||||
|
||||
function isAutoPassword(pw) {
|
||||
return AUTO_LABELS.includes(pw.label);
|
||||
return AUTO_LABELS.includes(pw.label);
|
||||
}
|
||||
|
||||
function renderPwRow(pw) {
|
||||
const tr = document.createElement('tr');
|
||||
const label = document.createElement('td');
|
||||
label.textContent = pw.label;
|
||||
const created = document.createElement('td');
|
||||
created.textContent = new Date(pw.created_at).toLocaleDateString();
|
||||
const lastUsed = document.createElement('td');
|
||||
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
|
||||
const status = document.createElement('td');
|
||||
const badge = document.createElement('span');
|
||||
if (pw.active !== false) {
|
||||
badge.className = 'badge badge-active';
|
||||
badge.textContent = t('profile.active');
|
||||
} else {
|
||||
badge.className = 'badge badge-expired';
|
||||
badge.textContent = t('profile.revoked');
|
||||
}
|
||||
status.appendChild(badge);
|
||||
const actions = document.createElement('td');
|
||||
if (pw.active !== false) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-danger-sm';
|
||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
btn.title = t('profile.revoke_title');
|
||||
btn.addEventListener('click', function () { revokeAppPassword(pw.id, pw.label); });
|
||||
actions.appendChild(btn);
|
||||
}
|
||||
tr.append(label, created, lastUsed, status, actions);
|
||||
return tr;
|
||||
const tr = document.createElement('tr');
|
||||
const label = document.createElement('td');
|
||||
label.textContent = pw.label;
|
||||
const created = document.createElement('td');
|
||||
created.textContent = new Date(pw.created_at).toLocaleDateString();
|
||||
const lastUsed = document.createElement('td');
|
||||
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
|
||||
const status = document.createElement('td');
|
||||
const badge = document.createElement('span');
|
||||
if (pw.active !== false) {
|
||||
badge.className = 'badge badge-active';
|
||||
badge.textContent = t('profile.active');
|
||||
} else {
|
||||
badge.className = 'badge badge-expired';
|
||||
badge.textContent = t('profile.revoked');
|
||||
}
|
||||
status.appendChild(badge);
|
||||
const actions = document.createElement('td');
|
||||
if (pw.active !== false) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-danger-sm';
|
||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
btn.title = t('profile.revoke_title');
|
||||
btn.addEventListener('click', function () {
|
||||
revokeAppPassword(pw.id, pw.label);
|
||||
});
|
||||
actions.appendChild(btn);
|
||||
}
|
||||
tr.append(label, created, lastUsed, status, actions);
|
||||
return tr;
|
||||
}
|
||||
|
||||
async function loadAppPasswords() {
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords', { headers: headers(), credentials: 'same-origin' });
|
||||
if (!resp.ok) {
|
||||
document.getElementById('app-passwords-section').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const passwords = data.app_passwords || data;
|
||||
const userPws = passwords.filter(function (pw) { return !isAutoPassword(pw); });
|
||||
const autoPws = passwords.filter(isAutoPassword);
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords', {
|
||||
headers: headers(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) {
|
||||
document.getElementById('app-passwords-section').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const passwords = data.app_passwords || data;
|
||||
const userPws = passwords.filter(function (pw) {
|
||||
return !isAutoPassword(pw);
|
||||
});
|
||||
const autoPws = passwords.filter(isAutoPassword);
|
||||
|
||||
// User-created passwords
|
||||
const tbody = document.getElementById('app-pw-tbody');
|
||||
const table = document.getElementById('app-pw-table');
|
||||
const empty = document.getElementById('app-pw-empty');
|
||||
tbody.innerHTML = '';
|
||||
if (userPws.length === 0) {
|
||||
table.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
} else {
|
||||
table.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
for (const pw of userPws) tbody.appendChild(renderPwRow(pw));
|
||||
}
|
||||
// User-created passwords
|
||||
const tbody = document.getElementById('app-pw-tbody');
|
||||
const table = document.getElementById('app-pw-table');
|
||||
const empty = document.getElementById('app-pw-empty');
|
||||
tbody.innerHTML = '';
|
||||
if (userPws.length === 0) {
|
||||
table.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
} else {
|
||||
table.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
for (const pw of userPws) tbody.appendChild(renderPwRow(pw));
|
||||
}
|
||||
|
||||
// Auto-generated (client session) passwords
|
||||
const autoSection = document.getElementById('app-pw-auto-section');
|
||||
if (autoPws.length === 0) {
|
||||
autoSection.classList.add('hidden');
|
||||
} else {
|
||||
autoSection.classList.remove('hidden');
|
||||
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
|
||||
const autoTbody = document.getElementById('app-pw-auto-tbody');
|
||||
autoTbody.innerHTML = '';
|
||||
for (const pw of autoPws) autoTbody.appendChild(renderPwRow(pw));
|
||||
// Auto-generated (client session) passwords
|
||||
const autoSection = document.getElementById('app-pw-auto-section');
|
||||
if (autoPws.length === 0) {
|
||||
autoSection.classList.add('hidden');
|
||||
} else {
|
||||
autoSection.classList.remove('hidden');
|
||||
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
|
||||
const autoTbody = document.getElementById('app-pw-auto-tbody');
|
||||
autoTbody.innerHTML = '';
|
||||
for (const pw of autoPws) autoTbody.appendChild(renderPwRow(pw));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load app passwords', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load app passwords', e);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoPasswords() {
|
||||
const body = document.getElementById('app-pw-auto-body');
|
||||
const chevron = document.getElementById('app-pw-auto-chevron');
|
||||
const isHidden = body.classList.contains('hidden');
|
||||
body.classList.toggle('hidden', !isHidden);
|
||||
chevron.className = isHidden ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
|
||||
const body = document.getElementById('app-pw-auto-body');
|
||||
const chevron = document.getElementById('app-pw-auto-chevron');
|
||||
const isHidden = body.classList.contains('hidden');
|
||||
body.classList.toggle('hidden', !isHidden);
|
||||
chevron.className = isHidden ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
|
||||
}
|
||||
|
||||
async function createAppPassword() {
|
||||
const labelInput = document.getElementById('app-pw-label');
|
||||
const label = labelInput.value.trim();
|
||||
const statusEl = document.getElementById('app-pw-status');
|
||||
const btn = document.getElementById('app-pw-generate');
|
||||
const labelInput = document.getElementById('app-pw-label');
|
||||
const label = labelInput.value.trim();
|
||||
const statusEl = document.getElementById('app-pw-status');
|
||||
const btn = document.getElementById('app-pw-generate');
|
||||
|
||||
if (!label) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
|
||||
statusEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ label: label })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(err.message || t('profile.error_create_pw')) + '</div>';
|
||||
return;
|
||||
if (!label) {
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
|
||||
statusEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ label: label })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||
escapeHtml(err.message || t('profile.error_create_pw')) +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
const result = await resp.json();
|
||||
document.getElementById('app-pw-created-label').textContent = result.label;
|
||||
document.getElementById('app-pw-created-password').textContent = result.password;
|
||||
document.getElementById('app-pw-created').classList.remove('hidden');
|
||||
labelInput.value = '';
|
||||
loadAppPasswords();
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
|
||||
}
|
||||
const result = await resp.json();
|
||||
document.getElementById('app-pw-created-label').textContent = result.label;
|
||||
document.getElementById('app-pw-created-password').textContent = result.password;
|
||||
document.getElementById('app-pw-created').classList.remove('hidden');
|
||||
labelInput.value = '';
|
||||
loadAppPasswords();
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
|
||||
}
|
||||
}
|
||||
|
||||
function copyAppPassword() {
|
||||
const pw = document.getElementById('app-pw-created-password').textContent;
|
||||
navigator.clipboard.writeText(pw).then(function () {
|
||||
const btn = document.getElementById('app-pw-copy-btn');
|
||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(function () { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 1500);
|
||||
});
|
||||
const pw = document.getElementById('app-pw-created-password').textContent;
|
||||
navigator.clipboard.writeText(pw).then(function () {
|
||||
const btn = document.getElementById('app-pw-copy-btn');
|
||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(function () {
|
||||
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
async function revokeAppPassword(id, label) {
|
||||
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
|
||||
method: 'DELETE',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (resp.ok || resp.status === 204) {
|
||||
document.getElementById('app-pw-created').classList.add('hidden');
|
||||
loadAppPasswords();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
alert(err.message || t('profile.error_revoke'));
|
||||
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
|
||||
method: 'DELETE',
|
||||
headers: headers(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (resp.ok || resp.status === 204) {
|
||||
document.getElementById('app-pw-created').classList.add('hidden');
|
||||
loadAppPasswords();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
alert(err.message || t('profile.error_revoke'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert(t('profile.error_network', { message: err.message }));
|
||||
}
|
||||
} catch (err) {
|
||||
alert(t('profile.error_network', { message: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -316,5 +348,9 @@ document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPass
|
||||
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
||||
|
||||
/* Re-render when language changes */
|
||||
window.addEventListener('translationsLoaded', function () { init(); });
|
||||
window.addEventListener('localeChanged', function () { init(); });
|
||||
window.addEventListener('translationsLoaded', function () {
|
||||
init();
|
||||
});
|
||||
window.addEventListener('localeChanged', function () {
|
||||
init();
|
||||
});
|
||||
|
||||
@@ -26,12 +26,12 @@ const sharedView = {
|
||||
this.attachEventListeners();
|
||||
this.loadItems().then(() => this.filterAndSortItems());
|
||||
const c = document.getElementById('shared-container');
|
||||
if (c) c.classList.remove("hidden");
|
||||
if (c) c.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hide() {
|
||||
const c = document.getElementById('shared-container');
|
||||
if (c) c.classList.add("hidden");
|
||||
if (c) c.classList.add('hidden');
|
||||
},
|
||||
|
||||
// Load shared items from backend API
|
||||
@@ -195,7 +195,7 @@ const sharedView = {
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
|
||||
if (!sel.contains(e.target)) sel.classList.remove('open');
|
||||
});
|
||||
});
|
||||
@@ -209,16 +209,24 @@ const sharedView = {
|
||||
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
if (enablePw) enablePw.addEventListener('change', () => {
|
||||
if (pwField) { pwField.disabled = !enablePw.checked; if (enablePw.checked) pwField.focus(); }
|
||||
});
|
||||
if (enablePw)
|
||||
enablePw.addEventListener('change', () => {
|
||||
if (pwField) {
|
||||
pwField.disabled = !enablePw.checked;
|
||||
if (enablePw.checked) pwField.focus();
|
||||
}
|
||||
});
|
||||
const genPwBtn = document.getElementById('sv-generate-password');
|
||||
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
if (enableExp) enableExp.addEventListener('change', () => {
|
||||
if (expField) { expField.disabled = !enableExp.checked; if (enableExp.checked) expField.focus(); }
|
||||
});
|
||||
if (enableExp)
|
||||
enableExp.addEventListener('change', () => {
|
||||
if (expField) {
|
||||
expField.disabled = !enableExp.checked;
|
||||
if (enableExp.checked) expField.focus();
|
||||
}
|
||||
});
|
||||
const updateBtn = document.getElementById('sv-update-share-btn');
|
||||
if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem());
|
||||
const removeBtn = document.getElementById('sv-remove-share-btn');
|
||||
@@ -253,17 +261,17 @@ const sharedView = {
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Close other open selects
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
|
||||
if (sel !== wrapper) sel.classList.remove('open');
|
||||
});
|
||||
wrapper.classList.toggle('open');
|
||||
});
|
||||
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach(option => {
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach((option) => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Update active state
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach(o => o.classList.remove('active'));
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach((o) => o.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
// Update label
|
||||
const label = toggle.querySelector('.shared-select-label');
|
||||
@@ -288,7 +296,7 @@ const sharedView = {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
|
||||
this.filteredItems = this.items.filter(item => {
|
||||
this.filteredItems = this.items.filter((item) => {
|
||||
if (type !== 'all' && item.item_type !== type) return false;
|
||||
const name = (item.item_name || item.item_id || '').toLowerCase();
|
||||
return name.includes(searchTerm);
|
||||
@@ -321,16 +329,15 @@ const sharedView = {
|
||||
sharedItemsList.innerHTML = '';
|
||||
|
||||
if (this.filteredItems.length === 0) {
|
||||
emptyState.classList.remove("hidden");
|
||||
listContainer.classList.add("hidden");
|
||||
emptyState.classList.remove('hidden');
|
||||
listContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add("hidden");
|
||||
listContainer.classList.remove("hidden");
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
listContainer.classList.remove('hidden');
|
||||
|
||||
this.filteredItems.forEach(item => {
|
||||
this.filteredItems.forEach((item) => {
|
||||
const row = document.createElement('tr');
|
||||
const displayName = item.item_name || item.item_id || 'Unknown';
|
||||
|
||||
@@ -383,7 +390,8 @@ const sharedView = {
|
||||
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
|
||||
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
|
||||
copyBtn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(item.url)
|
||||
navigator.clipboard
|
||||
.writeText(item.url)
|
||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
});
|
||||
@@ -392,7 +400,10 @@ const sharedView = {
|
||||
rmBtn.className = 'action-btn remove-btn';
|
||||
rmBtn.innerHTML = '<span class="action-icon">🗑️</span>';
|
||||
rmBtn.title = this.translate('shared_removeShare', 'Remove Share');
|
||||
rmBtn.addEventListener('click', () => { this.currentItem = item; this.removeSharedItem(); });
|
||||
rmBtn.addEventListener('click', () => {
|
||||
this.currentItem = item;
|
||||
this.removeSharedItem();
|
||||
});
|
||||
|
||||
actionsCell.append(editBtn, notifyBtn, copyBtn, rmBtn);
|
||||
row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell);
|
||||
@@ -428,7 +439,10 @@ const sharedView = {
|
||||
|
||||
if (enablePw) {
|
||||
enablePw.checked = item.has_password;
|
||||
if (pwField) { pwField.disabled = !enablePw.checked; pwField.value = ''; }
|
||||
if (pwField) {
|
||||
pwField.disabled = !enablePw.checked;
|
||||
pwField.value = '';
|
||||
}
|
||||
}
|
||||
if (enableExp) {
|
||||
enableExp.checked = !!item.expires_at;
|
||||
@@ -473,7 +487,8 @@ const sharedView = {
|
||||
copyShareLink() {
|
||||
const el = document.getElementById('sv-share-link-url');
|
||||
if (!el) return;
|
||||
navigator.clipboard.writeText(el.value)
|
||||
navigator.clipboard
|
||||
.writeText(el.value)
|
||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
},
|
||||
@@ -514,10 +529,8 @@ const sharedView = {
|
||||
write: permWrite ? permWrite.checked : false,
|
||||
reshare: permReshare ? permReshare.checked : false
|
||||
},
|
||||
password: (enablePw && enablePw.checked && pwField && pwField.value) ? pwField.value : null,
|
||||
expires_at: (enableExp && enableExp.checked && expField && expField.value)
|
||||
? Math.floor(new Date(expField.value).getTime() / 1000)
|
||||
: null
|
||||
password: enablePw && enablePw.checked && pwField && pwField.value ? pwField.value : null,
|
||||
expires_at: enableExp && enableExp.checked && expField && expField.value ? Math.floor(new Date(expField.value).getTime() / 1000) : null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -576,8 +589,12 @@ const sharedView = {
|
||||
}
|
||||
|
||||
if (window.fileSharing && window.fileSharing.sendShareNotification) {
|
||||
window.fileSharing.sendShareNotification(this.currentItem.url, email, message)
|
||||
.then(() => { this.closeNotificationDialog(); this.showNotification(this.translate('shared_notificationSent', 'Notification sent')); })
|
||||
window.fileSharing
|
||||
.sendShareNotification(this.currentItem.url, email, message)
|
||||
.then(() => {
|
||||
this.closeNotificationDialog();
|
||||
this.showNotification(this.translate('shared_notificationSent', 'Notification sent'));
|
||||
})
|
||||
.catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error'));
|
||||
}
|
||||
},
|
||||
|
||||
+554
-554
File diff suppressed because it is too large
Load Diff
+550
-550
File diff suppressed because it is too large
Load Diff
+554
-554
File diff suppressed because it is too large
Load Diff
+553
-553
File diff suppressed because it is too large
Load Diff
+524
-524
File diff suppressed because it is too large
Load Diff
+548
-548
File diff suppressed because it is too large
Load Diff
+554
-554
File diff suppressed because it is too large
Load Diff
+549
-549
File diff suppressed because it is too large
Load Diff
+554
-554
File diff suppressed because it is too large
Load Diff
+554
-554
File diff suppressed because it is too large
Load Diff
+550
-550
File diff suppressed because it is too large
Load Diff
+548
-548
File diff suppressed because it is too large
Load Diff
+554
-554
File diff suppressed because it is too large
Load Diff
+486
-486
@@ -1,489 +1,489 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "极简云存储系统"
|
||||
},
|
||||
"nav": {
|
||||
"files": "文件",
|
||||
"shared": "共享",
|
||||
"recent": "最近",
|
||||
"favorites": "收藏",
|
||||
"photos": "照片",
|
||||
"trash": "回收站"
|
||||
},
|
||||
"photos": {
|
||||
"empty_state": "还没有照片",
|
||||
"empty_hint": "上传图片或视频即可在此查看",
|
||||
"items_selected": "已选择",
|
||||
"view_daily": "日",
|
||||
"view_monthly": "月",
|
||||
"view_yearly": "年"
|
||||
},
|
||||
"actions": {
|
||||
"search": "搜索文件...",
|
||||
"new_folder": "新建文件夹",
|
||||
"upload": "上传",
|
||||
"upload_files": "上传文件",
|
||||
"upload_folder": "上传文件夹",
|
||||
"upload.uploading": "上传中...",
|
||||
"upload.complete": "{count} / {total} 已上传",
|
||||
"upload.files": "文件",
|
||||
"rename": "重命名",
|
||||
"move": "移动到...",
|
||||
"move_to": "移动到",
|
||||
"delete": "删除",
|
||||
"download": "下载",
|
||||
"view": "查看",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"share": "共享",
|
||||
"favorite": "添加到收藏",
|
||||
"unfavorite": "取消收藏",
|
||||
"copy": "复制",
|
||||
"notify": "通知",
|
||||
"send": "发送",
|
||||
"clear_recent": "清除最近",
|
||||
"logout": "退出登录",
|
||||
"create": "创建",
|
||||
"search_btn": "搜索",
|
||||
"close": "关闭"
|
||||
},
|
||||
"user_menu": {
|
||||
"appearance": "外观",
|
||||
"about": "关于 OxiCloud",
|
||||
"about_description": "基于 Rust 和整洁架构构建的云存储平台。快速、安全、私密。",
|
||||
"admin_panel": "管理面板",
|
||||
"profile": "我的资料",
|
||||
"role_user": "用户"
|
||||
},
|
||||
"share": {
|
||||
"dialogTitle": "共享链接",
|
||||
"linkLabel": "共享链接:",
|
||||
"copyLink": "复制",
|
||||
"permissions": "权限:",
|
||||
"permissionRead": "读取",
|
||||
"permissionWrite": "写入",
|
||||
"permissionReshare": "再共享",
|
||||
"password": "密码保护:",
|
||||
"generatePassword": "生成",
|
||||
"expiration": "过期日期:",
|
||||
"update": "更新共享",
|
||||
"remove": "移除共享",
|
||||
"notifyTitle": "发送通知",
|
||||
"notifyEmailLabel": "电子邮件地址:",
|
||||
"notifyMessageLabel": "消息(可选):",
|
||||
"notifySend": "发送通知",
|
||||
"shareWithOthers": "与他人共享",
|
||||
"sharePublicly": "公开共享",
|
||||
"shareSettings": "共享设置",
|
||||
"shareCopied": "链接已复制到剪贴板",
|
||||
"shareCreated": "共享链接创建成功",
|
||||
"shareUpdated": "共享设置更新成功",
|
||||
"shareRemoved": "共享已移除"
|
||||
},
|
||||
"share_dialogTitle": "共享链接",
|
||||
"share_linkLabel": "共享链接:",
|
||||
"share_copyLink": "复制",
|
||||
"share_permissions": "权限:",
|
||||
"share_permissionRead": "读取",
|
||||
"share_permissionWrite": "写入",
|
||||
"share_permissionReshare": "再共享",
|
||||
"share_password": "密码保护:",
|
||||
"share_generatePassword": "生成",
|
||||
"share_expiration": "过期日期:",
|
||||
"share_update": "更新共享",
|
||||
"share_remove": "移除共享",
|
||||
"share_notifyTitle": "发送通知",
|
||||
"share_notifyEmailLabel": "电子邮件地址:",
|
||||
"share_notifyMessageLabel": "消息(可选):",
|
||||
"share_notifySend": "发送通知",
|
||||
"shared": {
|
||||
"backToFiles": "返回文件",
|
||||
"pageTitle": "共享资源",
|
||||
"pageDescription": "管理你的共享文件和文件夹",
|
||||
"filterType": "类型:",
|
||||
"filterAll": "全部",
|
||||
"filterFiles": "文件",
|
||||
"filterFolders": "文件夹",
|
||||
"sortBy": "排序依据:",
|
||||
"sortByName": "名称",
|
||||
"sortByDate": "共享日期",
|
||||
"sortByExpiration": "过期日期",
|
||||
"search": "搜索",
|
||||
"colName": "名称",
|
||||
"colType": "类型",
|
||||
"colDateShared": "共享日期",
|
||||
"colExpiration": "过期日期",
|
||||
"colPermissions": "权限",
|
||||
"colPassword": "密码",
|
||||
"colActions": "操作",
|
||||
"emptyStateTitle": "尚未有共享资源",
|
||||
"emptyStateDesc": "当你共享文件或文件夹时,它们会出现在这里",
|
||||
"goToFiles": "前往文件",
|
||||
"typeFile": "文件",
|
||||
"typeFolder": "文件夹",
|
||||
"noExpiration": "无过期",
|
||||
"hasPassword": "有",
|
||||
"noPassword": "无",
|
||||
"editShare": "编辑共享",
|
||||
"notifyShare": "通知某人",
|
||||
"copyLink": "复制链接",
|
||||
"removeShare": "移除共享",
|
||||
"linkCopied": "链接已复制到剪贴板!",
|
||||
"linkCopyFailed": "复制链接失败",
|
||||
"itemUpdated": "共享设置更新成功",
|
||||
"itemRemoved": "共享已移除成功",
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"notificationSent": "通知已成功发送",
|
||||
"notificationFailed": "发送通知失败"
|
||||
},
|
||||
"files": {
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"size": "大小",
|
||||
"modified": "修改日期",
|
||||
"no_files": "此文件夹中没有文件",
|
||||
"empty_hint": "上传文件或创建文件夹以开始使用",
|
||||
"loading": "正在加载文件…",
|
||||
"view_grid": "网格视图",
|
||||
"view_list": "列表视图",
|
||||
"file_types": {
|
||||
"document": "文档",
|
||||
"image": "图片",
|
||||
"video": "视频",
|
||||
"audio": "音频",
|
||||
"pdf": "PDF",
|
||||
"text": "文本",
|
||||
"folder": "文件夹",
|
||||
"spreadsheet": "电子表格",
|
||||
"presentation": "演示文稿",
|
||||
"archive": "压缩文件",
|
||||
"installer": "安装程序",
|
||||
"code": "代码"
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "极简云存储系统"
|
||||
},
|
||||
"nav": {
|
||||
"files": "文件",
|
||||
"shared": "共享",
|
||||
"recent": "最近",
|
||||
"favorites": "收藏",
|
||||
"photos": "照片",
|
||||
"trash": "回收站"
|
||||
},
|
||||
"photos": {
|
||||
"empty_state": "还没有照片",
|
||||
"empty_hint": "上传图片或视频即可在此查看",
|
||||
"items_selected": "已选择",
|
||||
"view_daily": "日",
|
||||
"view_monthly": "月",
|
||||
"view_yearly": "年"
|
||||
},
|
||||
"actions": {
|
||||
"search": "搜索文件...",
|
||||
"new_folder": "新建文件夹",
|
||||
"upload": "上传",
|
||||
"upload_files": "上传文件",
|
||||
"upload_folder": "上传文件夹",
|
||||
"upload.uploading": "上传中...",
|
||||
"upload.complete": "{count} / {total} 已上传",
|
||||
"upload.files": "文件",
|
||||
"rename": "重命名",
|
||||
"move": "移动到...",
|
||||
"move_to": "移动到",
|
||||
"delete": "删除",
|
||||
"download": "下载",
|
||||
"view": "查看",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"share": "共享",
|
||||
"favorite": "添加到收藏",
|
||||
"unfavorite": "取消收藏",
|
||||
"copy": "复制",
|
||||
"notify": "通知",
|
||||
"send": "发送",
|
||||
"clear_recent": "清除最近",
|
||||
"logout": "退出登录",
|
||||
"create": "创建",
|
||||
"search_btn": "搜索",
|
||||
"close": "关闭"
|
||||
},
|
||||
"user_menu": {
|
||||
"appearance": "外观",
|
||||
"about": "关于 OxiCloud",
|
||||
"about_description": "基于 Rust 和整洁架构构建的云存储平台。快速、安全、私密。",
|
||||
"admin_panel": "管理面板",
|
||||
"profile": "我的资料",
|
||||
"role_user": "用户"
|
||||
},
|
||||
"share": {
|
||||
"dialogTitle": "共享链接",
|
||||
"linkLabel": "共享链接:",
|
||||
"copyLink": "复制",
|
||||
"permissions": "权限:",
|
||||
"permissionRead": "读取",
|
||||
"permissionWrite": "写入",
|
||||
"permissionReshare": "再共享",
|
||||
"password": "密码保护:",
|
||||
"generatePassword": "生成",
|
||||
"expiration": "过期日期:",
|
||||
"update": "更新共享",
|
||||
"remove": "移除共享",
|
||||
"notifyTitle": "发送通知",
|
||||
"notifyEmailLabel": "电子邮件地址:",
|
||||
"notifyMessageLabel": "消息(可选):",
|
||||
"notifySend": "发送通知",
|
||||
"shareWithOthers": "与他人共享",
|
||||
"sharePublicly": "公开共享",
|
||||
"shareSettings": "共享设置",
|
||||
"shareCopied": "链接已复制到剪贴板",
|
||||
"shareCreated": "共享链接创建成功",
|
||||
"shareUpdated": "共享设置更新成功",
|
||||
"shareRemoved": "共享已移除"
|
||||
},
|
||||
"share_dialogTitle": "共享链接",
|
||||
"share_linkLabel": "共享链接:",
|
||||
"share_copyLink": "复制",
|
||||
"share_permissions": "权限:",
|
||||
"share_permissionRead": "读取",
|
||||
"share_permissionWrite": "写入",
|
||||
"share_permissionReshare": "再共享",
|
||||
"share_password": "密码保护:",
|
||||
"share_generatePassword": "生成",
|
||||
"share_expiration": "过期日期:",
|
||||
"share_update": "更新共享",
|
||||
"share_remove": "移除共享",
|
||||
"share_notifyTitle": "发送通知",
|
||||
"share_notifyEmailLabel": "电子邮件地址:",
|
||||
"share_notifyMessageLabel": "消息(可选):",
|
||||
"share_notifySend": "发送通知",
|
||||
"shared": {
|
||||
"backToFiles": "返回文件",
|
||||
"pageTitle": "共享资源",
|
||||
"pageDescription": "管理你的共享文件和文件夹",
|
||||
"filterType": "类型:",
|
||||
"filterAll": "全部",
|
||||
"filterFiles": "文件",
|
||||
"filterFolders": "文件夹",
|
||||
"sortBy": "排序依据:",
|
||||
"sortByName": "名称",
|
||||
"sortByDate": "共享日期",
|
||||
"sortByExpiration": "过期日期",
|
||||
"search": "搜索",
|
||||
"colName": "名称",
|
||||
"colType": "类型",
|
||||
"colDateShared": "共享日期",
|
||||
"colExpiration": "过期日期",
|
||||
"colPermissions": "权限",
|
||||
"colPassword": "密码",
|
||||
"colActions": "操作",
|
||||
"emptyStateTitle": "尚未有共享资源",
|
||||
"emptyStateDesc": "当你共享文件或文件夹时,它们会出现在这里",
|
||||
"goToFiles": "前往文件",
|
||||
"typeFile": "文件",
|
||||
"typeFolder": "文件夹",
|
||||
"noExpiration": "无过期",
|
||||
"hasPassword": "有",
|
||||
"noPassword": "无",
|
||||
"editShare": "编辑共享",
|
||||
"notifyShare": "通知某人",
|
||||
"copyLink": "复制链接",
|
||||
"removeShare": "移除共享",
|
||||
"linkCopied": "链接已复制到剪贴板!",
|
||||
"linkCopyFailed": "复制链接失败",
|
||||
"itemUpdated": "共享设置更新成功",
|
||||
"itemRemoved": "共享已移除成功",
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"notificationSent": "通知已成功发送",
|
||||
"notificationFailed": "发送通知失败"
|
||||
},
|
||||
"files": {
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"size": "大小",
|
||||
"modified": "修改日期",
|
||||
"no_files": "此文件夹中没有文件",
|
||||
"empty_hint": "上传文件或创建文件夹以开始使用",
|
||||
"loading": "正在加载文件…",
|
||||
"view_grid": "网格视图",
|
||||
"view_list": "列表视图",
|
||||
"file_types": {
|
||||
"document": "文档",
|
||||
"image": "图片",
|
||||
"video": "视频",
|
||||
"audio": "音频",
|
||||
"pdf": "PDF",
|
||||
"text": "文本",
|
||||
"folder": "文件夹",
|
||||
"spreadsheet": "电子表格",
|
||||
"presentation": "演示文稿",
|
||||
"archive": "压缩文件",
|
||||
"installer": "安装程序",
|
||||
"code": "代码"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
"rename_folder": "重命名文件夹",
|
||||
"new_name": "新名称",
|
||||
"new_folder_title": "新建文件夹",
|
||||
"folder_name": "文件夹名称",
|
||||
"folder_placeholder": "我的文件夹",
|
||||
"rename_title": "重命名",
|
||||
"move_file": "移动文件",
|
||||
"select_destination": "选择目标文件夹",
|
||||
"root": "根目录",
|
||||
"delete_confirmation": "你确定要删除",
|
||||
"and_contents": "及其所有内容",
|
||||
"no_undo": "此操作无法撤销",
|
||||
"share_file": "共享文件",
|
||||
"share_folder": "共享文件夹",
|
||||
"existing_shares": "现有共享",
|
||||
"share_options": "共享选项",
|
||||
"password": "密码",
|
||||
"expiration": "过期日期",
|
||||
"permissions": "权限",
|
||||
"generated_link": "生成的链接",
|
||||
"notify": "发送通知",
|
||||
"recipient": "收件人",
|
||||
"message": "消息"
|
||||
},
|
||||
"dropzone": {
|
||||
"drag_files": "将文件拖到这里,或点击选择",
|
||||
"drop_files": "释放文件以上传"
|
||||
},
|
||||
"permissions": {
|
||||
"read": "读取",
|
||||
"write": "写入",
|
||||
"reshare": "再共享"
|
||||
},
|
||||
"errors": {
|
||||
"file_not_found": "文件未找到",
|
||||
"folder_not_found": "文件夹未找到",
|
||||
"delete_error": "删除时出错",
|
||||
"upload_error": "上传文件时出错",
|
||||
"rename_error": "重命名时出错",
|
||||
"move_error": "移动时出错",
|
||||
"empty_name": "名称不能为空",
|
||||
"name_exists": "已存在同名文件或文件夹",
|
||||
"generic_error": "发生错误"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"home": "主页"
|
||||
},
|
||||
"trash": {
|
||||
"empty_trash": "清空回收站",
|
||||
"empty_state": "回收站为空",
|
||||
"original_location": "原始位置",
|
||||
"deleted_date": "删除日期",
|
||||
"actions": "操作",
|
||||
"restore": "恢复",
|
||||
"delete_permanently": "永久删除",
|
||||
"empty_confirm": "你确定要清空回收站吗?这将永久删除所有项目。"
|
||||
},
|
||||
"auth": {
|
||||
"login_title": "登录",
|
||||
"username": "用户名",
|
||||
"username_placeholder": "输入你的用户名",
|
||||
"password": "密码",
|
||||
"password_placeholder": "输入你的密码",
|
||||
"login_button": "登录",
|
||||
"no_account": "没有账号?",
|
||||
"register": "注册",
|
||||
"admin_setup": "首次使用?",
|
||||
"setup": "设置管理员",
|
||||
"register_title": "创建账号",
|
||||
"email": "电子邮件",
|
||||
"email_placeholder": "输入你的电子邮件",
|
||||
"confirm_password": "确认密码",
|
||||
"confirm_password_placeholder": "确认你的密码",
|
||||
"register_button": "创建账号",
|
||||
"have_account": "已有账号?",
|
||||
"login": "登录",
|
||||
"setup_title": "初始设置",
|
||||
"setup_step1": "管理员",
|
||||
"setup_step2": "系统",
|
||||
"setup_step3": "完成",
|
||||
"admin_username": "管理员用户名",
|
||||
"admin_email": "管理员电子邮件",
|
||||
"admin_password": "管理员密码",
|
||||
"create_admin": "创建管理员",
|
||||
"back_to_login": "已设置完成?",
|
||||
"admin_success": "管理员账号创建成功!您现在可以登录。",
|
||||
"account_success": "账号创建成功!您现在可以登录。",
|
||||
"passwords_mismatch": "密码不匹配",
|
||||
"admin_create_error": "创建管理员账号时出错",
|
||||
"or": "或",
|
||||
"sso_login": "使用 SSO 登录",
|
||||
"sso_login_provider": "使用 {{provider}} 登录"
|
||||
},
|
||||
"storage": {
|
||||
"title": "存储空间",
|
||||
"calculating": "计算中...",
|
||||
"used": "{{percentage}}% 已使用 ({{used}} / {{total}})"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "无法预览此文件类型。",
|
||||
"download_file": "下载文件",
|
||||
"zoom_in": "放大",
|
||||
"zoom_out": "缩小",
|
||||
"zoom_reset": "重置缩放"
|
||||
},
|
||||
"language_selector": {
|
||||
"title": "欢迎!",
|
||||
"subtitle": "选择您的语言以继续",
|
||||
"continue": "继续",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"zh": "中文",
|
||||
"fa": "فارسی",
|
||||
"fr": "Français",
|
||||
"de": "Deutsch",
|
||||
"pt": "Português"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"empty_state": "还没有收藏",
|
||||
"empty_hint": "为文件或文件夹添加星标以将其添加到收藏夹",
|
||||
"add": "添加到收藏夹",
|
||||
"remove": "从收藏夹移除",
|
||||
"added_title": "已添加到收藏",
|
||||
"added_msg": "已添加到收藏",
|
||||
"removed_title": "已从收藏移除",
|
||||
"removed_msg": "已从收藏移除"
|
||||
},
|
||||
"recent": {
|
||||
"title": "最近",
|
||||
"clear": "清除最近",
|
||||
"accessed": "访问于",
|
||||
"empty_state": "没有最近文件",
|
||||
"empty_hint": "您打开的文件将显示在这里"
|
||||
},
|
||||
"batch": {
|
||||
"one_selected": "已选择 1 个项目",
|
||||
"n_selected": "已选择 {{count}} 个项目",
|
||||
"confirm_delete": "确定要将 {{count}} 个项目移至回收站吗?",
|
||||
"move_title": "移动 {{count}} 个项目",
|
||||
"add_favorites": "添加到收藏夹",
|
||||
"move_copy": "移动或复制"
|
||||
},
|
||||
"admin": {
|
||||
"page_title": "管理面板",
|
||||
"back_to_app": "返回 OxiCloud",
|
||||
"loading": "加载中…",
|
||||
"access_denied": "拒绝访问",
|
||||
"access_denied_desc": "需要管理员权限。",
|
||||
"sign_in": "登录",
|
||||
"tab_dashboard": "仪表盘",
|
||||
"tab_users": "用户",
|
||||
"tab_oidc": "SSO / OIDC",
|
||||
"total_users": "用户总数",
|
||||
"active_users": "活跃用户",
|
||||
"admins": "管理员",
|
||||
"version": "版本",
|
||||
"storage_overview": "存储概览",
|
||||
"used": "已使用",
|
||||
"total_quota": "总配额",
|
||||
"usage_pct": "使用率",
|
||||
"users_over_80": "超过80%配额",
|
||||
"users_over_quota": "超过配额",
|
||||
"system": "系统",
|
||||
"auth_label": "认证",
|
||||
"oidc_label": "OIDC",
|
||||
"quotas_label": "配额",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"active": "活跃",
|
||||
"off": "关闭",
|
||||
"allow_registration": "允许公开自助注册",
|
||||
"registration_warning": "公开注册已禁用。只有管理员可以创建新用户。",
|
||||
"user_management": "用户管理",
|
||||
"create_user": "创建用户",
|
||||
"col_user": "用户",
|
||||
"col_role": "角色",
|
||||
"col_auth": "认证",
|
||||
"col_status": "状态",
|
||||
"col_storage": "存储",
|
||||
"col_last_login": "最后登录",
|
||||
"col_actions": "操作",
|
||||
"loading_users": "正在加载用户…",
|
||||
"failed_load_users": "加载失败",
|
||||
"no_users_found": "未找到用户",
|
||||
"showing_users": "显示 {{from}}-{{to}} / {{total}}",
|
||||
"prev": "上一页",
|
||||
"next": "下一页",
|
||||
"inactive": "未激活",
|
||||
"you_badge": "(你)",
|
||||
"local": "本地",
|
||||
"never": "从未",
|
||||
"just_now": "刚刚",
|
||||
"minutes_ago": "{{n}}分钟前",
|
||||
"hours_ago": "{{n}}小时前",
|
||||
"days_ago": "{{n}}天前",
|
||||
"edit_quota_title": "编辑配额",
|
||||
"reset_password_title": "重置密码",
|
||||
"toggle_role_title": "切换角色",
|
||||
"deactivate_title": "停用",
|
||||
"activate_title": "启用",
|
||||
"delete_title": "删除",
|
||||
"sso_title": "单点登录 (OIDC / SSO)",
|
||||
"enable_sso": "启用 SSO 认证",
|
||||
"provider_name": "提供商名称",
|
||||
"issuer_url": "发行者 URL",
|
||||
"issuer_url_hint": "您的身份提供商的 OpenID Connect 发行者 URL",
|
||||
"auto_discover": "自动发现",
|
||||
"discovering": "发现中…",
|
||||
"client_id": "客户端 ID",
|
||||
"client_secret": "客户端密钥",
|
||||
"client_secret_placeholder": "留空以保留当前值",
|
||||
"secret_configured": "已配置客户端密钥",
|
||||
"callback_url": "回调 URL",
|
||||
"callback_url_hint": "(在您的 IdP 中注册)",
|
||||
"advanced_settings": "高级设置",
|
||||
"scopes": "范围",
|
||||
"auto_provision": "首次登录时自动配置用户",
|
||||
"admin_groups": "管理组",
|
||||
"admin_groups_hint": "映射到管理员角色的逗号分隔 OIDC 组名",
|
||||
"disable_password": "禁用密码登录 (仅 OIDC)",
|
||||
"password_warning": "这将阻止所有基于密码的登录!",
|
||||
"test_btn": "测试",
|
||||
"save_btn": "保存",
|
||||
"saving": "保存中…",
|
||||
"settings_saved": "设置已保存 — OIDC 现在 {{status}}",
|
||||
"quota_modal_title": "更新存储配额",
|
||||
"quota_user_label": "用户:",
|
||||
"new_quota": "新配额",
|
||||
"quota_unlimited_hint": "0表示无限制",
|
||||
"cancel": "取消",
|
||||
"create_user_title": "创建新用户",
|
||||
"username_label": "用户名",
|
||||
"username_placeholder": "zhangsan",
|
||||
"username_hint": "3–32个字符",
|
||||
"password_label": "密码",
|
||||
"password_placeholder": "至少8个字符",
|
||||
"email_label": "邮箱",
|
||||
"email_optional": "(可选)",
|
||||
"email_placeholder": "user@example.com (留空自动生成)",
|
||||
"role_label": "角色",
|
||||
"role_user": "用户",
|
||||
"role_admin": "管理员",
|
||||
"quota_label": "配额",
|
||||
"creating": "创建中…",
|
||||
"reset_pw_title": "重置密码",
|
||||
"new_password_label": "新密码",
|
||||
"resetting": "重置中…",
|
||||
"reset_btn": "重置",
|
||||
"confirm_role_change": "将角色更改为 {{role}}?",
|
||||
"confirm_deactivate": "确定要停用此用户吗?",
|
||||
"confirm_activate": "确定要启用此用户吗?",
|
||||
"confirm_delete_user": "删除用户 \"{{name}}\"?此操作无法撤消!",
|
||||
"confirm_action": "确认操作",
|
||||
"confirm_yes": "确认",
|
||||
"confirm_no": "取消",
|
||||
"error_username_short": "用户名至少需要3个字符",
|
||||
"error_password_short": "密码至少需要8个字符",
|
||||
"error_generic": "失败",
|
||||
"error_network": "网络错误:{{message}}",
|
||||
"error_create_user": "创建用户失败"
|
||||
},
|
||||
"profile": {
|
||||
"page_title": "个人资料",
|
||||
"back_to_app": "返回 OxiCloud",
|
||||
"loading": "加载中…",
|
||||
"not_authenticated": "未认证",
|
||||
"not_authenticated_desc": "请登录以查看您的个人资料。",
|
||||
"sign_in": "登录",
|
||||
"role_admin": "管理员",
|
||||
"role_user": "用户",
|
||||
"account_details": "账户详情",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"role": "角色",
|
||||
"last_login": "最后登录",
|
||||
"storage": "存储",
|
||||
"used": "已使用",
|
||||
"quota": "配额",
|
||||
"usage": "使用率",
|
||||
"unlimited": "无限制",
|
||||
"app_passwords": "应用密码",
|
||||
"app_pw_desc": "为 WebDAV、CalDAV 和 CardDAV 客户端生成密码。每个密码只显示一次。",
|
||||
"app_pw_label_placeholder": "标签(如 Thunderbird、macOS)",
|
||||
"generate": "生成",
|
||||
"generating": "生成中…",
|
||||
"new_password_for": "新密码用于",
|
||||
"copy_warning": "请立即复制此密码,之后将无法再次查看。",
|
||||
"copy_to_clipboard": "复制到剪贴板",
|
||||
"col_label": "标签",
|
||||
"col_created": "创建时间",
|
||||
"col_last_used": "最后使用",
|
||||
"col_status": "状态",
|
||||
"active": "活跃",
|
||||
"revoked": "已撤销",
|
||||
"revoke_title": "撤销",
|
||||
"no_app_passwords": "暂无应用密码。",
|
||||
"client_sessions": "客户端会话",
|
||||
"client_sessions_desc": "连接 Nextcloud 兼容客户端时自动生成。",
|
||||
"col_client": "客户端",
|
||||
"never": "从未",
|
||||
"just_now": "刚刚",
|
||||
"minutes_ago": "{{n}}分钟前",
|
||||
"hours_ago": "{{n}}小时前",
|
||||
"days_ago": "{{n}}天前",
|
||||
"change_password": "修改密码",
|
||||
"current_password": "当前密码",
|
||||
"new_password": "新密码",
|
||||
"min_8_chars": "至少8个字符",
|
||||
"confirm_password": "确认新密码",
|
||||
"update_password": "更新密码",
|
||||
"updating": "更新中…",
|
||||
"password_updated": "密码更新成功",
|
||||
"passwords_no_match": "密码不匹配",
|
||||
"password_too_short": "密码至少需要8个字符",
|
||||
"password_change_failed": "修改密码失败",
|
||||
"error_network": "网络错误:{{message}}",
|
||||
"error_label_required": "请输入标签",
|
||||
"error_create_pw": "创建应用密码失败",
|
||||
"confirm_revoke": "撤销应用密码\"{{label}}\"?使用此密码的客户端将停止工作。",
|
||||
"error_revoke": "撤销失败"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
"rename_folder": "重命名文件夹",
|
||||
"new_name": "新名称",
|
||||
"new_folder_title": "新建文件夹",
|
||||
"folder_name": "文件夹名称",
|
||||
"folder_placeholder": "我的文件夹",
|
||||
"rename_title": "重命名",
|
||||
"move_file": "移动文件",
|
||||
"select_destination": "选择目标文件夹",
|
||||
"root": "根目录",
|
||||
"delete_confirmation": "你确定要删除",
|
||||
"and_contents": "及其所有内容",
|
||||
"no_undo": "此操作无法撤销",
|
||||
"share_file": "共享文件",
|
||||
"share_folder": "共享文件夹",
|
||||
"existing_shares": "现有共享",
|
||||
"share_options": "共享选项",
|
||||
"password": "密码",
|
||||
"expiration": "过期日期",
|
||||
"permissions": "权限",
|
||||
"generated_link": "生成的链接",
|
||||
"notify": "发送通知",
|
||||
"recipient": "收件人",
|
||||
"message": "消息"
|
||||
},
|
||||
"dropzone": {
|
||||
"drag_files": "将文件拖到这里,或点击选择",
|
||||
"drop_files": "释放文件以上传"
|
||||
},
|
||||
"permissions": {
|
||||
"read": "读取",
|
||||
"write": "写入",
|
||||
"reshare": "再共享"
|
||||
},
|
||||
"errors": {
|
||||
"file_not_found": "文件未找到",
|
||||
"folder_not_found": "文件夹未找到",
|
||||
"delete_error": "删除时出错",
|
||||
"upload_error": "上传文件时出错",
|
||||
"rename_error": "重命名时出错",
|
||||
"move_error": "移动时出错",
|
||||
"empty_name": "名称不能为空",
|
||||
"name_exists": "已存在同名文件或文件夹",
|
||||
"generic_error": "发生错误"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"home": "主页"
|
||||
},
|
||||
"trash": {
|
||||
"empty_trash": "清空回收站",
|
||||
"empty_state": "回收站为空",
|
||||
"original_location": "原始位置",
|
||||
"deleted_date": "删除日期",
|
||||
"actions": "操作",
|
||||
"restore": "恢复",
|
||||
"delete_permanently": "永久删除",
|
||||
"empty_confirm": "你确定要清空回收站吗?这将永久删除所有项目。"
|
||||
},
|
||||
"auth": {
|
||||
"login_title": "登录",
|
||||
"username": "用户名",
|
||||
"username_placeholder": "输入你的用户名",
|
||||
"password": "密码",
|
||||
"password_placeholder": "输入你的密码",
|
||||
"login_button": "登录",
|
||||
"no_account": "没有账号?",
|
||||
"register": "注册",
|
||||
"admin_setup": "首次使用?",
|
||||
"setup": "设置管理员",
|
||||
"register_title": "创建账号",
|
||||
"email": "电子邮件",
|
||||
"email_placeholder": "输入你的电子邮件",
|
||||
"confirm_password": "确认密码",
|
||||
"confirm_password_placeholder": "确认你的密码",
|
||||
"register_button": "创建账号",
|
||||
"have_account": "已有账号?",
|
||||
"login": "登录",
|
||||
"setup_title": "初始设置",
|
||||
"setup_step1": "管理员",
|
||||
"setup_step2": "系统",
|
||||
"setup_step3": "完成",
|
||||
"admin_username": "管理员用户名",
|
||||
"admin_email": "管理员电子邮件",
|
||||
"admin_password": "管理员密码",
|
||||
"create_admin": "创建管理员",
|
||||
"back_to_login": "已设置完成?",
|
||||
"admin_success": "管理员账号创建成功!您现在可以登录。",
|
||||
"account_success": "账号创建成功!您现在可以登录。",
|
||||
"passwords_mismatch": "密码不匹配",
|
||||
"admin_create_error": "创建管理员账号时出错",
|
||||
"or": "或",
|
||||
"sso_login": "使用 SSO 登录",
|
||||
"sso_login_provider": "使用 {{provider}} 登录"
|
||||
},
|
||||
"storage": {
|
||||
"title": "存储空间",
|
||||
"calculating": "计算中...",
|
||||
"used": "{{percentage}}% 已使用 ({{used}} / {{total}})"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "无法预览此文件类型。",
|
||||
"download_file": "下载文件",
|
||||
"zoom_in": "放大",
|
||||
"zoom_out": "缩小",
|
||||
"zoom_reset": "重置缩放"
|
||||
},
|
||||
"language_selector": {
|
||||
"title": "欢迎!",
|
||||
"subtitle": "选择您的语言以继续",
|
||||
"continue": "继续",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"zh": "中文",
|
||||
"fa": "فارسی",
|
||||
"fr": "Français",
|
||||
"de": "Deutsch",
|
||||
"pt": "Português"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"empty_state": "还没有收藏",
|
||||
"empty_hint": "为文件或文件夹添加星标以将其添加到收藏夹",
|
||||
"add": "添加到收藏夹",
|
||||
"remove": "从收藏夹移除",
|
||||
"added_title": "已添加到收藏",
|
||||
"added_msg": "已添加到收藏",
|
||||
"removed_title": "已从收藏移除",
|
||||
"removed_msg": "已从收藏移除"
|
||||
},
|
||||
"recent": {
|
||||
"title": "最近",
|
||||
"clear": "清除最近",
|
||||
"accessed": "访问于",
|
||||
"empty_state": "没有最近文件",
|
||||
"empty_hint": "您打开的文件将显示在这里"
|
||||
},
|
||||
"batch": {
|
||||
"one_selected": "已选择 1 个项目",
|
||||
"n_selected": "已选择 {{count}} 个项目",
|
||||
"confirm_delete": "确定要将 {{count}} 个项目移至回收站吗?",
|
||||
"move_title": "移动 {{count}} 个项目",
|
||||
"add_favorites": "添加到收藏夹",
|
||||
"move_copy": "移动或复制"
|
||||
},
|
||||
"admin": {
|
||||
"page_title": "管理面板",
|
||||
"back_to_app": "返回 OxiCloud",
|
||||
"loading": "加载中…",
|
||||
"access_denied": "拒绝访问",
|
||||
"access_denied_desc": "需要管理员权限。",
|
||||
"sign_in": "登录",
|
||||
"tab_dashboard": "仪表盘",
|
||||
"tab_users": "用户",
|
||||
"tab_oidc": "SSO / OIDC",
|
||||
"total_users": "用户总数",
|
||||
"active_users": "活跃用户",
|
||||
"admins": "管理员",
|
||||
"version": "版本",
|
||||
"storage_overview": "存储概览",
|
||||
"used": "已使用",
|
||||
"total_quota": "总配额",
|
||||
"usage_pct": "使用率",
|
||||
"users_over_80": "超过80%配额",
|
||||
"users_over_quota": "超过配额",
|
||||
"system": "系统",
|
||||
"auth_label": "认证",
|
||||
"oidc_label": "OIDC",
|
||||
"quotas_label": "配额",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"active": "活跃",
|
||||
"off": "关闭",
|
||||
"allow_registration": "允许公开自助注册",
|
||||
"registration_warning": "公开注册已禁用。只有管理员可以创建新用户。",
|
||||
"user_management": "用户管理",
|
||||
"create_user": "创建用户",
|
||||
"col_user": "用户",
|
||||
"col_role": "角色",
|
||||
"col_auth": "认证",
|
||||
"col_status": "状态",
|
||||
"col_storage": "存储",
|
||||
"col_last_login": "最后登录",
|
||||
"col_actions": "操作",
|
||||
"loading_users": "正在加载用户…",
|
||||
"failed_load_users": "加载失败",
|
||||
"no_users_found": "未找到用户",
|
||||
"showing_users": "显示 {{from}}-{{to}} / {{total}}",
|
||||
"prev": "上一页",
|
||||
"next": "下一页",
|
||||
"inactive": "未激活",
|
||||
"you_badge": "(你)",
|
||||
"local": "本地",
|
||||
"never": "从未",
|
||||
"just_now": "刚刚",
|
||||
"minutes_ago": "{{n}}分钟前",
|
||||
"hours_ago": "{{n}}小时前",
|
||||
"days_ago": "{{n}}天前",
|
||||
"edit_quota_title": "编辑配额",
|
||||
"reset_password_title": "重置密码",
|
||||
"toggle_role_title": "切换角色",
|
||||
"deactivate_title": "停用",
|
||||
"activate_title": "启用",
|
||||
"delete_title": "删除",
|
||||
"sso_title": "单点登录 (OIDC / SSO)",
|
||||
"enable_sso": "启用 SSO 认证",
|
||||
"provider_name": "提供商名称",
|
||||
"issuer_url": "发行者 URL",
|
||||
"issuer_url_hint": "您的身份提供商的 OpenID Connect 发行者 URL",
|
||||
"auto_discover": "自动发现",
|
||||
"discovering": "发现中…",
|
||||
"client_id": "客户端 ID",
|
||||
"client_secret": "客户端密钥",
|
||||
"client_secret_placeholder": "留空以保留当前值",
|
||||
"secret_configured": "已配置客户端密钥",
|
||||
"callback_url": "回调 URL",
|
||||
"callback_url_hint": "(在您的 IdP 中注册)",
|
||||
"advanced_settings": "高级设置",
|
||||
"scopes": "范围",
|
||||
"auto_provision": "首次登录时自动配置用户",
|
||||
"admin_groups": "管理组",
|
||||
"admin_groups_hint": "映射到管理员角色的逗号分隔 OIDC 组名",
|
||||
"disable_password": "禁用密码登录 (仅 OIDC)",
|
||||
"password_warning": "这将阻止所有基于密码的登录!",
|
||||
"test_btn": "测试",
|
||||
"save_btn": "保存",
|
||||
"saving": "保存中…",
|
||||
"settings_saved": "设置已保存 — OIDC 现在 {{status}}",
|
||||
"quota_modal_title": "更新存储配额",
|
||||
"quota_user_label": "用户:",
|
||||
"new_quota": "新配额",
|
||||
"quota_unlimited_hint": "0表示无限制",
|
||||
"cancel": "取消",
|
||||
"create_user_title": "创建新用户",
|
||||
"username_label": "用户名",
|
||||
"username_placeholder": "zhangsan",
|
||||
"username_hint": "3–32个字符",
|
||||
"password_label": "密码",
|
||||
"password_placeholder": "至少8个字符",
|
||||
"email_label": "邮箱",
|
||||
"email_optional": "(可选)",
|
||||
"email_placeholder": "user@example.com (留空自动生成)",
|
||||
"role_label": "角色",
|
||||
"role_user": "用户",
|
||||
"role_admin": "管理员",
|
||||
"quota_label": "配额",
|
||||
"creating": "创建中…",
|
||||
"reset_pw_title": "重置密码",
|
||||
"new_password_label": "新密码",
|
||||
"resetting": "重置中…",
|
||||
"reset_btn": "重置",
|
||||
"confirm_role_change": "将角色更改为 {{role}}?",
|
||||
"confirm_deactivate": "确定要停用此用户吗?",
|
||||
"confirm_activate": "确定要启用此用户吗?",
|
||||
"confirm_delete_user": "删除用户 \"{{name}}\"?此操作无法撤消!",
|
||||
"confirm_action": "确认操作",
|
||||
"confirm_yes": "确认",
|
||||
"confirm_no": "取消",
|
||||
"error_username_short": "用户名至少需要3个字符",
|
||||
"error_password_short": "密码至少需要8个字符",
|
||||
"error_generic": "失败",
|
||||
"error_network": "网络错误:{{message}}",
|
||||
"error_create_user": "创建用户失败"
|
||||
},
|
||||
"profile": {
|
||||
"page_title": "个人资料",
|
||||
"back_to_app": "返回 OxiCloud",
|
||||
"loading": "加载中…",
|
||||
"not_authenticated": "未认证",
|
||||
"not_authenticated_desc": "请登录以查看您的个人资料。",
|
||||
"sign_in": "登录",
|
||||
"role_admin": "管理员",
|
||||
"role_user": "用户",
|
||||
"account_details": "账户详情",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"role": "角色",
|
||||
"last_login": "最后登录",
|
||||
"storage": "存储",
|
||||
"used": "已使用",
|
||||
"quota": "配额",
|
||||
"usage": "使用率",
|
||||
"unlimited": "无限制",
|
||||
"app_passwords": "应用密码",
|
||||
"app_pw_desc": "为 WebDAV、CalDAV 和 CardDAV 客户端生成密码。每个密码只显示一次。",
|
||||
"app_pw_label_placeholder": "标签(如 Thunderbird、macOS)",
|
||||
"generate": "生成",
|
||||
"generating": "生成中…",
|
||||
"new_password_for": "新密码用于",
|
||||
"copy_warning": "请立即复制此密码,之后将无法再次查看。",
|
||||
"copy_to_clipboard": "复制到剪贴板",
|
||||
"col_label": "标签",
|
||||
"col_created": "创建时间",
|
||||
"col_last_used": "最后使用",
|
||||
"col_status": "状态",
|
||||
"active": "活跃",
|
||||
"revoked": "已撤销",
|
||||
"revoke_title": "撤销",
|
||||
"no_app_passwords": "暂无应用密码。",
|
||||
"client_sessions": "客户端会话",
|
||||
"client_sessions_desc": "连接 Nextcloud 兼容客户端时自动生成。",
|
||||
"col_client": "客户端",
|
||||
"never": "从未",
|
||||
"just_now": "刚刚",
|
||||
"minutes_ago": "{{n}}分钟前",
|
||||
"hours_ago": "{{n}}小时前",
|
||||
"days_ago": "{{n}}天前",
|
||||
"change_password": "修改密码",
|
||||
"current_password": "当前密码",
|
||||
"new_password": "新密码",
|
||||
"min_8_chars": "至少8个字符",
|
||||
"confirm_password": "确认新密码",
|
||||
"update_password": "更新密码",
|
||||
"updating": "更新中…",
|
||||
"password_updated": "密码更新成功",
|
||||
"passwords_no_match": "密码不匹配",
|
||||
"password_too_short": "密码至少需要8个字符",
|
||||
"password_change_failed": "修改密码失败",
|
||||
"error_network": "网络错误:{{message}}",
|
||||
"error_label_required": "请输入标签",
|
||||
"error_create_pw": "创建应用密码失败",
|
||||
"confirm_revoke": "撤销应用密码\"{{label}}\"?使用此密码的客户端将停止工作。",
|
||||
"error_revoke": "撤销失败"
|
||||
}
|
||||
}
|
||||
|
||||
+89
-82
@@ -1,100 +1,107 @@
|
||||
// OxiCloud Service Worker
|
||||
const CACHE_NAME = 'oxicloud-cache-v16';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/js/icons.js',
|
||||
'/js/i18n.js',
|
||||
'/js/languageSelector.js',
|
||||
'/js/notifications.js',
|
||||
'/locales/en.json',
|
||||
'/locales/es.json',
|
||||
'/locales/fa.json',
|
||||
'/locales/de.json',
|
||||
'/locales/nl.json',
|
||||
'/favicon.ico'
|
||||
'/',
|
||||
'/index.html',
|
||||
'/js/icons.js',
|
||||
'/js/i18n.js',
|
||||
'/js/languageSelector.js',
|
||||
'/js/notifications.js',
|
||||
'/locales/en.json',
|
||||
'/locales/es.json',
|
||||
'/locales/fa.json',
|
||||
'/locales/de.json',
|
||||
'/locales/nl.json',
|
||||
'/favicon.ico'
|
||||
];
|
||||
|
||||
// Install event - cache assets
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Cache opened');
|
||||
return cache.addAll(ASSETS_TO_CACHE);
|
||||
})
|
||||
.then(() => self.skipWaiting()) // Activate immediately
|
||||
);
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('Cache opened');
|
||||
return cache.addAll(ASSETS_TO_CACHE);
|
||||
})
|
||||
.then(() => self.skipWaiting()) // Activate immediately
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean old caches
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.filter(cacheName => {
|
||||
return cacheName !== CACHE_NAME;
|
||||
}).map(cacheName => {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
}).then(() => self.clients.claim()) // Take control of clients
|
||||
);
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((cacheName) => {
|
||||
return cacheName !== CACHE_NAME;
|
||||
})
|
||||
.map((cacheName) => {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim()) // Take control of clients
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache, update cache from network
|
||||
self.addEventListener('fetch', event => {
|
||||
// Don't intercept API requests - let them go straight to the network
|
||||
if (event.request.url.includes('/api/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
// Cache hit - return the response from the cached version
|
||||
if (response) {
|
||||
// For non-core assets, still fetch from network for updates
|
||||
if (!ASSETS_TO_CACHE.includes(new URL(event.request.url).pathname)) {
|
||||
fetch(event.request).then(networkResponse => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const clonedResponse = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
cache.put(event.request, clonedResponse);
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Don't intercept API requests - let them go straight to the network
|
||||
if (event.request.url.includes('/api/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
// Cache hit - return the response from the cached version
|
||||
if (response) {
|
||||
// For non-core assets, still fetch from network for updates
|
||||
if (!ASSETS_TO_CACHE.includes(new URL(event.request.url).pathname)) {
|
||||
fetch(event.request)
|
||||
.then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const clonedResponse = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, clonedResponse);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore network fetch errors - we already have a cached version
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Not in cache - get from network and add to cache
|
||||
return fetch(event.request).then((response) => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the response as it's a stream and can only be consumed once
|
||||
const responseToCache = response.clone();
|
||||
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignore network fetch errors - we already have a cached version
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Not in cache - get from network and add to cache
|
||||
return fetch(event.request).then(response => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the response as it's a stream and can only be consumed once
|
||||
const responseToCache = response.clone();
|
||||
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Background sync for failed requests
|
||||
self.addEventListener('sync', event => {
|
||||
if (event.tag === 'oxicloud-sync') {
|
||||
event.waitUntil(
|
||||
// Implement background sync for pending file operations
|
||||
Promise.resolve() // Placeholder for actual implementation
|
||||
);
|
||||
}
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'oxicloud-sync') {
|
||||
event.waitUntil(
|
||||
// Implement background sync for pending file operations
|
||||
Promise.resolve() // Placeholder for actual implementation
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user