Merge pull request #271 from EdouardVanbelle/linter
This commit is contained in:
@@ -2,7 +2,11 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main", "dev" ]
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- "feat/**"
|
||||||
|
- "fix/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main", "dev" ]
|
branches: [ "main", "dev" ]
|
||||||
|
|
||||||
@@ -12,8 +16,46 @@ env:
|
|||||||
DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test"
|
DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt:
|
|
||||||
|
# Detect which parts of the codebase changed
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
frontend: ${{ steps.filter.outputs.frontend }}
|
||||||
|
backend: ${{ steps.filter.outputs.backend }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
frontend:
|
||||||
|
- 'static/**'
|
||||||
|
- 'biome.json'
|
||||||
|
- 'jsconfig.json'
|
||||||
|
backend:
|
||||||
|
- 'src/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
|
||||||
|
frontend-linter:
|
||||||
|
name: Frontend — Biome
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.frontend == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Biome
|
||||||
|
uses: biomejs/setup-biome@v2
|
||||||
|
|
||||||
|
- name: Run Biome check
|
||||||
|
run: biome ci static/
|
||||||
|
|
||||||
|
rust-fmt:
|
||||||
name: Rustfmt
|
name: Rustfmt
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.backend == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -22,8 +64,10 @@ jobs:
|
|||||||
components: rustfmt
|
components: rustfmt
|
||||||
- run: cargo fmt --all --check
|
- run: cargo fmt --all --check
|
||||||
|
|
||||||
clippy:
|
rust-clippy:
|
||||||
name: Clippy
|
name: Clippy
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.backend == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -33,8 +77,10 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo clippy --all-targets --all-features -- -D warnings
|
- run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
test:
|
rust-test:
|
||||||
name: Tests
|
name: Tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.backend == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -65,8 +111,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test"
|
DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test"
|
||||||
|
|
||||||
audit:
|
rust-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.backend == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -81,7 +129,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build Check
|
name: Build Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [fmt, clippy, test]
|
needs: [frontend-linter, rust-fmt, rust-clippy, rust-test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ Full reference: [`example.env`](example.env) · [Deployment guide](doc/deploymen
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Server side
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build # Dev build
|
cargo build # Dev build
|
||||||
cargo run # Run locally
|
cargo run # Run locally
|
||||||
@@ -223,6 +225,16 @@ cargo fmt --all --check # Format check
|
|||||||
RUST_LOG=debug cargo run # Debug logging
|
RUST_LOG=debug cargo run # Debug logging
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Client side
|
||||||
|
|
||||||
|
Run server in dev profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROFILE=dev cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS & JS linter is `biome` (can be installed via `cargo install biome-cli` or on MacOS: `brew install biome`)
|
||||||
|
|
||||||
### Project stats
|
### Project stats
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"includes": ["static/**/*.js", "static/**/*.css", "static/**/*.json" ]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4,
|
||||||
|
"lineWidth": 160,
|
||||||
|
"lineEnding": "lf"
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedVariables": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4,
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"semicolons": "always",
|
||||||
|
"trailingCommas": "none",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"operatorLinebreak": "after"
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"linter": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable type checking on all JS files (equivalent to @ts-check globally)
|
||||||
|
"checkJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
// Treat all JS files as modules
|
||||||
|
"moduleDetection": "force"
|
||||||
|
},
|
||||||
|
"include": ["static/js/**/*.js"],
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -12,13 +12,16 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[dir='rtl'] .fa-arrow-left::before {
|
html[dir="rtl"] .fa-arrow-left::before {
|
||||||
content: "\f061";
|
content: "\f061";
|
||||||
}
|
}
|
||||||
|
|
||||||
html[dir='rtl'] .fa-sign-out-alt {
|
html[dir="rtl"] .fa-sign-out-alt {
|
||||||
-webkit-transform: rotate(180deg);
|
-webkit-transform: rotate(180deg);
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
/* Utility: hide elements without inline style="" (CSP-safe) */
|
/* 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-link {
|
.breadcrumb-link {
|
||||||
|
|||||||
@@ -46,12 +46,16 @@
|
|||||||
.files-grid-view .file-item.selected {
|
.files-grid-view .file-item.selected {
|
||||||
border-color: #ff5e3a;
|
border-color: #ff5e3a;
|
||||||
background: #fff8f6;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.files-grid-view .file-item.selected:hover {
|
.files-grid-view .file-item.selected:hover {
|
||||||
border-color: #ff5e3a;
|
border-color: #ff5e3a;
|
||||||
box-shadow: 0 0 0 1px rgba(255, 94, 58, 0.2), 0 6px 18px rgba(255, 94, 58, 0.15);
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 94, 58, 0.2),
|
||||||
|
0 6px 18px rgba(255, 94, 58, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* element hidden on grid view */
|
/* element hidden on grid view */
|
||||||
@@ -634,7 +638,8 @@
|
|||||||
left: 18px;
|
left: 18px;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
background: repeating-linear-gradient(to bottom, #0d904f 0px, #0d904f 1px, transparent 1px, transparent 11px),
|
background:
|
||||||
|
repeating-linear-gradient(to bottom, #0d904f 0px, #0d904f 1px, transparent 1px, transparent 11px),
|
||||||
repeating-linear-gradient(to right, #0d904f 0px, #0d904f 1px, transparent 1px, transparent 22px);
|
repeating-linear-gradient(to right, #0d904f 0px, #0d904f 1px, transparent 1px, transparent 22px);
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
box-shadow:
|
||||||
|
0 10px 36px rgba(0, 0, 0, 0.12),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Icon spacing ── */
|
/* ── Icon spacing ── */
|
||||||
.icon-mr { margin-right: 5px; }
|
.icon-mr {
|
||||||
.icon-ml { margin-left: 4px; font-size: 12px; }
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.icon-ml {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Success check icon ── */
|
/* ── Success check icon ── */
|
||||||
.check-icon { color: #48bb78; }
|
.check-icon {
|
||||||
|
color: #48bb78;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Move dialog ── */
|
/* ── Move dialog ── */
|
||||||
.move-dialog-hint {
|
.move-dialog-hint {
|
||||||
@@ -69,8 +76,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── About modal (userMenu.js profile popup) ── */
|
/* ── About modal (userMenu.js profile popup) ── */
|
||||||
.about-modal-body { max-width: 380px; }
|
.about-modal-body {
|
||||||
.about-modal-header { text-align: center; padding: 20px 20px 0; }
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
.about-modal-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
}
|
||||||
.about-modal-avatar {
|
.about-modal-avatar {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
@@ -84,8 +96,16 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.about-modal-username { margin: 0; font-size: 18px; color: #1a1a2e; }
|
.about-modal-username {
|
||||||
.about-modal-email { margin: 4px 0 0; font-size: 13px; color: #64748b; }
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.about-modal-email {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
.about-modal-role {
|
.about-modal-role {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -94,17 +114,27 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.about-modal-role-admin { background: #dbeafe; color: #1d4ed8; }
|
.about-modal-role-admin {
|
||||||
.about-modal-role-user { background: #f1f5f9; color: #64748b; }
|
background: #dbeafe;
|
||||||
.about-modal-storage { padding: 16px 20px; }
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
.about-modal-role-user {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.about-modal-storage {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
.about-modal-storage-label {
|
.about-modal-storage-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: .05em;
|
letter-spacing: 0.05em;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.about-modal-storage-label i { margin-right: 4px; }
|
.about-modal-storage-label i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
.about-modal-bar-bg {
|
.about-modal-bar-bg {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -115,10 +145,18 @@
|
|||||||
.about-modal-bar-fill {
|
.about-modal-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: width .3s;
|
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 {
|
.about-modal-close-btn {
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
@@ -128,23 +166,47 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .15s;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dark theme overrides ── */
|
/* ── Dark theme overrides ── */
|
||||||
[data-theme="dark"] .empty-state-icon { color: #475569; }
|
[data-theme="dark"] .empty-state-icon {
|
||||||
[data-theme="dark"] .empty-state-icon.error { color: #ef4444; }
|
color: #475569;
|
||||||
[data-theme="dark"] .about-modal-username { color: #f1f5f9; }
|
}
|
||||||
[data-theme="dark"] .about-modal-email { color: #94a3b8; }
|
[data-theme="dark"] .empty-state-icon.error {
|
||||||
[data-theme="dark"] .about-modal-role-admin { background: #1e3a5f; color: #60a5fa; }
|
color: #ef4444;
|
||||||
[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-username {
|
||||||
[data-theme="dark"] .about-modal-bar-bg { background: #334155; }
|
color: #f1f5f9;
|
||||||
[data-theme="dark"] .about-modal-bar-text { color: #94a3b8; }
|
}
|
||||||
|
[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 {
|
[data-theme="dark"] .about-modal-close-btn {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
border-color: #334155;
|
border-color: #334155;
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
[data-theme="dark"] .move-dialog-hint { color: #94a3b8; }
|
[data-theme="dark"] .move-dialog-hint {
|
||||||
[data-theme="dark"] .search-empty-text { color: #94a3b8; }
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .search-empty-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
/* Dialog animations */
|
/* Dialog animations */
|
||||||
@keyframes modalFadeIn {
|
@keyframes modalFadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalSlideIn {
|
@keyframes modalSlideIn {
|
||||||
from { transform: scale(0.95) translateY(-10px); opacity: 0; }
|
from {
|
||||||
to { transform: scale(1) translateY(0); opacity: 1; }
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialog (Rename / Move / Confirm) — Modern Style */
|
/* Dialog (Rename / Move / Confirm) — Modern Style */
|
||||||
|
|||||||
@@ -373,7 +373,6 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[data-theme="dark"] .files-list-view {
|
[data-theme="dark"] .files-list-view {
|
||||||
background-color: #1e293b;
|
background-color: #1e293b;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
@@ -431,4 +430,3 @@
|
|||||||
background: #334155;
|
background: #334155;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes oxi-spin {
|
@keyframes oxi-spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,9 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +177,7 @@
|
|||||||
margin-right: 14px;
|
margin-right: 14px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 14px;
|
margin-left: 14px;
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
@@ -210,7 +212,7 @@
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,12 @@
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
transition: opacity 0.2s, max-height 0.25s, transform 0.2s, margin 0.2s, padding 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
max-height 0.25s,
|
||||||
|
transform 0.2s,
|
||||||
|
margin 0.2s,
|
||||||
|
padding 0.2s;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +69,9 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-bar-close:hover {
|
.batch-bar-close:hover {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
border-right: 4px solid #ff5e3a;
|
border-right: 4px solid #ff5e3a;
|
||||||
right: unset;
|
right: unset;
|
||||||
@@ -48,7 +48,9 @@
|
|||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
transform: translateY(-100px);
|
transform: translateY(-100px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform 0.3s, opacity 0.3s;
|
transition:
|
||||||
|
transform 0.3s,
|
||||||
|
opacity 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-banner.active {
|
.notification-banner.active {
|
||||||
@@ -122,14 +124,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bellRing {
|
@keyframes bellRing {
|
||||||
0%, 100% { transform: rotate(0deg); }
|
0%,
|
||||||
13% { transform: rotate(22deg); }
|
100% {
|
||||||
26% { transform: rotate(-22deg); }
|
transform: rotate(0deg);
|
||||||
39% { transform: rotate(14deg); }
|
}
|
||||||
52% { transform: rotate(-14deg); }
|
13% {
|
||||||
65% { transform: rotate(8deg); }
|
transform: rotate(22deg);
|
||||||
78% { transform: rotate(-8deg); }
|
}
|
||||||
91% { transform: rotate(3deg); }
|
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 {
|
.notif-bell-btn.ring {
|
||||||
@@ -145,7 +164,9 @@
|
|||||||
max-height: 480px;
|
max-height: 480px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05);
|
box-shadow:
|
||||||
|
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: notifPanelIn 0.2s ease-out;
|
animation: notifPanelIn 0.2s ease-out;
|
||||||
@@ -157,8 +178,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes notifPanelIn {
|
@keyframes notifPanelIn {
|
||||||
from { opacity: 0; transform: translateY(-8px) scale(0.97); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notif-panel-header {
|
.notif-panel-header {
|
||||||
@@ -349,8 +376,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes uploadToastSlideIn {
|
@keyframes uploadToastSlideIn {
|
||||||
from { transform: translateY(20px); opacity: 0; }
|
from {
|
||||||
to { transform: translateY(0); opacity: 1; }
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-toast-header {
|
.upload-toast-header {
|
||||||
@@ -513,7 +546,9 @@
|
|||||||
|
|
||||||
[data-theme="dark"] .notif-panel {
|
[data-theme="dark"] .notif-panel {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06);
|
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 {
|
[data-theme="dark"] .notif-panel-header {
|
||||||
|
|||||||
@@ -51,7 +51,9 @@
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
box-shadow:
|
||||||
|
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: userMenuIn 0.2s ease-out;
|
animation: userMenuIn 0.2s ease-out;
|
||||||
|
|||||||
@@ -143,4 +143,3 @@
|
|||||||
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir='rtl'] .sidebar {
|
[dir="rtl"] .sidebar {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir='rtl'] .sidebar.open {
|
[dir="rtl"] .sidebar.open {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,9 +75,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
box-shadow: 0 3px 10px rgba(255, 94, 58, 0.35);
|
box-shadow: 0 3px 10px rgba(255, 94, 58, 0.35);
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
@@ -122,7 +124,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: 3px solid transparent;
|
border-right: 3px solid transparent;
|
||||||
}
|
}
|
||||||
@@ -139,7 +141,7 @@
|
|||||||
border-left-color: #ff5e3a;
|
border-left-color: #ff5e3a;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
border-right-color: #ff5e3a;
|
border-right-color: #ff5e3a;
|
||||||
}
|
}
|
||||||
@@ -152,29 +154,67 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: color 0.2s, transform 0.2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
transform 0.2s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 14px;
|
margin-left: 14px;
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored icons per section */
|
/* 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(1) i,
|
||||||
.nav-item:nth-child(2) i, .nav-item:nth-child(2) .oxi-icon { color: #74b9ff; } /* Shared - blue */
|
.nav-item:nth-child(1) .oxi-icon {
|
||||||
.nav-item:nth-child(3) i, .nav-item:nth-child(3) .oxi-icon { color: #81ecec; } /* Recent - teal */
|
color: #ffa94d;
|
||||||
.nav-item:nth-child(4) i, .nav-item:nth-child(4) .oxi-icon { color: #ffd43b; } /* Favorites - gold */
|
} /* Files - orange */
|
||||||
.nav-item:nth-child(5) i, .nav-item:nth-child(5) .oxi-icon { color: #ff6b9d; } /* Photos - pink */
|
.nav-item:nth-child(2) i,
|
||||||
.nav-item:nth-child(6) i, .nav-item:nth-child(6) .oxi-icon { color: #ff7675; } /* Trash - red */
|
.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(1) i,
|
||||||
.nav-item.active:nth-child(2) i, .nav-item.active:nth-child(2) .oxi-icon { color: #0984e3; }
|
.nav-item.active:nth-child(1) .oxi-icon {
|
||||||
.nav-item.active:nth-child(3) i, .nav-item.active:nth-child(3) .oxi-icon { color: #00cec9; }
|
color: #ff5e3a;
|
||||||
.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(2) i,
|
||||||
.nav-item.active:nth-child(6) i, .nav-item.active:nth-child(6) .oxi-icon { color: #e74c3c; }
|
.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 i,
|
||||||
.nav-item:hover .oxi-icon {
|
.nav-item:hover .oxi-icon {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
@@ -108,7 +110,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
left: unset;
|
left: unset;
|
||||||
}
|
}
|
||||||
@@ -137,7 +139,7 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
left: 6px;
|
left: 6px;
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
|
|||||||
+1132
-240
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,8 +148,14 @@
|
|||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-subtitle { margin: 20px 0; color: var(--color-text-muted); font-size: 14px; }
|
.auth-subtitle {
|
||||||
.auth-action-wrap { margin-top: 20px; }
|
margin: 20px 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.auth-action-wrap {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* SSO / OIDC button */
|
/* SSO / OIDC button */
|
||||||
.auth-button-oidc {
|
.auth-button-oidc {
|
||||||
@@ -188,7 +194,7 @@
|
|||||||
|
|
||||||
.auth-divider::before,
|
.auth-divider::before,
|
||||||
.auth-divider::after {
|
.auth-divider::after {
|
||||||
content: '';
|
content: "";
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--color-border);
|
background: var(--color-border);
|
||||||
@@ -379,8 +385,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes langPickerSlideDown {
|
@keyframes langPickerSlideDown {
|
||||||
from { opacity: 0; transform: translateY(-4px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search inside dropdown */
|
/* Search inside dropdown */
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
--border: #e2e8f0;
|
--border: #e2e8f0;
|
||||||
--radius: 12px;
|
--radius: 12px;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -31,12 +35,32 @@ body {
|
|||||||
max-width: 440px;
|
max-width: 440px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.logo { text-align: center; margin-bottom: 1.5rem; }
|
.logo {
|
||||||
.logo h1 { font-size: 1.5rem; font-weight: 700; }
|
text-align: center;
|
||||||
.logo span { color: var(--primary); }
|
margin-bottom: 1.5rem;
|
||||||
h2 { font-size: 1.15rem; margin-bottom: 0.5rem; }
|
}
|
||||||
p.subtitle { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
.logo h1 {
|
||||||
label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 0.4rem; }
|
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"] {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
@@ -49,17 +73,33 @@ input[type="text"] {
|
|||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
input[type="text"]:focus { border-color: var(--primary); }
|
input[type="text"]:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
.device-info {
|
.device-info {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
.device-info .row { display: flex; justify-content: space-between; margin-bottom: 0.3rem; }
|
.device-info .row {
|
||||||
.device-info .label { color: var(--muted); font-size: 0.85rem; }
|
display: flex;
|
||||||
.device-info .value { font-weight: 600; font-size: 0.85rem; }
|
justify-content: space-between;
|
||||||
.actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }
|
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 {
|
button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -70,11 +110,24 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
.btn-approve { background: var(--primary); color: #fff; }
|
.btn-approve {
|
||||||
.btn-approve:hover { background: var(--primary-hover); }
|
background: var(--primary);
|
||||||
.btn-deny { background: var(--danger); color: #fff; }
|
color: #fff;
|
||||||
.btn-deny:hover { background: var(--danger-hover); }
|
}
|
||||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
.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 {
|
.status {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -82,8 +135,23 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.status.success { background: #dcfce7; color: var(--success); }
|
.status.success {
|
||||||
.status.denied { background: #fef2f2; color: var(--danger); }
|
background: #dcfce7;
|
||||||
.status.error { background: #fef2f2; color: var(--danger); }
|
color: var(--success);
|
||||||
.error-text { color: var(--danger); font-size: 0.85rem; margin-top: 0.5rem; }
|
}
|
||||||
.hidden { display: none !important; }
|
.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;
|
z-index: 5;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
left: 10px;
|
left: 10px;
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
.file-item.favorite-item {
|
.file-item.favorite-item {
|
||||||
border-left: 3px solid #ffc107;
|
border-left: 3px solid #ffc107;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
border-right: 3px solid #ffc107;
|
border-right: 3px solid #ffc107;
|
||||||
border-left: unset;
|
border-left: unset;
|
||||||
}
|
}
|
||||||
@@ -89,9 +89,15 @@
|
|||||||
|
|
||||||
/* Animation for the favorite star */
|
/* Animation for the favorite star */
|
||||||
@keyframes favorite-pulse {
|
@keyframes favorite-pulse {
|
||||||
0% { transform: scale(1); }
|
0% {
|
||||||
50% { transform: scale(1.2); }
|
transform: scale(1);
|
||||||
100% { transform: scale(1); }
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.favorite-indicator.active {
|
.favorite-indicator.active {
|
||||||
@@ -119,7 +125,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
filter: drop-shadow(0 0 1px rgba(255, 193, 7, 0.4));
|
filter: drop-shadow(0 0 1px rgba(255, 193, 7, 0.4));
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme support */
|
/* Dark theme support */
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
font-family: 'Courier New', Consolas, Monaco, monospace;
|
font-family: "Courier New", Consolas, Monaco, monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #2d3748;
|
color: #2d3748;
|
||||||
@@ -290,8 +290,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes audio-pulse {
|
@keyframes audio-pulse {
|
||||||
0%, 100% { opacity: 0.6; transform: scale(1); }
|
0%,
|
||||||
50% { opacity: 1; transform: scale(1.05); }
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-viewer-audio-name {
|
.inline-viewer-audio-name {
|
||||||
|
|||||||
@@ -80,7 +80,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
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;
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-tile img {
|
.photo-tile img {
|
||||||
@@ -134,7 +137,9 @@
|
|||||||
.photo-tile.selected {
|
.photo-tile.selected {
|
||||||
border-color: #ff5e3a;
|
border-color: #ff5e3a;
|
||||||
background: #fff8f6;
|
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 {
|
.photo-tile.selected img {
|
||||||
@@ -221,7 +226,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selection bar */
|
/* Selection bar */
|
||||||
|
|||||||
+948
-211
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
left: 10px;
|
left: 10px;
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
.files-grid-view .file-item.recent-item {
|
.files-grid-view .file-item.recent-item {
|
||||||
border-left: 3px solid #6c757d;
|
border-left: 3px solid #6c757d;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir="rtl"] & {
|
||||||
border-right: 3px solid #6c757d;
|
border-right: 3px solid #6c757d;
|
||||||
border-left: unset;
|
border-left: unset;
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes recent-fade-in {
|
@keyframes recent-fade-in {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ async function refreshUserData() {
|
|||||||
|
|
||||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||||
window.updateStorageUsageDisplay(userData);
|
window.updateStorageUsageDisplay(userData);
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing user data:', error);
|
console.error('Error refreshing user data:', error);
|
||||||
@@ -82,7 +81,7 @@ async function checkAuthentication() {
|
|||||||
if (userData.username) {
|
if (userData.username) {
|
||||||
// We have cached user data — render immediately, refresh in background
|
// We have cached user data — render immediately, refresh in background
|
||||||
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
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;
|
el.textContent = userInitials;
|
||||||
});
|
});
|
||||||
const menuName = document.getElementById('user-menu-name');
|
const menuName = document.getElementById('user-menu-name');
|
||||||
@@ -103,7 +102,10 @@ async function checkAuthentication() {
|
|||||||
const r = await fetch('/api/auth/refresh', {
|
const r = await fetch('/api/auth/refresh', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...getCsrfHeaders()
|
||||||
|
},
|
||||||
body: '{}'
|
body: '{}'
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@@ -113,7 +115,7 @@ async function checkAuthentication() {
|
|||||||
window.location.href = '/login?source=session_expired';
|
window.location.href = '/login?source=session_expired';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
localStorage.removeItem(USER_DATA_KEY);
|
localStorage.removeItem(USER_DATA_KEY);
|
||||||
window.location.href = '/login?source=session_expired';
|
window.location.href = '/login?source=session_expired';
|
||||||
return;
|
return;
|
||||||
@@ -126,9 +128,11 @@ async function checkAuthentication() {
|
|||||||
console.log('No cached user data, fetching from server');
|
console.log('No cached user data, fetching from server');
|
||||||
try {
|
try {
|
||||||
const freshData = await refreshUserData();
|
const freshData = await refreshUserData();
|
||||||
if (freshData && freshData.username) {
|
if (freshData?.username) {
|
||||||
const userInitials = freshData.username.substring(0, 2).toUpperCase();
|
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);
|
window.updateStorageUsageDisplay(freshData);
|
||||||
resolveHomeFolder().then(() => window.loadFiles());
|
resolveHomeFolder().then(() => window.loadFiles());
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +158,9 @@ async function resolveHomeFolder() {
|
|||||||
|
|
||||||
if (app.userHomeFolderId) return;
|
if (app.userHomeFolderId) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/folders', { credentials: 'same-origin' });
|
const response = await fetch('/api/folders', {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(`Could not fetch home folder: ${response.status}`);
|
console.warn(`Could not fetch home folder: ${response.status}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+21
-23
@@ -22,11 +22,10 @@
|
|||||||
* @returns {Promise<FolderInfo>}
|
* @returns {Promise<FolderInfo>}
|
||||||
*/
|
*/
|
||||||
async function getFolder(id) {
|
async function getFolder(id) {
|
||||||
|
|
||||||
/** @type {HeadersInit} */
|
/** @type {HeadersInit} */
|
||||||
const headers = {
|
const headers = {
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Pragma': 'no-cache'
|
Pragma: 'no-cache'
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {RequestInit} */
|
/** @type {RequestInit} */
|
||||||
@@ -36,11 +35,10 @@ async function getFolder( id) {
|
|||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
};
|
};
|
||||||
|
|
||||||
let folderInformations = await fetch( `/api/folders/${id}`, requestOptions);
|
const folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
|
||||||
if (folderInformations.ok) {
|
if (folderInformations.ok) {
|
||||||
return folderInformations.json()
|
return folderInformations.json();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.warn(`Error fetching folder ${id}`);
|
console.warn(`Error fetching folder ${id}`);
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
}
|
}
|
||||||
@@ -50,8 +48,8 @@ async function getFolder( id) {
|
|||||||
* rebuild breadcrumb from selected folder (iterate up to root)
|
* rebuild breadcrumb from selected folder (iterate up to root)
|
||||||
*/
|
*/
|
||||||
async function rebuildBreadCrumb() {
|
async function rebuildBreadCrumb() {
|
||||||
|
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the leaf (this is the current displayed folder)
|
* Store the leaf (this is the current displayed folder)
|
||||||
* @type {FolderInfo | null}
|
* @type {FolderInfo | null}
|
||||||
@@ -69,7 +67,7 @@ async function rebuildBreadCrumb() {
|
|||||||
while (id !== null) {
|
while (id !== null) {
|
||||||
console.log(`fetching folder information for folder ${id}`);
|
console.log(`fetching folder information for folder ${id}`);
|
||||||
try {
|
try {
|
||||||
let folderInfo = await getFolder(id);
|
const folderInfo = await getFolder(id);
|
||||||
|
|
||||||
// store the Leaf which is the current folder
|
// store the Leaf which is the current folder
|
||||||
if (currentFolderInfo === null) {
|
if (currentFolderInfo === null) {
|
||||||
@@ -78,13 +76,15 @@ async function rebuildBreadCrumb() {
|
|||||||
|
|
||||||
// XXX do not enter root into bread crumb updateBreadcrumb() method always display it
|
// XXX do not enter root into bread crumb updateBreadcrumb() method always display it
|
||||||
if (!folderInfo.is_root) {
|
if (!folderInfo.is_root) {
|
||||||
app.breadcrumbPath.unshift( {id: folderInfo.id, name: folderInfo.name});
|
app.breadcrumbPath.unshift({
|
||||||
|
id: folderInfo.id,
|
||||||
|
name: folderInfo.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate to parent folder
|
// iterate to parent folder
|
||||||
id = folderInfo.parent_id;
|
id = folderInfo.parent_id;
|
||||||
}
|
} catch (_e) {
|
||||||
catch( e) {
|
|
||||||
console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`);
|
console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`);
|
||||||
// fallback of root
|
// fallback of root
|
||||||
window.uiNotifications.show(
|
window.uiNotifications.show(
|
||||||
@@ -96,6 +96,7 @@ async function rebuildBreadCrumb() {
|
|||||||
app.currentPath = id;
|
app.currentPath = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// store informations on the current folder
|
// store informations on the current folder
|
||||||
app.currentFolderInfo = currentFolderInfo;
|
app.currentFolderInfo = currentFolderInfo;
|
||||||
}
|
}
|
||||||
@@ -109,36 +110,35 @@ async function rebuildBreadCrumb() {
|
|||||||
*/
|
*/
|
||||||
async function loadFiles(options = { insertHistory: true }) {
|
async function loadFiles(options = { insertHistory: true }) {
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
const elements = window.appElements;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Starting loadFiles() - loading files...", options);
|
console.log('Starting loadFiles() - loading files...', options);
|
||||||
|
|
||||||
const forceRefresh = options.forceRefresh || false;
|
const forceRefresh = options.forceRefresh || false;
|
||||||
|
|
||||||
if (window.isLoadingFiles) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.isLoadingFiles = true;
|
window.isLoadingFiles = true;
|
||||||
|
|
||||||
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
|
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
|
||||||
let loadingFiles = setTimeout( () => {
|
const loadingFiles = setTimeout(() => {
|
||||||
// display loader after few delay (will be canceled if result take less time)
|
// display loader after few delay (will be canceled if result take less time)
|
||||||
window.ui.showError(`
|
window.ui.showError(`
|
||||||
<div class="files-loading-spinner">
|
<div class="files-loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
if (!app.userHomeFolderId) {
|
if (!app.userHomeFolderId) {
|
||||||
await window.resolveHomeFolder();
|
await window.resolveHomeFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
await rebuildBreadCrumb();
|
await rebuildBreadCrumb();
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
|||||||
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
||||||
} else {
|
} else {
|
||||||
url = `/api/folders?t=${timestamp}`;
|
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 {
|
} else {
|
||||||
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
|
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
|
||||||
@@ -168,7 +168,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
|||||||
/** @type {HeadersInit} */
|
/** @type {HeadersInit} */
|
||||||
const headers = {
|
const headers = {
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Pragma': 'no-cache'
|
Pragma: 'no-cache'
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {RequestInit} */
|
/** @type {RequestInit} */
|
||||||
@@ -180,7 +180,6 @@ async function loadFiles(options = { insertHistory: true}) {
|
|||||||
|
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
url += `&force_refresh=true`;
|
url += `&force_refresh=true`;
|
||||||
// @ts-ignore
|
|
||||||
requestOptions.headers['X-Force-Refresh'] = 'true';
|
requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||||
console.log('Forcing complete refresh ignoring cache');
|
console.log('Forcing complete refresh ignoring cache');
|
||||||
}
|
}
|
||||||
@@ -192,7 +191,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
|||||||
clearTimeout(loadingFiles);
|
clearTimeout(loadingFiles);
|
||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
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
|
// FIXME: i18n
|
||||||
window.ui.showError(`<p>Could not load files</p>`);
|
window.ui.showError(`<p>Could not load files</p>`);
|
||||||
return;
|
return;
|
||||||
@@ -235,8 +234,7 @@ async function loadFiles(options = { insertHistory: true}) {
|
|||||||
if (fileFound) {
|
if (fileFound) {
|
||||||
console.log(`file ${window.app.viewFile} found, calling viewer`);
|
console.log(`file ${window.app.viewFile} found, calling viewer`);
|
||||||
await window.inlineViewer.openFile(fileFound);
|
await window.inlineViewer.openFile(fileFound);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// remove file
|
// remove file
|
||||||
console.log(`file ${window.app.viewFile} not found`);
|
console.log(`file ${window.app.viewFile} not found`);
|
||||||
window.app.viewFile = null;
|
window.app.viewFile = null;
|
||||||
|
|||||||
+54
-55
@@ -3,8 +3,6 @@
|
|||||||
* This file contains the core functionality, initialization and state management
|
* This file contains the core functionality, initialization and state management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
const elements = window.appElements;
|
const elements = window.appElements;
|
||||||
|
|
||||||
@@ -105,9 +103,9 @@ function setActionsBarMode(mode, force = false) {
|
|||||||
if (!elements.actionsBar) return;
|
if (!elements.actionsBar) return;
|
||||||
|
|
||||||
if (mode === 'hidden') {
|
if (mode === 'hidden') {
|
||||||
elements.actionsBar.classList.add("hidden");
|
elements.actionsBar.classList.add('hidden');
|
||||||
elements.actionsBar.dataset.mode = 'hidden';
|
elements.actionsBar.dataset.mode = 'hidden';
|
||||||
console.log("......setup actions bar to hidden");
|
console.log('......setup actions bar to hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ function setActionsBarMode(mode, force = false) {
|
|||||||
if (!html) return;
|
if (!html) return;
|
||||||
|
|
||||||
elements.actionsBar.innerHTML = html;
|
elements.actionsBar.innerHTML = html;
|
||||||
elements.actionsBar.classList.remove("hidden");
|
elements.actionsBar.classList.remove('hidden');
|
||||||
elements.actionsBar.dataset.mode = mode;
|
elements.actionsBar.dataset.mode = mode;
|
||||||
|
|
||||||
// Refresh cached action elements after rebuild
|
// Refresh cached action elements after rebuild
|
||||||
@@ -128,7 +126,7 @@ function setActionsBarMode(mode, force = false) {
|
|||||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
elements.listViewBtn = document.getElementById('list-view-btn');
|
elements.listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(elements.actionsBar);
|
window.i18n.translateElement(elements.actionsBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,24 +215,24 @@ function setupActionsBarDelegation() {
|
|||||||
* @returns {OxiContext}
|
* @returns {OxiContext}
|
||||||
*/
|
*/
|
||||||
function deserializeHash() {
|
function deserializeHash() {
|
||||||
let hashContext = /** type {OxiContext} */ {};
|
const hashContext = /** type {OxiContext} */ {};
|
||||||
|
|
||||||
// FIXME rename files into drive ?
|
// FIXME rename files into drive ?
|
||||||
hashContext.section = 'files';
|
hashContext.section = 'files';
|
||||||
|
|
||||||
let hash_elements = window.location.hash.split("/");
|
const hash_elements = window.location.hash.split('/');
|
||||||
|
|
||||||
let section = hash_elements[1];
|
const section = hash_elements[1];
|
||||||
|
|
||||||
// FIXME: use navigation.VIEW_FLAGS
|
// FIXME: use navigation.VIEW_FLAGS
|
||||||
if (section && ['files', 'shared', 'recent', 'favorites', 'trash', 'photos'].includes(section)) {
|
if (section && ['files', 'shared', 'recent', 'favorites', 'trash', 'photos'].includes(section)) {
|
||||||
hashContext.section = section;
|
hashContext.section = section;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash_elements[1] == 'files' && hash_elements[2] == 'folder' && hash_elements[3] !== null) {
|
if (hash_elements[1] === 'files' && hash_elements[2] === 'folder' && hash_elements[3] !== null) {
|
||||||
hashContext.path = hash_elements[3];
|
hashContext.path = hash_elements[3];
|
||||||
|
|
||||||
if (hash_elements[4] == 'file' && hash_elements[5] !== null) {
|
if (hash_elements[4] === 'file' && hash_elements[5] !== null) {
|
||||||
hashContext.file = hash_elements[5];
|
hashContext.file = hash_elements[5];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,10 +248,10 @@ function deserializeHash() {
|
|||||||
function updateHistory(insertHistory) {
|
function updateHistory(insertHistory) {
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
|
|
||||||
let historyData = {
|
const historyData = {
|
||||||
section: app.currentSection,
|
section: app.currentSection,
|
||||||
id: app.currentFolder,
|
id: app.currentFolder,
|
||||||
file: app.viewFile,
|
file: app.viewFile
|
||||||
};
|
};
|
||||||
|
|
||||||
let historyUrl = `#/${app.currentSection}`;
|
let historyUrl = `#/${app.currentSection}`;
|
||||||
@@ -270,15 +268,12 @@ function updateHistory( insertHistory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (insertHistory) {
|
if (insertHistory) {
|
||||||
console.log(`adding history with ${historyUrl}`)
|
console.log(`adding history with ${historyUrl}`);
|
||||||
window.history.pushState(historyData, "", historyUrl);
|
window.history.pushState(historyData, '', historyUrl);
|
||||||
|
} else {
|
||||||
|
console.log(`replace history with ${historyUrl}`);
|
||||||
|
window.history.replaceState(historyData, '', historyUrl);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
console.log(`replace history with ${historyUrl}`)
|
|
||||||
window.history.replaceState(historyData, "", historyUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,32 +282,32 @@ function updateHistory( insertHistory) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function switchSectionTo(section) {
|
function switchSectionTo(section) {
|
||||||
|
|
||||||
if (window.app.currentSection === section)
|
if (window.app.currentSection === section)
|
||||||
// no change ...
|
// no change ...
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "files":
|
case 'files':
|
||||||
switchToFilesSection();
|
switchToFilesSection();
|
||||||
|
break;
|
||||||
|
|
||||||
case "shared":
|
case 'shared':
|
||||||
switchToSharedSection();
|
switchToSharedSection();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "recent":
|
case 'recent':
|
||||||
switchToRecentFilesSection();
|
switchToRecentFilesSection();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "favorites":
|
case 'favorites':
|
||||||
switchToFavoritesSection();
|
switchToFavoritesSection();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "photos":
|
case 'photos':
|
||||||
switchToPhotosSection();
|
switchToPhotosSection();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "trash":
|
case 'trash':
|
||||||
switchToTrashSection();
|
switchToTrashSection();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -330,7 +325,7 @@ function initApp() {
|
|||||||
cacheElements();
|
cacheElements();
|
||||||
|
|
||||||
// Initialize file sharing module first
|
// Initialize file sharing module first
|
||||||
if (window.fileSharing && window.fileSharing.init) {
|
if (window.fileSharing?.init) {
|
||||||
window.fileSharing.init();
|
window.fileSharing.init();
|
||||||
} else {
|
} else {
|
||||||
console.warn('fileSharing module not fully initialized');
|
console.warn('fileSharing module not fully initialized');
|
||||||
@@ -354,7 +349,7 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize favorites module if available
|
// Initialize favorites module if available
|
||||||
if (window.favorites && window.favorites.init) {
|
if (window.favorites?.init) {
|
||||||
console.log('Initializing favorites module');
|
console.log('Initializing favorites module');
|
||||||
window.favorites.init();
|
window.favorites.init();
|
||||||
} else {
|
} else {
|
||||||
@@ -362,7 +357,7 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize recent files module if available
|
// Initialize recent files module if available
|
||||||
if (window.recent && window.recent.init) {
|
if (window.recent?.init) {
|
||||||
console.log('Initializing recent files module');
|
console.log('Initializing recent files module');
|
||||||
window.recent.init();
|
window.recent.init();
|
||||||
} else {
|
} else {
|
||||||
@@ -370,16 +365,16 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize multi-select / batch actions
|
// Initialize multi-select / batch actions
|
||||||
if (window.multiSelect && window.multiSelect.init) {
|
if (window.multiSelect?.init) {
|
||||||
console.log('Initializing multi-select module');
|
console.log('Initializing multi-select module');
|
||||||
window.multiSelect.init();
|
window.multiSelect.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('authenticationDone', () => {
|
window.addEventListener('authenticationDone', () => {
|
||||||
// Check if a context was provided in the URL
|
// Check if a context was provided in the URL
|
||||||
let hashContext = deserializeHash();
|
const hashContext = deserializeHash();
|
||||||
switchSectionTo(hashContext.section);
|
switchSectionTo(hashContext.section);
|
||||||
if (hashContext.section === "files") {
|
if (hashContext.section === 'files') {
|
||||||
if (hashContext.path) {
|
if (hashContext.path) {
|
||||||
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
|
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
|
||||||
window.app.currentPath = hashContext.path;
|
window.app.currentPath = hashContext.path;
|
||||||
@@ -390,11 +385,10 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for translations to load before checking authentication
|
// Wait for translations to load before checking authentication
|
||||||
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
|
if (window.i18n?.isLoaded?.()) {
|
||||||
// Translations already loaded, proceed with authentication
|
// Translations already loaded, proceed with authentication
|
||||||
window.checkAuthentication();
|
window.checkAuthentication();
|
||||||
} else {
|
} else {
|
||||||
@@ -407,7 +401,7 @@ function initApp() {
|
|||||||
|
|
||||||
// Set a timeout as a fallback in case translations take too long
|
// Set a timeout as a fallback in case translations take too long
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!window.i18n || !window.i18n.isLoaded || !window.i18n.isLoaded()) {
|
if (!window.i18n?.isLoaded?.()) {
|
||||||
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
||||||
window.checkAuthentication();
|
window.checkAuthentication();
|
||||||
}
|
}
|
||||||
@@ -451,27 +445,33 @@ function setupUploadDropdown() {
|
|||||||
const signal = uploadDropdownBindingsController.signal;
|
const signal = uploadDropdownBindingsController.signal;
|
||||||
|
|
||||||
// Toggle dropdown on button click
|
// Toggle dropdown on button click
|
||||||
uploadBtn.addEventListener('click', (e) => {
|
uploadBtn.addEventListener(
|
||||||
|
'click',
|
||||||
|
(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const isOpen = menu.classList.contains('show');
|
const isOpen = menu.classList.contains('show');
|
||||||
// Close any other open dropdowns
|
// Close any other open dropdowns
|
||||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => {
|
||||||
|
m.classList.remove('show');
|
||||||
|
});
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
menu.classList.add('show');
|
menu.classList.add('show');
|
||||||
}
|
}
|
||||||
}, { signal });
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
// remove+add stable handler: guarantees exactly one global listener
|
// remove+add stable handler: guarantees exactly one global listener
|
||||||
if (uploadDropdownDocumentClickHandler) {
|
if (uploadDropdownDocumentClickHandler) {
|
||||||
// @ts-ignore
|
|
||||||
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
||||||
}
|
}
|
||||||
uploadDropdownDocumentClickHandler = (e) => {
|
uploadDropdownDocumentClickHandler = (e) => {
|
||||||
if (e.target.closest('#upload-dropdown')) return;
|
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);
|
document.addEventListener('click', uploadDropdownDocumentClickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,17 +488,16 @@ function setupEventListeners() {
|
|||||||
const SEARCH_MIN_CHARS = 3;
|
const SEARCH_MIN_CHARS = 3;
|
||||||
|
|
||||||
// handle history / url change
|
// handle history / url change
|
||||||
window.addEventListener("popstate", (e) => {
|
window.addEventListener('popstate', (e) => {
|
||||||
if (e.state === null) {
|
if (e.state === null) {
|
||||||
// change is from user (url explicitely change, read information from hash)
|
// change is from user (url explicitely change, read information from hash)
|
||||||
let hashContext = deserializeHash();
|
const hashContext = deserializeHash();
|
||||||
switchSectionTo(hashContext.section);
|
switchSectionTo(hashContext.section);
|
||||||
if (hashContext.path) {
|
if (hashContext.path) {
|
||||||
window.app.currentPath = 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
|
// change is from history, data provided in event
|
||||||
switchSectionTo(e.state.section);
|
switchSectionTo(e.state.section);
|
||||||
window.app.currentPath = e.state.id;
|
window.app.currentPath = e.state.id;
|
||||||
@@ -587,16 +586,18 @@ function setupEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar navigation
|
// Sidebar navigation
|
||||||
elements.navItems.forEach(item => {
|
elements.navItems.forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
// Remove active class from all nav items
|
// 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
|
// Add active class to clicked item
|
||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
let _updateHistory = true;
|
let _updateHistory = true;
|
||||||
|
|
||||||
let itemI18nKey=item.querySelector('span').getAttribute('data-i18n')
|
const itemI18nKey = item.querySelector('span').getAttribute('data-i18n');
|
||||||
switch (itemI18nKey) {
|
switch (itemI18nKey) {
|
||||||
case 'nav.shared':
|
case 'nav.shared':
|
||||||
// Switch to shared view
|
// Switch to shared view
|
||||||
@@ -652,13 +653,11 @@ function setupEventListeners() {
|
|||||||
const folderMenu = document.getElementById('folder-context-menu');
|
const folderMenu = document.getElementById('folder-context-menu');
|
||||||
const fileMenu = document.getElementById('file-context-menu');
|
const fileMenu = document.getElementById('file-context-menu');
|
||||||
|
|
||||||
if (folderMenu && folderMenu.style.display === 'block' &&
|
if (folderMenu && folderMenu.style.display === 'block' && !folderMenu.contains(e.target)) {
|
||||||
!folderMenu.contains(e.target)) {
|
|
||||||
ui.closeContextMenu();
|
ui.closeContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileMenu && fileMenu.style.display === 'block' &&
|
if (fileMenu && fileMenu.style.display === 'block' && !fileMenu.contains(e.target)) {
|
||||||
!fileMenu.contains(e.target)) {
|
|
||||||
ui.closeFileContextMenu();
|
ui.closeFileContextMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -717,7 +716,7 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
storageInfo.removeAttribute('data-i18n');
|
storageInfo.removeAttribute('data-i18n');
|
||||||
|
|
||||||
// Use i18n if available
|
// Use i18n if available
|
||||||
if (window.i18n && window.i18n.t) {
|
if (window.i18n?.t) {
|
||||||
storageInfo.textContent = window.i18n.t('storage.used', {
|
storageInfo.textContent = window.i18n.t('storage.used', {
|
||||||
percentage: usagePercentage,
|
percentage: usagePercentage,
|
||||||
used: usedFormatted,
|
used: usedFormatted,
|
||||||
|
|||||||
+23
-31
@@ -14,15 +14,14 @@ function syncViewContainers() {
|
|||||||
|
|
||||||
const isGrid = window.app.currentView === 'grid';
|
const isGrid = window.app.currentView === 'grid';
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
filesList.classList.remove("files-list-view");
|
filesList.classList.remove('files-list-view');
|
||||||
filesList.classList.add("files-grid-view");
|
filesList.classList.add('files-grid-view');
|
||||||
|
|
||||||
gridViewBtn?.classList.add('active');
|
gridViewBtn?.classList.add('active');
|
||||||
listViewBtn?.classList.remove('active');
|
listViewBtn?.classList.remove('active');
|
||||||
}
|
} else {
|
||||||
else {
|
filesList.classList.add('files-list-view');
|
||||||
filesList.classList.add("files-list-view");
|
filesList.classList.remove('files-grid-view');
|
||||||
filesList.classList.remove("files-grid-view");
|
|
||||||
|
|
||||||
gridViewBtn?.classList.remove('active');
|
gridViewBtn?.classList.remove('active');
|
||||||
listViewBtn?.classList.add('active');
|
listViewBtn?.classList.add('active');
|
||||||
@@ -83,7 +82,7 @@ function initSidebarToggle() {
|
|||||||
|
|
||||||
// Close sidebar when navigating (nav item click on mobile)
|
// Close sidebar when navigating (nav item click on mobile)
|
||||||
const navItems = sidebar.querySelectorAll('.nav-item');
|
const navItems = sidebar.querySelectorAll('.nav-item');
|
||||||
navItems.forEach(item => {
|
navItems.forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
if (window.innerWidth <= 768) {
|
if (window.innerWidth <= 768) {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
@@ -104,12 +103,12 @@ document.addEventListener('DOMContentLoaded', initSidebarToggle);
|
|||||||
|
|
||||||
// Mapping of section names to their corresponding view flags
|
// Mapping of section names to their corresponding view flags
|
||||||
const VIEW_FLAGS = {
|
const VIEW_FLAGS = {
|
||||||
'files': 'isFilesView',
|
files: 'isFilesView',
|
||||||
'shared': 'isSharedView',
|
shared: 'isSharedView',
|
||||||
'recent': 'isRecentView',
|
recent: 'isRecentView',
|
||||||
'favorites': 'isFavoritesView',
|
favorites: 'isFavoritesView',
|
||||||
'trash': 'isTrashView',
|
trash: 'isTrashView',
|
||||||
'photos': 'isPhotosView'
|
photos: 'isPhotosView'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,18 +127,17 @@ function getSectionFromNavItem(navItem) {
|
|||||||
* @returns {boolean} true if the section changed
|
* @returns {boolean} true if the section changed
|
||||||
*/
|
*/
|
||||||
function setCurrentSection(section) {
|
function setCurrentSection(section) {
|
||||||
|
if (window.app.currentSection === section) return false;
|
||||||
if (window.app.currentSection == section) return false;
|
|
||||||
|
|
||||||
// Set all view flags - true for active section, false for others
|
// Set all view flags - true for active section, false for others
|
||||||
Object.entries(VIEW_FLAGS).forEach(([key, flag]) => {
|
Object.entries(VIEW_FLAGS).forEach(([key, flag]) => {
|
||||||
window.app[flag] = (key === section);
|
window.app[flag] = key === section;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.app.currentSection = section;
|
window.app.currentSection = section;
|
||||||
|
|
||||||
// Update nav item active classes by finding matching item from DOM
|
// 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);
|
const itemSection = getSectionFromNavItem(item);
|
||||||
item.classList.toggle('active', itemSection === section);
|
item.classList.toggle('active', itemSection === section);
|
||||||
});
|
});
|
||||||
@@ -164,12 +162,11 @@ function setCurrentSection(section) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchToSharedSection() {
|
function switchToSharedSection() {
|
||||||
|
|
||||||
if (!setCurrentSection('shared')) return;
|
if (!setCurrentSection('shared')) return;
|
||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.add("hidden");
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// Hide actions-bar for shared view
|
// Hide actions-bar for shared view
|
||||||
window.setActionsBarMode('hidden');
|
window.setActionsBarMode('hidden');
|
||||||
@@ -185,7 +182,6 @@ function switchToSharedSection() {
|
|||||||
window.sharedView.show();
|
window.sharedView.show();
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToFilesSection() {
|
function switchToFilesSection() {
|
||||||
@@ -196,7 +192,7 @@ function switchToFilesSection() {
|
|||||||
|
|
||||||
// Show breadcrumb (only in Files view)
|
// Show breadcrumb (only in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.remove("hidden");
|
breadcrumb?.classList.remove('hidden');
|
||||||
|
|
||||||
// show files container
|
// show files container
|
||||||
toggleFileContainer(true);
|
toggleFileContainer(true);
|
||||||
@@ -224,7 +220,7 @@ function switchToFavoritesSection() {
|
|||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.add("hidden");
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// show files container
|
// show files container
|
||||||
toggleFileContainer(true);
|
toggleFileContainer(true);
|
||||||
@@ -242,12 +238,10 @@ function switchToFavoritesSection() {
|
|||||||
window.ui.showError(`
|
window.ui.showError(`
|
||||||
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
||||||
<p>Error loading the favorites module</p>
|
<p>Error loading the favorites module</p>
|
||||||
`
|
`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToRecentFilesSection() {
|
function switchToRecentFilesSection() {
|
||||||
@@ -258,7 +252,7 @@ function switchToRecentFilesSection() {
|
|||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.add("hidden");
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// show files container
|
// show files container
|
||||||
toggleFileContainer(true);
|
toggleFileContainer(true);
|
||||||
@@ -276,8 +270,7 @@ function switchToRecentFilesSection() {
|
|||||||
window.ui.showError(`
|
window.ui.showError(`
|
||||||
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
||||||
<p>Error loading the recent module</p>
|
<p>Error loading the recent module</p>
|
||||||
`
|
`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
}
|
}
|
||||||
@@ -287,7 +280,7 @@ function switchToPhotosSection() {
|
|||||||
|
|
||||||
// Hide breadcrumb
|
// Hide breadcrumb
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.add("hidden");
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// Hide file containers
|
// Hide file containers
|
||||||
toggleFileContainer(false);
|
toggleFileContainer(false);
|
||||||
@@ -303,7 +296,6 @@ function switchToPhotosSection() {
|
|||||||
window.photosView.show();
|
window.photosView.show();
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToTrashSection() {
|
function switchToTrashSection() {
|
||||||
@@ -311,7 +303,7 @@ function switchToTrashSection() {
|
|||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb?.classList.add("hidden");
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// Show files containers (to be filled with trash)
|
// Show files containers (to be filled with trash)
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ async function performSearch(query, sortBy) {
|
|||||||
app.isSearchMode = true;
|
app.isSearchMode = true;
|
||||||
window.ui.updateBreadcrumb(`Search: "${query}"`);
|
window.ui.updateBreadcrumb(`Search: "${query}"`);
|
||||||
|
|
||||||
window.ui.showError(
|
window.ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
||||||
`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -37,7 +34,6 @@ async function performSearch(query, sortBy) {
|
|||||||
|
|
||||||
const searchResults = await window.search.searchFiles(query, options);
|
const searchResults = await window.search.searchFiles(query, options);
|
||||||
window.search.displaySearchResults(searchResults);
|
window.search.displaySearchResults(searchResults);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', error);
|
console.error('Search error:', error);
|
||||||
window.ui.showNotification('Error', 'Error performing search');
|
window.ui.showNotification('Error', 'Error performing search');
|
||||||
@@ -46,7 +42,7 @@ async function performSearch(query, sortBy) {
|
|||||||
|
|
||||||
document.addEventListener('search-resort', (e) => {
|
document.addEventListener('search-resort', (e) => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const searchInput = document.querySelector('.search-container input');
|
||||||
if (searchInput && searchInput.value.trim()) {
|
if (searchInput?.value.trim()) {
|
||||||
performSearch(searchInput.value.trim(), e.detail.sort_by);
|
performSearch(searchInput.value.trim(), e.detail.sort_by);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,5 +29,4 @@ window.app = {
|
|||||||
viewFile: null // current file in inline view
|
viewFile: null // current file in inline view
|
||||||
};
|
};
|
||||||
|
|
||||||
window.appElements = {
|
window.appElements = {};
|
||||||
};
|
|
||||||
|
|||||||
+11
-14
@@ -8,7 +8,7 @@ async function loadTrashItems() {
|
|||||||
try {
|
try {
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
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?.t ? window.i18n.t : (k) => k.split('.').pop();
|
||||||
elements.filesList.innerHTML = `
|
elements.filesList.innerHTML = `
|
||||||
<div class="list-header trash-header">
|
<div class="list-header trash-header">
|
||||||
<div data-i18n="files.name">${_tt('files.name')}</div>
|
<div data-i18n="files.name">${_tt('files.name')}</div>
|
||||||
@@ -31,10 +31,9 @@ async function loadTrashItems() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trashItems.forEach(item => {
|
trashItems.forEach((item) => {
|
||||||
addTrashItemToView(item);
|
addTrashItemToView(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading trash items:', error);
|
console.error('Error loading trash items:', error);
|
||||||
window.ui.showNotification('Error', 'Error loading trash items');
|
window.ui.showNotification('Error', 'Error loading trash items');
|
||||||
@@ -54,22 +53,20 @@ function addTrashItemToView(item) {
|
|||||||
iconClass = item.icon_class || 'fas fa-folder';
|
iconClass = item.icon_class || 'fas fa-folder';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
||||||
} else {
|
} else {
|
||||||
iconClass = item.icon_class || (window.ui && window.ui.getIconClass
|
iconClass = item.icon_class || (window.ui?.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
|
||||||
? window.ui.getIconClass(item.name)
|
iconSpecialClass = window.ui?.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : '';
|
||||||
: 'fas fa-file');
|
|
||||||
iconSpecialClass = (window.ui && window.ui.getIconSpecialClass)
|
|
||||||
? window.ui.getIconSpecialClass(item.name)
|
|
||||||
: '';
|
|
||||||
const cat = item.category || '';
|
const cat = item.category || '';
|
||||||
typeLabel = cat
|
typeLabel = cat
|
||||||
? (window.i18n ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat)
|
? window.i18n
|
||||||
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
|
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
||||||
|
: cat
|
||||||
|
: window.i18n
|
||||||
|
? window.i18n.t('files.file_types.document')
|
||||||
|
: 'Document';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFolder = !isFile;
|
const isFolder = !isFile;
|
||||||
const iconWrapClass = isFolder
|
const iconWrapClass = isFolder ? 'file-icon folder-icon' : `file-icon ${iconSpecialClass}`.trim();
|
||||||
? 'file-icon folder-icon'
|
|
||||||
: `file-icon ${iconSpecialClass}`.trim();
|
|
||||||
|
|
||||||
const listElement = document.createElement('div');
|
const listElement = document.createElement('div');
|
||||||
listElement.className = 'file-item trash-item';
|
listElement.className = 'file-item trash-item';
|
||||||
|
|||||||
+155
-146
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
// UI Module
|
// UI Module
|
||||||
const ui = {
|
const ui = {
|
||||||
|
|
||||||
/** @type {HTMLDListElement | null} */
|
/** @type {HTMLDListElement | null} */
|
||||||
//dragPreview,
|
//dragPreview,
|
||||||
|
|
||||||
@@ -285,8 +284,8 @@ const ui = {
|
|||||||
*/
|
*/
|
||||||
setupDragAndDrop() {
|
setupDragAndDrop() {
|
||||||
// prepare area to build dragged elements
|
// prepare area to build dragged elements
|
||||||
this.dragPreview = document.createElement("div");
|
this.dragPreview = document.createElement('div');
|
||||||
this.dragPreview.className="drag-preview";
|
this.dragPreview.className = 'drag-preview';
|
||||||
document.body.appendChild(this.dragPreview);
|
document.body.appendChild(this.dragPreview);
|
||||||
this.draggedItems = null;
|
this.draggedItems = null;
|
||||||
|
|
||||||
@@ -294,9 +293,7 @@ const ui = {
|
|||||||
|
|
||||||
const collectDroppedEntries = async (dataTransfer) => {
|
const collectDroppedEntries = async (dataTransfer) => {
|
||||||
const items = Array.from(dataTransfer?.items || []);
|
const items = Array.from(dataTransfer?.items || []);
|
||||||
const rootEntries = items
|
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
|
||||||
.map(it => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (rootEntries.length === 0) return null;
|
if (rootEntries.length === 0) return null;
|
||||||
|
|
||||||
@@ -362,11 +359,11 @@ const ui = {
|
|||||||
// First try directory-aware extraction (Finder folder drag & drop)
|
// First try directory-aware extraction (Finder folder drag & drop)
|
||||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||||
if (droppedEntries && droppedEntries.length > 0) {
|
if (droppedEntries && droppedEntries.length > 0) {
|
||||||
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
||||||
if (hasFolderStructure) {
|
if (hasFolderStructure) {
|
||||||
fileOps.uploadFolderEntries(droppedEntries);
|
fileOps.uploadFolderEntries(droppedEntries);
|
||||||
} else {
|
} else {
|
||||||
fileOps.uploadFiles(droppedEntries.map(x => x.file));
|
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dropzone.style.display = 'none';
|
dropzone.style.display = 'none';
|
||||||
@@ -375,9 +372,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
||||||
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
|
|
||||||
);
|
|
||||||
if (hasRelativePaths) {
|
if (hasRelativePaths) {
|
||||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||||
} else {
|
} else {
|
||||||
@@ -399,8 +394,7 @@ const ui = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('dragleave', (e) => {
|
document.addEventListener('dragleave', (e) => {
|
||||||
if (e.clientX <= 0 || e.clientY <= 0 ||
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||||
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
|
||||||
dropzone.classList.remove('active');
|
dropzone.classList.remove('active');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!dropzone.classList.contains('active')) {
|
if (!dropzone.classList.contains('active')) {
|
||||||
@@ -421,11 +415,11 @@ const ui = {
|
|||||||
// First try directory-aware extraction (Finder folder drag & drop)
|
// First try directory-aware extraction (Finder folder drag & drop)
|
||||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||||
if (droppedEntries && droppedEntries.length > 0) {
|
if (droppedEntries && droppedEntries.length > 0) {
|
||||||
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
||||||
if (hasFolderStructure) {
|
if (hasFolderStructure) {
|
||||||
fileOps.uploadFolderEntries(droppedEntries);
|
fileOps.uploadFolderEntries(droppedEntries);
|
||||||
} else {
|
} else {
|
||||||
fileOps.uploadFiles(droppedEntries.map(x => x.file));
|
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dropzone.style.display = 'none';
|
dropzone.style.display = 'none';
|
||||||
@@ -434,9 +428,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
||||||
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
|
|
||||||
);
|
|
||||||
if (hasRelativePaths) {
|
if (hasRelativePaths) {
|
||||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||||
} else {
|
} else {
|
||||||
@@ -482,13 +474,11 @@ const ui = {
|
|||||||
updateBreadcrumb() {
|
updateBreadcrumb() {
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
||||||
|
|
||||||
// Helper function to safely get translation text
|
// Helper function to safely get translation text
|
||||||
const getTranslatedText = (key, defaultValue) => {
|
const getTranslatedText = (key, defaultValue) => {
|
||||||
if (!window.i18n || !window.i18n.t) return defaultValue;
|
if (!window.i18n?.t) return defaultValue;
|
||||||
return window.i18n.t(key);
|
return window.i18n.t(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -504,7 +494,7 @@ const ui = {
|
|||||||
homeIcon.addEventListener('click', () => {
|
homeIcon.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = [];
|
window.app.breadcrumbPath = [];
|
||||||
window.app.currentPath = window.app.userHomeFolderId;
|
window.app.currentPath = window.app.userHomeFolderId;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -514,7 +504,10 @@ const ui = {
|
|||||||
// TODO clarify the difference between homeIcon & this first element
|
// TODO clarify the difference between homeIcon & this first element
|
||||||
if (window.app.userHomeFolderName) {
|
if (window.app.userHomeFolderName) {
|
||||||
if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) {
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,30 +533,30 @@ const ui = {
|
|||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = path.slice(0, index + 1);
|
window.app.breadcrumbPath = path.slice(0, index + 1);
|
||||||
window.app.currentPath = segment.id;
|
window.app.currentPath = segment.id;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
// can drag files on this folder
|
// can drag files on this folder
|
||||||
// dragover – only folders are valid drop targets
|
// dragover – only folders are valid drop targets
|
||||||
item.addEventListener('dragover', (e) => {
|
item.addEventListener('dragover', (e) => {
|
||||||
const card = e.target.closest("span");
|
const card = e.target.closest('span');
|
||||||
if (!card || !card.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
card.classList.add('drop-target');
|
card.classList.add('drop-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragleave
|
// dragleave
|
||||||
item.addEventListener('dragleave', (e) => {
|
item.addEventListener('dragleave', (e) => {
|
||||||
console.log( "dragleave ",e );
|
console.log('dragleave ', e);
|
||||||
const card = e.target.closest("span");
|
const card = e.target.closest('span');
|
||||||
if (!card || !card.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
// drop – only folders accept drops
|
// drop – only folders accept drops
|
||||||
item.addEventListener('drop', async (e) => {
|
item.addEventListener('drop', async (e) => {
|
||||||
const card = e.target.closest("span");
|
const card = e.target.closest('span');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const targetFolderId = card.dataset.folderId;
|
const targetFolderId = card.dataset.folderId;
|
||||||
if (!targetFolderId) return;
|
if (!targetFolderId) return;
|
||||||
@@ -572,7 +565,7 @@ const ui = {
|
|||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
const action = e.dataTransfer?.dropEffect;
|
const action = e.dataTransfer?.dropEffect;
|
||||||
await self._dropToFolder( action, targetFolderId, e.dataTransfer );
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Last segment: current location, not clickable
|
// Last segment: current location, not clickable
|
||||||
@@ -639,8 +632,6 @@ const ui = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Data store + event delegation (replaces per-item listeners)
|
* Data store + event delegation (replaces per-item listeners)
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
@@ -672,7 +663,7 @@ const ui = {
|
|||||||
*/
|
*/
|
||||||
_renderFoldersToView(folders) {
|
_renderFoldersToView(folders) {
|
||||||
if (!Array.isArray(folders) || folders.length === 0) return;
|
if (!Array.isArray(folders) || folders.length === 0) return;
|
||||||
const target = document.getElementById('files-list')
|
const target = document.getElementById('files-list');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
@@ -689,19 +680,19 @@ const ui = {
|
|||||||
*/
|
*/
|
||||||
_renderFilesToView(files) {
|
_renderFilesToView(files) {
|
||||||
if (!Array.isArray(files) || files.length === 0) return;
|
if (!Array.isArray(files) || files.length === 0) return;
|
||||||
const target = document.getElementById('files-list')
|
const target = document.getElementById('files-list');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
frag.appendChild( this._createFileItem(file))
|
frag.appendChild(this._createFileItem(file));
|
||||||
}
|
}
|
||||||
target.appendChild(frag);
|
target.appendChild(frag);
|
||||||
},
|
},
|
||||||
|
|
||||||
_upsertById(arr, item) {
|
_upsertById(arr, item) {
|
||||||
if (!Array.isArray(arr) || !item || !item.id) return;
|
if (!Array.isArray(arr) || !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) {
|
if (idx >= 0) {
|
||||||
arr[idx] = item;
|
arr[idx] = item;
|
||||||
} else {
|
} else {
|
||||||
@@ -716,26 +707,24 @@ const ui = {
|
|||||||
* @param {any} dataTransfer fallback if nothing is selected
|
* @param {any} dataTransfer fallback if nothing is selected
|
||||||
*/
|
*/
|
||||||
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
||||||
let selection = window.multiSelect.getSelection(targetFolderId);
|
const selection = window.multiSelect.getSelection(targetFolderId);
|
||||||
|
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
|
|
||||||
if (selection.fileIds.length == 0 && selection.folderIds.length == 0) {
|
if (selection.fileIds.length === 0 && selection.folderIds.length === 0) {
|
||||||
|
|
||||||
// try to use dataTransfer (direct move without selection)
|
// try to use dataTransfer (direct move without selection)
|
||||||
const id = dataTransfer.getData('text/plain');
|
const id = dataTransfer.getData('text/plain');
|
||||||
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
||||||
|
|
||||||
if (isFolder && id === targetFolderId) {
|
if (isFolder && id === targetFolderId) {
|
||||||
console.log("nothing to do");
|
console.log('nothing to do');
|
||||||
return; //nothing to do
|
return; //nothing to do
|
||||||
}
|
}
|
||||||
// append current item to selection
|
// append current item to selection
|
||||||
if (isFolder) {
|
if (isFolder) {
|
||||||
console.log(`adding ${id} as folder`);
|
console.log(`adding ${id} as folder`);
|
||||||
selection.folderIds.push(id);
|
selection.folderIds.push(id);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.log(`adding ${id} as file`);
|
console.log(`adding ${id} as file`);
|
||||||
selection.fileIds.push(id);
|
selection.fileIds.push(id);
|
||||||
}
|
}
|
||||||
@@ -750,15 +739,14 @@ const ui = {
|
|||||||
|
|
||||||
let result;
|
let result;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "copy":
|
case 'copy':
|
||||||
result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId);
|
result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "move":
|
case 'move':
|
||||||
result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
|
result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
|
||||||
// redraw directory
|
// redraw directory
|
||||||
if (result.success > 0)
|
if (result.success > 0) window.loadFiles();
|
||||||
window.loadFiles();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -795,15 +783,25 @@ const ui = {
|
|||||||
if (!filesList) return;
|
if (!filesList) return;
|
||||||
this._delegationReady = true;
|
this._delegationReady = true;
|
||||||
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
// ── helpers ────────────────────────────────────────────────
|
// ── helpers ────────────────────────────────────────────────
|
||||||
const itemInfo = (card) => {
|
const itemInfo = (card) => {
|
||||||
if (!card) return null;
|
if (!card) return null;
|
||||||
const fileId = card.dataset.fileId;
|
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: this._items.get(fileId)
|
||||||
|
};
|
||||||
const folderId = card.dataset.folderId;
|
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: this._items.get(folderId)
|
||||||
|
};
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -816,25 +814,23 @@ const ui = {
|
|||||||
// But NOT image files - those should be previewed in the inline viewer
|
// But NOT image files - those should be previewed in the inline viewer
|
||||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
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);
|
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
||||||
try {
|
try {
|
||||||
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
|
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||||
await window.wopiEditor.openInModal(file.id, file.name, 'edit')
|
await window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
console.warn(`WOPI Editor failed, falling bck to classic view `, e);
|
console.warn(`WOPI Editor failed, falling bck to classic view `, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.isViewableFile(file) || isImage) {
|
if (this.isViewableFile(file) || isImage) {
|
||||||
if (window.inlineViewer) {
|
if (window.inlineViewer) {
|
||||||
window.inlineViewer.openFile(file);
|
window.inlineViewer.openFile(file);
|
||||||
// update history
|
// update history
|
||||||
window.app.viewFile = file.id;
|
window.app.viewFile = file.id;
|
||||||
window.updateHistory(false);
|
window.updateHistory(false);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
window.fileOps.downloadFile(file.id, file.name);
|
window.fileOps.downloadFile(file.id, file.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -847,7 +843,7 @@ const ui = {
|
|||||||
const folderName = card.dataset.folderName;
|
const folderName = card.dataset.folderName;
|
||||||
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
||||||
window.app.currentPath = folderId;
|
window.app.currentPath = folderId;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -856,13 +852,13 @@ const ui = {
|
|||||||
window.app.contextMenuTargetFolder = {
|
window.app.contextMenuTargetFolder = {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
name: card.dataset.folderName,
|
name: card.dataset.folderName,
|
||||||
parent_id: card.dataset.parentId || ""
|
parent_id: card.dataset.parentId || ''
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
window.app.contextMenuTargetFile = {
|
window.app.contextMenuTargetFile = {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
name: card.dataset.fileName,
|
name: card.dataset.fileName,
|
||||||
folder_id: card.dataset.folderId || ""
|
folder_id: card.dataset.folderId || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -878,10 +874,8 @@ const ui = {
|
|||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
setContextTarget(card, info);
|
setContextTarget(card, info);
|
||||||
const menuId = info.type === 'folder'
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||||
? 'folder-context-menu' : 'file-context-menu';
|
showContextMenuAtElement(e.target.closest('.file-actions'), menuId);
|
||||||
showContextMenuAtElement(
|
|
||||||
e.target.closest('.file-actions'), menuId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,14 +928,13 @@ const ui = {
|
|||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
setContextTarget(card, info);
|
setContextTarget(card, info);
|
||||||
const menuId = info.type === 'folder'
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||||
? 'folder-context-menu' : 'file-context-menu';
|
|
||||||
const menu = document.getElementById(menuId);
|
const menu = document.getElementById(menuId);
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
||||||
window.contextMenus.syncFavoriteOptionLabels();
|
window.contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
menu.style.left = `${e.pageX}px`;
|
menu.style.left = `${e.pageX}px`;
|
||||||
menu.style.top = `${e.pageY}px`;
|
menu.style.top = `${e.pageY}px`;
|
||||||
@@ -950,25 +943,30 @@ const ui = {
|
|||||||
|
|
||||||
// dragstart
|
// dragstart
|
||||||
filesList.addEventListener('dragstart', (e) => {
|
filesList.addEventListener('dragstart', (e) => {
|
||||||
let card = e.target.closest('.file-item');
|
const card = e.target.closest('.file-item');
|
||||||
if (!card) { e.preventDefault(); return; }
|
if (!card) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
if (!info) { e.preventDefault(); return; }
|
if (!info) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.dataTransfer.setData('text/plain', info.id);
|
e.dataTransfer.setData('text/plain', info.id);
|
||||||
if (info.type === 'folder') {
|
if (info.type === 'folder') {
|
||||||
e.dataTransfer.setData(
|
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
||||||
'application/oxicloud-folder', 'true');
|
|
||||||
}
|
}
|
||||||
// allow copy or move (handled by the browser)
|
// allow copy or move (handled by the browser)
|
||||||
e.dataTransfer.effectAllowed = "copyMove";
|
e.dataTransfer.effectAllowed = 'copyMove';
|
||||||
|
|
||||||
self.draggedItems = document.createElement("div");
|
this.draggedItems = document.createElement('div');
|
||||||
self.draggedItems.className = "dragged-items";
|
this.draggedItems.className = 'dragged-items';
|
||||||
|
|
||||||
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
||||||
if (selectedCardFromList.length == 0) {
|
if (selectedCardFromList.length === 0) {
|
||||||
// fallback to current element
|
// fallback to current element
|
||||||
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
||||||
}
|
}
|
||||||
@@ -978,53 +976,54 @@ const ui = {
|
|||||||
let lastItemDiv = null;
|
let lastItemDiv = null;
|
||||||
|
|
||||||
while (index < selectedCardFromList.length && index < maxElements) {
|
while (index < selectedCardFromList.length && index < maxElements) {
|
||||||
let iconCell = document.createElement("div");
|
const iconCell = document.createElement('div');
|
||||||
let icon = selectedCardFromList[index].getElementsByClassName("file-icon").item(0)?.cloneNode(true);
|
const icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
|
||||||
if (icon) {
|
if (icon) {
|
||||||
iconCell.appendChild(icon);
|
iconCell.appendChild(icon);
|
||||||
iconCell.querySelectorAll('img')?.forEach((img) => { img.loading="eager"; } );
|
iconCell.querySelectorAll('img')?.forEach((img) => {
|
||||||
|
img.loading = 'eager';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameCell = document.createElement("div");
|
const nameCell = document.createElement('div');
|
||||||
let name = selectedCardFromList[index].getElementsByTagName("span").item(0)?.cloneNode(true);
|
const name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
|
||||||
if (name) {
|
if (name) {
|
||||||
nameCell.appendChild(name);
|
nameCell.appendChild(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let div = document.createElement("div");
|
const div = document.createElement('div');
|
||||||
div.className="file-item";
|
div.className = 'file-item';
|
||||||
div.appendChild(iconCell);
|
div.appendChild(iconCell);
|
||||||
div.appendChild(nameCell);
|
div.appendChild(nameCell);
|
||||||
|
|
||||||
self.draggedItems.appendChild( div);
|
this.draggedItems.appendChild(div);
|
||||||
index += 1;
|
index += 1;
|
||||||
lastItemDiv = div;
|
lastItemDiv = div;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if more than 1 item, display the badge
|
// if more than 1 item, display the badge
|
||||||
if (selectedCardFromList.length > 1) {
|
if (selectedCardFromList.length > 1) {
|
||||||
let badge = document.createElement("span");
|
const badge = document.createElement('span');
|
||||||
badge.className="dragged-items-badge";
|
badge.className = 'dragged-items-badge';
|
||||||
badge.innerText = `${selectedCardFromList.length}`;
|
badge.innerText = `${selectedCardFromList.length}`;
|
||||||
self.draggedItems.appendChild( badge);
|
this.draggedItems.appendChild(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if more than maxElements display the fading
|
// if more than maxElements display the fading
|
||||||
if (selectedCardFromList.length > maxElements) {
|
if (selectedCardFromList.length > maxElements) {
|
||||||
lastItemDiv.classList.add("fading");
|
lastItemDiv.classList.add('fading');
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dragPreview.appendChild( self.draggedItems);
|
this.dragPreview.appendChild(this.draggedItems);
|
||||||
e.dataTransfer.setDragImage(self.draggedItems, 0, 0);
|
e.dataTransfer.setDragImage(this.draggedItems, 0, 0);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragend
|
// dragend
|
||||||
filesList.addEventListener('dragend', (e) => {
|
filesList.addEventListener('dragend', (_e) => {
|
||||||
|
this.dragPreview.removeChild(this.draggedItems);
|
||||||
self.dragPreview.removeChild( self.draggedItems);
|
document.querySelectorAll('.drop-target').forEach((el) => {
|
||||||
document.querySelectorAll('.drop-target')
|
el.classList.remove('drop-target');
|
||||||
.forEach(el => el.classList.remove('drop-target'));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragover – only folders are valid drop targets
|
// dragover – only folders are valid drop targets
|
||||||
@@ -1054,9 +1053,8 @@ const ui = {
|
|||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
const action = e.dataTransfer.dropEffect;
|
const action = e.dataTransfer.dropEffect;
|
||||||
await self._dropToFolder( action, targetFolderId, e.dataTransfer);
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
@@ -1099,9 +1097,7 @@ const ui = {
|
|||||||
* Sync favorite visuals for a file/folder across grid and list views.
|
* Sync favorite visuals for a file/folder across grid and list views.
|
||||||
*/
|
*/
|
||||||
setFavoriteVisualState(itemId, itemType, isFavorite) {
|
setFavoriteVisualState(itemId, itemType, isFavorite) {
|
||||||
const selector = itemType === 'folder'
|
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
|
||||||
? `#files-list .file-item[data-folder-id="${itemId}"]`
|
|
||||||
: `#files-list .file-item[data-file-id="${itemId}"]`;
|
|
||||||
|
|
||||||
const card = document.querySelector(selector);
|
const card = document.querySelector(selector);
|
||||||
const starBtn = card ? card.querySelector('.favorite-star') : null;
|
const starBtn = card ? card.querySelector('.favorite-star') : null;
|
||||||
@@ -1111,8 +1107,8 @@ const ui = {
|
|||||||
|
|
||||||
// SVG icon path (after icons.js replacement)
|
// SVG icon path (after icons.js replacement)
|
||||||
const svg = starBtn.querySelector('svg');
|
const svg = starBtn.querySelector('svg');
|
||||||
const filledPath = window.OxiIcons && window.OxiIcons['star'];
|
const filledPath = window.OxiIcons?.star;
|
||||||
const outlinePath = window.OxiIcons && window.OxiIcons['star-outline'];
|
const outlinePath = window.OxiIcons?.['star-outline'];
|
||||||
const targetPath = isFavorite ? filledPath : outlinePath;
|
const targetPath = isFavorite ? filledPath : outlinePath;
|
||||||
if (svg && targetPath) {
|
if (svg && targetPath) {
|
||||||
const p = svg.querySelector('path');
|
const p = svg.querySelector('path');
|
||||||
@@ -1157,9 +1153,9 @@ const ui = {
|
|||||||
el.className = 'file-item';
|
el.className = 'file-item';
|
||||||
el.dataset.folderId = folder.id;
|
el.dataset.folderId = folder.id;
|
||||||
el.dataset.folderName = folder.name;
|
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 isFav = window.favorites?.isFavorite(folder.id, 'folder');
|
||||||
const formattedDate = window.formatDateTime(folder.modified_at);
|
const formattedDate = window.formatDateTime(folder.modified_at);
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
@@ -1182,7 +1178,7 @@ const ui = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.app.currentPath !== "") {
|
if (window.app.currentPath !== '') {
|
||||||
el.setAttribute('draggable', 'true');
|
el.setAttribute('draggable', 'true');
|
||||||
}
|
}
|
||||||
this._bindStarClick(el);
|
this._bindStarClick(el);
|
||||||
@@ -1195,17 +1191,21 @@ const ui = {
|
|||||||
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
||||||
const cat = file.category || '';
|
const cat = file.category || '';
|
||||||
const typeLabel = cat
|
const typeLabel = cat
|
||||||
? (window.i18n ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat)
|
? window.i18n
|
||||||
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
|
? 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 fileSize = file.size_formatted || window.formatFileSize(file.size);
|
||||||
const formattedDate = window.formatDateTime(file.modified_at);
|
const formattedDate = window.formatDateTime(file.modified_at);
|
||||||
const isFav = window.favorites && window.favorites.isFavorite(file.id, 'file');
|
const isFav = window.favorites?.isFavorite(file.id, 'file');
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'file-item';
|
el.className = 'file-item';
|
||||||
el.dataset.fileId = file.id;
|
el.dataset.fileId = file.id;
|
||||||
el.dataset.fileName = file.name;
|
el.dataset.fileName = file.name;
|
||||||
el.dataset.folderId = file.folder_id || "";
|
el.dataset.folderId = file.folder_id || '';
|
||||||
el.setAttribute('draggable', 'true');
|
el.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
@@ -1230,7 +1230,10 @@ const ui = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
var thumb = el.querySelector('.file-thumb');
|
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);
|
this._bindStarClick(el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
@@ -1241,7 +1244,7 @@ const ui = {
|
|||||||
|
|
||||||
resetFilesList() {
|
resetFilesList() {
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
const filesContainerError=document.getElementById("files-container-error");
|
const filesContainerError = document.getElementById('files-container-error');
|
||||||
|
|
||||||
if (!filesList) return;
|
if (!filesList) return;
|
||||||
|
|
||||||
@@ -1255,11 +1258,10 @@ const ui = {
|
|||||||
<div></div><!-- actions -->
|
<div></div><!-- actions -->
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement)
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesList);
|
||||||
window.i18n.translateElement( filesList);
|
|
||||||
|
|
||||||
filesList.classList.remove("hidden");
|
filesList.classList.remove('hidden');
|
||||||
filesContainerError?.classList.add("hidden");
|
filesContainerError?.classList.add('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
showEmptyList() {
|
showEmptyList() {
|
||||||
@@ -1267,8 +1269,7 @@ const ui = {
|
|||||||
<i class="fas fa-folder-open empty-state-icon"></i>
|
<i class="fas fa-folder-open empty-state-icon"></i>
|
||||||
<p data-i18n="files.no_files"></p>
|
<p data-i18n="files.no_files"></p>
|
||||||
<p data-i18n="files.empty_hint"></p>
|
<p data-i18n="files.empty_hint"></p>
|
||||||
`
|
`);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1277,16 +1278,14 @@ const ui = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
showError(content) {
|
showError(content) {
|
||||||
const filesContainerError=document.getElementById("files-container-error");
|
const filesContainerError = document.getElementById('files-container-error');
|
||||||
const filesList=document.getElementById("files-list");
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesContainerError)
|
if (filesContainerError) filesContainerError.innerHTML = content;
|
||||||
filesContainerError.innerHTML=content;
|
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement)
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesContainerError);
|
||||||
window.i18n.translateElement( filesContainerError);
|
|
||||||
|
|
||||||
filesContainerError?.classList.remove("hidden");
|
filesContainerError?.classList.remove('hidden');
|
||||||
filesList?.classList.add("hidden");
|
filesList?.classList.add('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1381,7 +1380,9 @@ function toggleCardSelection(card, event) {
|
|||||||
*/
|
*/
|
||||||
function showContextMenuAtElement(triggerElement, menuId) {
|
function showContextMenuAtElement(triggerElement, menuId) {
|
||||||
// Hide any open menus first
|
// 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);
|
const menu = document.getElementById(menuId);
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
@@ -1403,7 +1404,7 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
|||||||
window.contextMenus.syncFavoriteOptionLabels();
|
window.contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.style.left = `${left}px`;
|
menu.style.left = `${left}px`;
|
||||||
@@ -1426,7 +1427,8 @@ function initRubberBandSelection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active = false;
|
let active = false;
|
||||||
let startX = 0, startY = 0;
|
let startX = 0,
|
||||||
|
startY = 0;
|
||||||
|
|
||||||
// We listen on the whole files-container (covers grid + empty space)
|
// We listen on the whole files-container (covers grid + empty space)
|
||||||
const container = document.querySelector('.files-container') || document.getElementById('files-list');
|
const container = document.querySelector('.files-container') || document.getElementById('files-list');
|
||||||
@@ -1435,11 +1437,16 @@ function initRubberBandSelection() {
|
|||||||
container.addEventListener('mousedown', (e) => {
|
container.addEventListener('mousedown', (e) => {
|
||||||
// Only start if clicking empty area (not on a card, button, menu, input…)
|
// Only start if clicking empty area (not on a card, button, menu, input…)
|
||||||
if (e.button !== 0) return; // left click only
|
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('.context-menu') ||
|
||||||
e.target.closest('.upload-dropdown') || e.target.closest('button') ||
|
e.target.closest('.upload-dropdown') ||
|
||||||
e.target.closest('input') || e.target.closest('.breadcrumb') ||
|
e.target.closest('button') ||
|
||||||
e.target.closest('.list-header')) return;
|
e.target.closest('input') ||
|
||||||
|
e.target.closest('.breadcrumb') ||
|
||||||
|
e.target.closest('.list-header')
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
active = true;
|
active = true;
|
||||||
startX = e.clientX;
|
startX = e.clientX;
|
||||||
@@ -1478,13 +1485,10 @@ function initRubberBandSelection() {
|
|||||||
// Highlight cards that intersect with the rectangle
|
// Highlight cards that intersect with the rectangle
|
||||||
const rectBounds = { left, top, right: left + width, bottom: top + height };
|
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 cardRect = card.getBoundingClientRect();
|
||||||
const intersects =
|
const intersects =
|
||||||
cardRect.left < rectBounds.right &&
|
cardRect.left < rectBounds.right && cardRect.right > rectBounds.left && cardRect.top < rectBounds.bottom && cardRect.bottom > rectBounds.top;
|
||||||
cardRect.right > rectBounds.left &&
|
|
||||||
cardRect.top < rectBounds.bottom &&
|
|
||||||
cardRect.bottom > rectBounds.top;
|
|
||||||
|
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
@@ -1498,7 +1502,7 @@ function initRubberBandSelection() {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
// Deselect from multiSelect module
|
// Deselect from multiSelect module
|
||||||
if (window.multiSelect) {
|
if (window.multiSelect) {
|
||||||
const info = window.multiSelect._extractInfo(card)
|
const info = window.multiSelect._extractInfo(card);
|
||||||
if (info) window.multiSelect.deselect(info.id);
|
if (info) window.multiSelect.deselect(info.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1516,7 +1520,9 @@ function initRubberBandSelection() {
|
|||||||
// deselect handler doesn't immediately clear the selection.
|
// deselect handler doesn't immediately clear the selection.
|
||||||
if (hadSelection) {
|
if (hadSelection) {
|
||||||
window.__rubberBandJustFinished = true;
|
window.__rubberBandJustFinished = true;
|
||||||
requestAnimationFrame(() => { window.__rubberBandJustFinished = false; });
|
requestAnimationFrame(() => {
|
||||||
|
window.__rubberBandJustFinished = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1533,7 +1539,6 @@ window.toggleCardSelection = toggleCardSelection;
|
|||||||
window.showContextMenuAtElement = showContextMenuAtElement;
|
window.showContextMenuAtElement = showContextMenuAtElement;
|
||||||
window.initRubberBandSelection = initRubberBandSelection;
|
window.initRubberBandSelection = initRubberBandSelection;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a modern confirm dialog (replaces native confirm())
|
* Show a modern confirm dialog (replaces native confirm())
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@@ -1573,7 +1578,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
|||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
// Force layout then show
|
// Force layout then show
|
||||||
requestAnimationFrame(() => { overlay.classList.add('active'); });
|
requestAnimationFrame(() => {
|
||||||
|
overlay.classList.add('active');
|
||||||
|
});
|
||||||
|
|
||||||
const cleanup = (result) => {
|
const cleanup = (result) => {
|
||||||
overlay.classList.remove('active');
|
overlay.classList.remove('active');
|
||||||
@@ -1583,7 +1590,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
|||||||
|
|
||||||
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
|
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
|
||||||
overlay.querySelector('.confirm-dialog-ok').addEventListener('click', () => cleanup(true));
|
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;
|
window.showConfirmDialog = showConfirmDialog;
|
||||||
|
|||||||
+138
-46
@@ -6,7 +6,7 @@
|
|||||||
const uiFileTypes = {
|
const uiFileTypes = {
|
||||||
// TODO: 'd better to use a canViw() method in inlineViewer
|
// TODO: 'd better to use a canViw() method in inlineViewer
|
||||||
isViewableFile(file) {
|
isViewableFile(file) {
|
||||||
if (!file || !file.mime_type) return false;
|
if (!file?.mime_type) return false;
|
||||||
if (file.mime_type.startsWith('image/')) return true;
|
if (file.mime_type.startsWith('image/')) return true;
|
||||||
if (file.mime_type === 'application/pdf') return true;
|
if (file.mime_type === 'application/pdf') return true;
|
||||||
if (file.mime_type.startsWith('audio/')) return true;
|
if (file.mime_type.startsWith('audio/')) return true;
|
||||||
@@ -18,24 +18,57 @@ const uiFileTypes = {
|
|||||||
if (!fileName) return 'fas fa-file';
|
if (!fileName) return 'fas fa-file';
|
||||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||||
const map = {
|
const map = {
|
||||||
pdf:'fas fa-file-pdf', doc:'fas fa-file-word', docx:'fas fa-file-word',
|
pdf: 'fas fa-file-pdf',
|
||||||
txt:'fas fa-file-alt', rtf:'fas fa-file-alt', odt:'fas fa-file-alt',
|
doc: 'fas fa-file-word',
|
||||||
xls:'fas fa-file-excel', xlsx:'fas fa-file-excel', csv:'fas fa-file-excel', ods:'fas fa-file-excel',
|
docx: 'fas fa-file-word',
|
||||||
ppt:'fas fa-file-powerpoint', pptx:'fas fa-file-powerpoint', odp:'fas fa-file-powerpoint',
|
txt: 'fas fa-file-alt',
|
||||||
jpg:'fas fa-file-image', jpeg:'fas fa-file-image', png:'fas fa-file-image',
|
rtf: 'fas fa-file-alt',
|
||||||
gif:'fas fa-file-image', svg:'fas fa-file-image', webp:'fas fa-file-image',
|
odt: 'fas fa-file-alt',
|
||||||
bmp:'fas fa-file-image', ico:'fas fa-file-image',
|
xls: 'fas fa-file-excel',
|
||||||
mp4:'fas fa-file-video', avi:'fas fa-file-video', mov:'fas fa-file-video',
|
xlsx: 'fas fa-file-excel',
|
||||||
mkv:'fas fa-file-video', webm:'fas fa-file-video', flv:'fas fa-file-video',
|
csv: 'fas fa-file-excel',
|
||||||
mp3:'fas fa-file-audio', wav:'fas fa-file-audio', ogg:'fas fa-file-audio',
|
ods: 'fas fa-file-excel',
|
||||||
flac:'fas fa-file-audio', aac:'fas fa-file-audio', m4a:'fas fa-file-audio',
|
ppt: 'fas fa-file-powerpoint',
|
||||||
zip:'fas fa-file-archive', rar:'fas fa-file-archive', '7z':'fas fa-file-archive',
|
pptx: 'fas fa-file-powerpoint',
|
||||||
tar:'fas fa-file-archive', gz:'fas fa-file-archive',
|
odp: 'fas fa-file-powerpoint',
|
||||||
js:'fas fa-file-code', ts:'fas fa-file-code', py:'fas fa-file-code',
|
jpg: 'fas fa-file-image',
|
||||||
rs:'fas fa-file-code', java:'fas fa-file-code', html:'fas fa-file-code',
|
jpeg: 'fas fa-file-image',
|
||||||
css:'fas fa-file-code', json:'fas fa-file-code', xml:'fas fa-file-code',
|
png: 'fas fa-file-image',
|
||||||
sh:'fas fa-terminal', bash:'fas fa-terminal', bat:'fas fa-terminal',
|
gif: 'fas fa-file-image',
|
||||||
md:'fas fa-file-alt',
|
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';
|
return map[ext] || 'fas fa-file';
|
||||||
},
|
},
|
||||||
@@ -45,34 +78,93 @@ const uiFileTypes = {
|
|||||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||||
const map = {
|
const map = {
|
||||||
pdf: 'pdf-icon',
|
pdf: 'pdf-icon',
|
||||||
doc:'doc-icon', docx:'doc-icon', odt:'doc-icon', rtf:'doc-icon',
|
doc: 'doc-icon',
|
||||||
xls:'spreadsheet-icon', xlsx:'spreadsheet-icon', ods:'spreadsheet-icon', csv:'spreadsheet-icon',
|
docx: 'doc-icon',
|
||||||
ppt:'presentation-icon', pptx:'presentation-icon', odp:'presentation-icon', key:'presentation-icon',
|
odt: 'doc-icon',
|
||||||
jpg:'image-icon', jpeg:'image-icon', png:'image-icon', gif:'image-icon',
|
rtf: 'doc-icon',
|
||||||
svg:'image-icon', webp:'image-icon', bmp:'image-icon', ico:'image-icon',
|
xls: 'spreadsheet-icon',
|
||||||
heic:'image-icon', heif:'image-icon', avif:'image-icon', tiff:'image-icon',
|
xlsx: 'spreadsheet-icon',
|
||||||
mp4:'video-icon', avi:'video-icon', mkv:'video-icon', mov:'video-icon',
|
ods: 'spreadsheet-icon',
|
||||||
wmv:'video-icon', flv:'video-icon', webm:'video-icon', m4v:'video-icon',
|
csv: 'spreadsheet-icon',
|
||||||
mp3:'audio-icon', wav:'audio-icon', ogg:'audio-icon', flac:'audio-icon',
|
ppt: 'presentation-icon',
|
||||||
aac:'audio-icon', wma:'audio-icon', m4a:'audio-icon', opus:'audio-icon',
|
pptx: 'presentation-icon',
|
||||||
zip:'archive-icon', rar:'archive-icon', '7z':'archive-icon',
|
odp: 'presentation-icon',
|
||||||
tar:'archive-icon', gz:'archive-icon', bz2:'archive-icon', xz:'archive-icon',
|
key: 'presentation-icon',
|
||||||
exe:'installer-icon', msi:'installer-icon', dmg:'installer-icon',
|
jpg: 'image-icon',
|
||||||
deb:'installer-icon', rpm:'installer-icon', appimage:'installer-icon',
|
jpeg: 'image-icon',
|
||||||
py:'code-icon py-icon', rs:'code-icon rust-icon', go:'code-icon go-icon',
|
png: 'image-icon',
|
||||||
js:'code-icon js-icon', jsx:'code-icon js-icon', mjs:'code-icon js-icon',
|
gif: 'image-icon',
|
||||||
ts:'code-icon ts-icon', tsx:'code-icon ts-icon',
|
svg: 'image-icon',
|
||||||
java:'code-icon java-icon', c:'code-icon c-icon', cpp:'code-icon c-icon',
|
webp: 'image-icon',
|
||||||
cs:'code-icon cs-icon', rb:'code-icon ruby-icon', php:'code-icon php-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',
|
swift: 'code-icon swift-icon',
|
||||||
html:'code-icon html-icon', htm:'code-icon html-icon',
|
html: 'code-icon html-icon',
|
||||||
css:'code-icon css-icon', scss:'code-icon css-icon',
|
htm: 'code-icon html-icon',
|
||||||
json:'code-icon json-icon', xml:'code-icon html-icon',
|
css: 'code-icon css-icon',
|
||||||
yaml:'code-icon config-icon', yml:'code-icon config-icon',
|
scss: 'code-icon css-icon',
|
||||||
toml:'code-icon config-icon', ini:'code-icon config-icon',
|
json: 'code-icon json-icon',
|
||||||
sql:'code-icon sql-icon', vue:'code-icon js-icon', svelte:'code-icon js-icon',
|
xml: 'code-icon html-icon',
|
||||||
sh:'script-icon', bash:'script-icon', zsh:'script-icon', bat:'script-icon',
|
yaml: 'code-icon config-icon',
|
||||||
md:'code-icon md-icon', txt:'doc-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] || '';
|
return map[ext] || '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ const uiNotifications = {
|
|||||||
} else if (normalizedTitle.includes('favorite') || normalizedTitle.includes('favorit') || normalizedTitle.includes('fav')) {
|
} else if (normalizedTitle.includes('favorite') || normalizedTitle.includes('favorit') || normalizedTitle.includes('fav')) {
|
||||||
icon = 'fa-star';
|
icon = 'fa-star';
|
||||||
iconClass = 'success';
|
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';
|
icon = 'fa-check-circle';
|
||||||
iconClass = 'success';
|
iconClass = 'success';
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-14
@@ -31,9 +31,15 @@ function setupUserMenu() {
|
|||||||
const USER_DATA_KEY = 'oxicloud_user';
|
const USER_DATA_KEY = 'oxicloud_user';
|
||||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||||
const isAdmin = userData.role === 'admin';
|
const isAdmin = userData.role === 'admin';
|
||||||
if (adminBtn) { isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden'); }
|
if (adminBtn) {
|
||||||
if (adminDivider) { isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden'); }
|
isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden');
|
||||||
if (roleBadge) { isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.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(
|
window.ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled');
|
||||||
newIsDark ? '🌙' : '☀️',
|
|
||||||
newIsDark ? 'Dark mode enabled' : 'Light mode enabled'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +168,10 @@ function updateUserMenuData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const usedBytes = userData.storage_used_bytes || 0;
|
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 percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||||
|
|
||||||
if (storageFill) storageFill.style.width = percentage + '%';
|
if (storageFill) storageFill.style.width = `${percentage}%`;
|
||||||
if (storageText) {
|
if (storageText) {
|
||||||
const used = window.formatFileSize(usedBytes);
|
const used = window.formatFileSize(usedBytes);
|
||||||
const total = window.formatQuotaSize(quotaBytes);
|
const total = window.formatQuotaSize(quotaBytes);
|
||||||
@@ -199,11 +202,11 @@ function showUserProfileModal() {
|
|||||||
const role = userData.role || 'user';
|
const role = userData.role || 'user';
|
||||||
const initials = username.substring(0, 2).toUpperCase();
|
const initials = username.substring(0, 2).toUpperCase();
|
||||||
const usedBytes = userData.storage_used_bytes || 0;
|
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 percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||||
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
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?.t ? window.i18n.t(key) || fallback : fallback);
|
||||||
|
|
||||||
const existing = document.getElementById('profile-modal-overlay');
|
const existing = document.getElementById('profile-modal-overlay');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
@@ -217,7 +220,7 @@ function showUserProfileModal() {
|
|||||||
<div class="about-modal-avatar">${initials}</div>
|
<div class="about-modal-avatar">${initials}</div>
|
||||||
<h3 class="about-modal-username">${username}</h3>
|
<h3 class="about-modal-username">${username}</h3>
|
||||||
<p class="about-modal-email">${email}</p>
|
<p class="about-modal-email">${email}</p>
|
||||||
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : '👤 ' + t('user_menu.role_user', 'User')}</span>
|
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${t('user_menu.role_user', 'User')}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-storage">
|
<div class="about-modal-storage">
|
||||||
<div class="about-modal-storage-label">
|
<div class="about-modal-storage-label">
|
||||||
@@ -237,7 +240,7 @@ function showUserProfileModal() {
|
|||||||
// Set dynamic bar width and color via JS property (CSP-safe)
|
// Set dynamic bar width and color via JS property (CSP-safe)
|
||||||
const barFill = overlay.querySelector('#about-bar-fill');
|
const barFill = overlay.querySelector('#about-bar-fill');
|
||||||
if (barFill) {
|
if (barFill) {
|
||||||
barFill.style.width = percentage + '%';
|
barFill.style.width = `${percentage}%`;
|
||||||
barFill.style.background = barColor;
|
barFill.style.background = barColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +271,11 @@ async function logout() {
|
|||||||
// cleared before redirecting, otherwise the login page's session probe
|
// cleared before redirecting, otherwise the login page's session probe
|
||||||
// will refresh the token and redirect back to the app).
|
// will refresh the token and redirect back to the app).
|
||||||
try {
|
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 (_) {
|
} catch (_) {
|
||||||
// Best-effort
|
// Best-effort
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,11 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function getCsrfToken() {
|
function getCsrfToken() {
|
||||||
const match = document.cookie
|
const match = document.cookie.split('; ').find((row) => row.startsWith('oxicloud_csrf='));
|
||||||
.split('; ')
|
|
||||||
.find(row => row.startsWith('oxicloud_csrf='));
|
|
||||||
return match ? match.split('=')[1] : '';
|
return match ? match.split('=')[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
function getCsrfHeaders() {
|
function getCsrfHeaders() {
|
||||||
const token = getCsrfToken();
|
const token = getCsrfToken();
|
||||||
return token ? { 'X-CSRF-Token': token } : {};
|
return token ? { 'X-CSRF-Token': token } : {};
|
||||||
|
|||||||
@@ -5,12 +5,7 @@
|
|||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (typeof str !== 'string') return '';
|
if (typeof str !== 'string') return '';
|
||||||
return str
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/\"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
@@ -20,7 +15,7 @@ function formatFileSize(bytes) {
|
|||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a byte count for quota display. When bytes is 0, returns "∞" (unlimited).
|
/// Formats a byte count for quota display. When bytes is 0, returns "∞" (unlimited).
|
||||||
@@ -39,25 +34,33 @@ function formatDateTime(value) {
|
|||||||
} else {
|
} else {
|
||||||
dateValue = new Date(value);
|
dateValue = new Date(value);
|
||||||
}
|
}
|
||||||
if (isNaN(dateValue.getTime())) return String(value);
|
if (Number.isNaN(dateValue.getTime())) return String(value);
|
||||||
return dateValue.toLocaleDateString() + ' ' +
|
return `${dateValue.toLocaleDateString()} ${dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||||
dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateShort(value) {
|
function formatDateShort(value) {
|
||||||
if (!value) return 'N/A';
|
if (!value) return 'N/A';
|
||||||
const dateValue = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
|
const dateValue = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
|
||||||
if (isNaN(dateValue.getTime())) return String(value);
|
if (Number.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) {
|
function isTextViewable(mimeType) {
|
||||||
if (!mimeType) return false;
|
if (!mimeType) return false;
|
||||||
if (mimeType.startsWith('text/')) return true;
|
if (mimeType.startsWith('text/')) return true;
|
||||||
const textTypes = [
|
const textTypes = [
|
||||||
'application/json', 'application/xml', 'application/javascript',
|
'application/json',
|
||||||
'application/x-sh', 'application/x-yaml', 'application/toml',
|
'application/xml',
|
||||||
'application/x-toml', 'application/sql',
|
'application/javascript',
|
||||||
|
'application/x-sh',
|
||||||
|
'application/x-yaml',
|
||||||
|
'application/toml',
|
||||||
|
'application/x-toml',
|
||||||
|
'application/sql'
|
||||||
];
|
];
|
||||||
return textTypes.includes(mimeType);
|
return textTypes.includes(mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-14
@@ -6,10 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Current locale code (default to browser locale if available, fallback to English)
|
// Current locale code (default to browser locale if available, fallback to English)
|
||||||
let currentLocale =
|
let currentLocale = navigator.language?.substring(0, 2) || navigator.userLanguage?.substring(0, 2) || 'en';
|
||||||
(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)
|
// Supported locales (languages that have locale files on the server)
|
||||||
// When a locale file is not found, the system gracefully falls back to English
|
// When a locale file is not found, the system gracefully falls back to English
|
||||||
@@ -62,7 +59,7 @@ function getNestedValue(obj, path) {
|
|||||||
// Try direct key match first
|
// Try direct key match first
|
||||||
if (obj && typeof obj === 'object' && path in obj) {
|
if (obj && typeof obj === 'object' && path in obj) {
|
||||||
const value = obj[path];
|
const value = obj[path];
|
||||||
return (typeof value === 'string') ? value : null;
|
return typeof value === 'string' ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try standard dot notation for nested values
|
// Try standard dot notation for nested values
|
||||||
@@ -87,7 +84,7 @@ function getNestedValue(obj, path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (typeof current === 'string') ? current : null;
|
return typeof current === 'string' ? current : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,6 +93,8 @@ function getNestedValue(obj, path) {
|
|||||||
* @param {object} params - Parameters to replace in the translation (e.g., {name: 'John'})
|
* @param {object} params - Parameters to replace in the translation (e.g., {name: 'John'})
|
||||||
* @returns {string} - The translated string or the key itself if not found
|
* @returns {string} - The translated string or the key itself if not found
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
function t(key, params = {}) {
|
function t(key, params = {}) {
|
||||||
// Get translation from cache
|
// Get translation from cache
|
||||||
const localeData = translations[currentLocale];
|
const localeData = translations[currentLocale];
|
||||||
@@ -134,8 +133,8 @@ function t(key, params = {}) {
|
|||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
// Try fallback to English
|
// Try fallback to English
|
||||||
if (currentLocale !== 'en' && translations['en']) {
|
if (currentLocale !== 'en' && translations.en) {
|
||||||
let fallbackValue = getNestedValue(translations['en'], key);
|
let fallbackValue = getNestedValue(translations.en, key);
|
||||||
|
|
||||||
if (!fallbackValue) {
|
if (!fallbackValue) {
|
||||||
const aliasMap = {
|
const aliasMap = {
|
||||||
@@ -146,7 +145,7 @@ function t(key, params = {}) {
|
|||||||
};
|
};
|
||||||
const aliasKey = aliasMap[key];
|
const aliasKey = aliasMap[key];
|
||||||
if (aliasKey) {
|
if (aliasKey) {
|
||||||
fallbackValue = getNestedValue(translations['en'], aliasKey);
|
fallbackValue = getNestedValue(translations.en, aliasKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,17 +251,17 @@ function translateElement(root) {
|
|||||||
// (e.g. admin.js) shadow the global t() function.
|
// (e.g. admin.js) shadow the global t() function.
|
||||||
const resolve = safeT;
|
const resolve = safeT;
|
||||||
const el = root || document;
|
const el = root || document;
|
||||||
el.querySelectorAll('[data-i18n]').forEach(element => {
|
el.querySelectorAll('[data-i18n]').forEach((element) => {
|
||||||
const key = element.getAttribute('data-i18n');
|
const key = element.getAttribute('data-i18n');
|
||||||
element.textContent = resolve(key);
|
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');
|
const key = element.getAttribute('data-i18n-placeholder');
|
||||||
element.placeholder = resolve(key);
|
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');
|
const key = element.getAttribute('data-i18n-title');
|
||||||
element.title = resolve(key);
|
element.title = resolve(key);
|
||||||
});
|
});
|
||||||
@@ -307,8 +306,8 @@ function safeT(key, params = {}) {
|
|||||||
let value = getNestedValue(localeData, key);
|
let value = getNestedValue(localeData, key);
|
||||||
|
|
||||||
// Fallback to English
|
// Fallback to English
|
||||||
if (!value && currentLocale !== 'en' && translations['en']) {
|
if (!value && currentLocale !== 'en' && translations.en) {
|
||||||
value = getNestedValue(translations['en'], key);
|
value = getNestedValue(translations.en, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) return key;
|
if (!value) return key;
|
||||||
|
|||||||
+377
-112
@@ -10,101 +10,357 @@
|
|||||||
* https://fontawesome.com/license/free
|
* https://fontawesome.com/license/free
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// ── Icon registry ──────────────────────────────────────────────
|
// ── Icon registry ──────────────────────────────────────────────
|
||||||
// Each entry: [viewBox-width, svg-path-d]
|
// Each entry: [viewBox-width, svg-path-d]
|
||||||
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
|
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
|
||||||
// Keys use FA5 class names (without "fa-" prefix) for backward compatibility.
|
// Keys use FA5 class names (without "fa-" prefix) for backward compatibility.
|
||||||
|
|
||||||
const _ICONS = {
|
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"],
|
'arrow-left': [
|
||||||
"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"],
|
448,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'arrows-alt': [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
ban: [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
bars: [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
bell: [
|
||||||
"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"],
|
448,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'bell-slash': [
|
||||||
"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"],
|
640,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
box: [
|
||||||
"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"],
|
448,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
broom: [
|
||||||
"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"],
|
576,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'caret-down': [
|
||||||
"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"],
|
320,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'chart-pie': [
|
||||||
"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"],
|
576,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
check: [
|
||||||
"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"],
|
448,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'check-circle': [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'check-double': [
|
||||||
"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"],
|
448,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'chevron-down': [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'chevron-left': [
|
||||||
"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"],
|
320,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'chevron-right': [
|
||||||
"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"],
|
320,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'circle-notch': [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
clock: [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
cloud: [
|
||||||
"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"],
|
640,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
'cloud-upload-alt': [
|
||||||
"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"],
|
640,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
cog: [
|
||||||
"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"],
|
512,
|
||||||
"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"],
|
'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'
|
||||||
"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"],
|
cogs: [
|
||||||
"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"],
|
640,
|
||||||
"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"],
|
'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).
|
// Icon CSS is now in /css/components/icons.css (loaded via main.css).
|
||||||
@@ -119,9 +375,9 @@ const _ICONS = {
|
|||||||
*/
|
*/
|
||||||
function oxiIcon(name, extraClass) {
|
function oxiIcon(name, extraClass) {
|
||||||
const entry = _ICONS[name];
|
const entry = _ICONS[name];
|
||||||
if (!entry) return "";
|
if (!entry) return '';
|
||||||
const [w, d] = entry;
|
const [w, d] = entry;
|
||||||
const cls = extraClass ? "oxi-icon " + extraClass : "oxi-icon";
|
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>`;
|
return `<svg class="${cls}" viewBox="0 0 ${w} 512" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +392,7 @@ function oxiIcon(name, extraClass) {
|
|||||||
*/
|
*/
|
||||||
function replaceIconsInElement(container) {
|
function replaceIconsInElement(container) {
|
||||||
if (!container) container = document.body;
|
if (!container) container = document.body;
|
||||||
const icons = container.querySelectorAll("i[class*=\"fa-\"]");
|
const icons = container.querySelectorAll('i[class*="fa-"]');
|
||||||
for (let i = 0; i < icons.length; i++) {
|
for (let i = 0; i < icons.length; i++) {
|
||||||
const el = icons[i];
|
const el = icons[i];
|
||||||
const classes = el.className.split(/\s+/);
|
const classes = el.className.split(/\s+/);
|
||||||
@@ -149,18 +405,24 @@ function replaceIconsInElement(container) {
|
|||||||
let isRegular = false; // far = outline variant
|
let isRegular = false; // far = outline variant
|
||||||
|
|
||||||
for (const cls of classes) {
|
for (const cls of classes) {
|
||||||
if (cls === "fa-spin") { isSpin = true; continue; }
|
if (cls === 'fa-spin') {
|
||||||
if (cls === "far") { isRegular = true; continue; }
|
isSpin = true;
|
||||||
if (cls.startsWith("fa-") && cls !== "fa") {
|
continue;
|
||||||
|
}
|
||||||
|
if (cls === 'far') {
|
||||||
|
isRegular = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cls.startsWith('fa-') && cls !== 'fa') {
|
||||||
iconName = cls.substring(3); // strip "fa-"
|
iconName = cls.substring(3); // strip "fa-"
|
||||||
} else if (cls !== "fas" && cls !== "fab" && cls !== "fa") {
|
} else if (cls !== 'fas' && cls !== 'fab' && cls !== 'fa') {
|
||||||
extraClasses.push(cls);
|
extraClasses.push(cls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use outline variant if available and element uses "far"
|
// Use outline variant if available and element uses "far"
|
||||||
if (isRegular && _ICONS[iconName + "-outline"]) {
|
if (isRegular && _ICONS[`${iconName}-outline`]) {
|
||||||
iconName = iconName + "-outline";
|
iconName = `${iconName}-outline`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iconName || !_ICONS[iconName]) continue;
|
if (!iconName || !_ICONS[iconName]) continue;
|
||||||
@@ -168,14 +430,14 @@ function replaceIconsInElement(container) {
|
|||||||
const [w, d] = _ICONS[iconName];
|
const [w, d] = _ICONS[iconName];
|
||||||
|
|
||||||
// Build SVG element
|
// Build SVG element
|
||||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
svg.setAttribute("viewBox", "0 0 " + w + " 512");
|
svg.setAttribute('viewBox', `0 0 ${w} 512`);
|
||||||
svg.setAttribute("aria-hidden", "true");
|
svg.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
let svgClass = "oxi-icon";
|
let svgClass = 'oxi-icon';
|
||||||
if (isSpin) svgClass += " oxi-icon-spin";
|
if (isSpin) svgClass += ' oxi-icon-spin';
|
||||||
if (extraClasses.length) svgClass += " " + extraClasses.join(" ");
|
if (extraClasses.length) svgClass += ` ${extraClasses.join(' ')}`;
|
||||||
svg.setAttribute("class", svgClass);
|
svg.setAttribute('class', svgClass);
|
||||||
|
|
||||||
// Copy inline style if present
|
// Copy inline style if present
|
||||||
if (el.style.cssText) svg.style.cssText = el.style.cssText;
|
if (el.style.cssText) svg.style.cssText = el.style.cssText;
|
||||||
@@ -183,9 +445,9 @@ function replaceIconsInElement(container) {
|
|||||||
// Preserve id attribute if present
|
// Preserve id attribute if present
|
||||||
if (el.id) svg.id = el.id;
|
if (el.id) svg.id = el.id;
|
||||||
|
|
||||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
path.setAttribute("fill", "currentColor");
|
path.setAttribute('fill', 'currentColor');
|
||||||
path.setAttribute("d", d);
|
path.setAttribute('d', d);
|
||||||
svg.appendChild(path);
|
svg.appendChild(path);
|
||||||
|
|
||||||
el.replaceWith(svg);
|
el.replaceWith(svg);
|
||||||
@@ -202,17 +464,20 @@ window.OxiIcons = _ICONS;
|
|||||||
// to inline SVGs automatically. Debounced to at most once per frame.
|
// to inline SVGs automatically. Debounced to at most once per frame.
|
||||||
(function autoReplace() {
|
(function autoReplace() {
|
||||||
let raf = 0;
|
let raf = 0;
|
||||||
const scan = () => { raf = 0; replaceIconsInElement(); };
|
const scan = () => {
|
||||||
|
raf = 0;
|
||||||
|
replaceIconsInElement();
|
||||||
|
};
|
||||||
|
|
||||||
// Initial sweep once the DOM is ready
|
// Initial sweep once the DOM is ready
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener("DOMContentLoaded", scan);
|
document.addEventListener('DOMContentLoaded', scan);
|
||||||
} else {
|
} else {
|
||||||
scan();
|
scan();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe future mutations (dynamic renders, modals, etc.)
|
// Observe future mutations (dynamic renders, modals, etc.)
|
||||||
new MutationObserver(function (mutations) {
|
new MutationObserver((mutations) => {
|
||||||
if (raf) return;
|
if (raf) return;
|
||||||
for (let i = 0; i < mutations.length; i++) {
|
for (let i = 0; i < mutations.length; i++) {
|
||||||
if (mutations[i].addedNodes.length) {
|
if (mutations[i].addedNodes.length) {
|
||||||
|
|||||||
@@ -5,18 +5,13 @@
|
|||||||
|
|
||||||
// Locale files that actually exist (have full translations)
|
// Locale files that actually exist (have full translations)
|
||||||
// Keep this list in sync when adding new locale JSON files
|
// Keep this list in sync when adding new locale JSON files
|
||||||
const AVAILABLE_LOCALES = new Set([
|
const AVAILABLE_LOCALES = new Set(['en', 'es', 'zh', 'fa', 'fr', 'de', 'pt', 'it', 'nl', 'hi', 'ar', 'ru', 'ja', 'ko']);
|
||||||
'en', 'es', 'zh', 'fa', 'fr', 'de', 'pt', 'it', 'nl',
|
|
||||||
'hi', 'ar', 'ru', 'ja', 'ko'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Language codes, names, and flag emojis
|
// Language codes, names, and flag emojis
|
||||||
// Only returns languages that have a real locale file
|
// Only returns languages that have a real locale file
|
||||||
function getAvailableLanguages() {
|
function getAvailableLanguages() {
|
||||||
if (typeof ALL_LANGUAGES !== 'undefined') {
|
if (typeof ALL_LANGUAGES !== 'undefined') {
|
||||||
return ALL_LANGUAGES
|
return ALL_LANGUAGES.filter((l) => AVAILABLE_LOCALES.has(l.code)).map((l) => ({ code: l.code, name: l.nativeName, flag: l.flag }));
|
||||||
.filter(l => AVAILABLE_LOCALES.has(l.code))
|
|
||||||
.map(l => ({ code: l.code, name: l.nativeName, flag: l.flag }));
|
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
||||||
@@ -69,7 +64,7 @@ function createLanguageSelector(containerId = 'language-selector') {
|
|||||||
// Get current language
|
// Get current language
|
||||||
const languages = getAvailableLanguages();
|
const languages = getAvailableLanguages();
|
||||||
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
|
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
|
// Set initial HTML attributes
|
||||||
updateHtmlAttributes(currentLocale);
|
updateHtmlAttributes(currentLocale);
|
||||||
@@ -93,7 +88,7 @@ function createLanguageSelector(containerId = 'language-selector') {
|
|||||||
dropdown.setAttribute('role', 'listbox');
|
dropdown.setAttribute('role', 'listbox');
|
||||||
|
|
||||||
// Add language options
|
// Add language options
|
||||||
languages.forEach(lang => {
|
languages.forEach((lang) => {
|
||||||
const option = document.createElement('div');
|
const option = document.createElement('div');
|
||||||
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
||||||
option.setAttribute('role', 'option');
|
option.setAttribute('role', 'option');
|
||||||
@@ -204,7 +199,7 @@ async function selectLanguage(langCode, container) {
|
|||||||
*/
|
*/
|
||||||
function updateSelectedLanguage(langCode, container) {
|
function updateSelectedLanguage(langCode, container) {
|
||||||
const languages = getAvailableLanguages();
|
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
|
// Update toggle button text
|
||||||
const langCodeSpan = container.querySelector('.lang-code');
|
const langCodeSpan = container.querySelector('.lang-code');
|
||||||
@@ -214,7 +209,7 @@ function updateSelectedLanguage(langCode, container) {
|
|||||||
|
|
||||||
// Update active state on options
|
// Update active state on options
|
||||||
const options = container.querySelectorAll('.language-option');
|
const options = container.querySelectorAll('.language-option');
|
||||||
options.forEach(option => {
|
options.forEach((option) => {
|
||||||
const isActive = option.getAttribute('data-lang') === langCode;
|
const isActive = option.getAttribute('data-lang') === langCode;
|
||||||
option.classList.toggle('active', isActive);
|
option.classList.toggle('active', isActive);
|
||||||
option.setAttribute('aria-selected', isActive);
|
option.setAttribute('aria-selected', isActive);
|
||||||
|
|||||||
@@ -77,15 +77,7 @@ const Modal = {
|
|||||||
*/
|
*/
|
||||||
prompt(options = {}) {
|
prompt(options = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const {
|
const { title = 'Input', label = '', placeholder = '', value = '', icon = 'fa-keyboard', confirmText = null, cancelText = null } = options;
|
||||||
title = 'Input',
|
|
||||||
label = '',
|
|
||||||
placeholder = '',
|
|
||||||
value = '',
|
|
||||||
icon = 'fa-keyboard',
|
|
||||||
confirmText = null,
|
|
||||||
cancelText = null
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Set modal content - update the icon
|
// Set modal content - update the icon
|
||||||
const iconContainer = document.querySelector('.modal-icon');
|
const iconContainer = document.querySelector('.modal-icon');
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const notifications = (() => {
|
const notifications = (() => {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* ── state ──────────────────────────────────────────────── */
|
/* ── state ──────────────────────────────────────────────── */
|
||||||
let _badgeCount = 0;
|
let _badgeCount = 0;
|
||||||
let _batchSeq = 0;
|
let _batchSeq = 0;
|
||||||
@@ -81,10 +79,10 @@ const notifications = (() => {
|
|||||||
const badge = $('notif-badge');
|
const badge = $('notif-badge');
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
if (_badgeCount > 0) {
|
if (_badgeCount > 0) {
|
||||||
badge.classList.remove("hidden");
|
badge.classList.remove('hidden');
|
||||||
badge.textContent = _badgeCount > 99 ? '99+' : _badgeCount;
|
badge.textContent = _badgeCount > 99 ? '99+' : _badgeCount;
|
||||||
} else {
|
} else {
|
||||||
badge.classList.add("hidden");
|
badge.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function _ringBell() {
|
function _ringBell() {
|
||||||
@@ -126,7 +124,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// If panel is closed, bump badge
|
// If panel is closed, bump badge
|
||||||
const wrapper = $('notif-wrapper');
|
const wrapper = $('notif-wrapper');
|
||||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
if (!wrapper?.classList.contains('open')) {
|
||||||
_incrementBadge();
|
_incrementBadge();
|
||||||
}
|
}
|
||||||
_showEmptyIfNeeded();
|
_showEmptyIfNeeded();
|
||||||
@@ -141,7 +139,7 @@ const notifications = (() => {
|
|||||||
* @param {string} [folderName] root folder name (for folder uploads)
|
* @param {string} [folderName] root folder name (for folder uploads)
|
||||||
*/
|
*/
|
||||||
function addUploadBatch(totalFiles, folderName) {
|
function addUploadBatch(totalFiles, folderName) {
|
||||||
const batchId = 'batch-' + (++_batchSeq);
|
const batchId = `batch-${++_batchSeq}`;
|
||||||
const body = $('notif-panel-body');
|
const body = $('notif-panel-body');
|
||||||
if (!body) return batchId;
|
if (!body) return batchId;
|
||||||
|
|
||||||
@@ -150,9 +148,7 @@ const notifications = (() => {
|
|||||||
item.id = batchId;
|
item.id = batchId;
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = window.i18n?.t || ((k) => k);
|
||||||
const uploadingText = folderName
|
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
|
||||||
? `📁 ${t('upload.uploading')} ${_esc(folderName)}…`
|
|
||||||
: t('upload.uploading');
|
|
||||||
const filesLabel = t('upload.files');
|
const filesLabel = t('upload.files');
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
@@ -219,11 +215,9 @@ const notifications = (() => {
|
|||||||
if (!shouldUpdate) return;
|
if (!shouldUpdate) return;
|
||||||
|
|
||||||
// Show just the file name being uploaded (truncate long paths)
|
// Show just the file name being uploaded (truncate long paths)
|
||||||
const curEl = $(batchId + '-current');
|
const curEl = $(`${batchId}-current`);
|
||||||
if (curEl) {
|
if (curEl) {
|
||||||
const shortName = fileName.length > 50
|
const shortName = fileName.length > 50 ? `…${fileName.slice(-49)}` : fileName;
|
||||||
? '…' + fileName.slice(-49)
|
|
||||||
: fileName;
|
|
||||||
curEl.textContent = shortName;
|
curEl.textContent = shortName;
|
||||||
}
|
}
|
||||||
batch.lastLabelFile = fileName;
|
batch.lastLabelFile = fileName;
|
||||||
@@ -231,13 +225,11 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// Update progress bar with per-file granularity:
|
// Update progress bar with per-file granularity:
|
||||||
// overall% = (completed_files + current_file_fraction) / total_files
|
// overall% = (completed_files + current_file_fraction) / total_files
|
||||||
const overallPct = Math.round(
|
const overallPct = Math.round(((batch.completed + pct / 100) / batch.totalFiles) * 100);
|
||||||
((batch.completed + (pct / 100)) / batch.totalFiles) * 100
|
const fillEl = $(`${batchId}-fill`);
|
||||||
);
|
const pctEl = $(`${batchId}-pct`);
|
||||||
const fillEl = $(batchId + '-fill');
|
if (fillEl) fillEl.style.width = `${overallPct}%`;
|
||||||
const pctEl = $(batchId + '-pct');
|
if (pctEl) pctEl.textContent = `${overallPct}%`;
|
||||||
if (fillEl) fillEl.style.width = overallPct + '%';
|
|
||||||
if (pctEl) pctEl.textContent = overallPct + '%';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,15 +248,15 @@ const notifications = (() => {
|
|||||||
if (!isLast && batch.completed % 5 !== 0) return;
|
if (!isLast && batch.completed % 5 !== 0) return;
|
||||||
|
|
||||||
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
||||||
const fillEl = $(batchId + '-fill');
|
const fillEl = $(`${batchId}-fill`);
|
||||||
const pctEl = $(batchId + '-pct');
|
const pctEl = $(`${batchId}-pct`);
|
||||||
const statsEl = $(batchId + '-stats');
|
const statsEl = $(`${batchId}-stats`);
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = window.i18n?.t || ((k) => k);
|
||||||
const filesLabel = t('upload.files');
|
const filesLabel = t('upload.files');
|
||||||
|
|
||||||
if (fillEl) fillEl.style.width = pctVal + '%';
|
if (fillEl) fillEl.style.width = `${pctVal}%`;
|
||||||
if (pctEl) pctEl.textContent = pctVal + '%';
|
if (pctEl) pctEl.textContent = `${pctVal}%`;
|
||||||
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +267,7 @@ const notifications = (() => {
|
|||||||
const batch = _batches[batchId];
|
const batch = _batches[batchId];
|
||||||
if (!batch) return;
|
if (!batch) return;
|
||||||
|
|
||||||
const fillEl = $(batchId + '-fill');
|
const fillEl = $(`${batchId}-fill`);
|
||||||
if (fillEl) {
|
if (fillEl) {
|
||||||
fillEl.style.width = '100%';
|
fillEl.style.width = '100%';
|
||||||
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
||||||
@@ -285,12 +277,14 @@ const notifications = (() => {
|
|||||||
const iconEl = batch.el.querySelector('.notif-item-icon');
|
const iconEl = batch.el.querySelector('.notif-item-icon');
|
||||||
|
|
||||||
// Clear the current-file label
|
// Clear the current-file label
|
||||||
const curEl = $(batchId + '-current');
|
const curEl = $(`${batchId}-current`);
|
||||||
if (curEl) curEl.textContent = '';
|
if (curEl) curEl.textContent = '';
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = window.i18n?.t || ((k) => k);
|
||||||
const filesLabel = t('upload.files');
|
const completeText = t('upload.complete', {
|
||||||
const completeText = t('upload.complete', { count: successCount, total: totalFiles });
|
count: successCount,
|
||||||
|
total: totalFiles
|
||||||
|
});
|
||||||
if (titleEl) titleEl.textContent = completeText;
|
if (titleEl) titleEl.textContent = completeText;
|
||||||
|
|
||||||
if (iconEl) {
|
if (iconEl) {
|
||||||
@@ -305,7 +299,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// If the panel is closed, bump badge
|
// If the panel is closed, bump badge
|
||||||
const wrapper = $('notif-wrapper');
|
const wrapper = $('notif-wrapper');
|
||||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
if (!wrapper?.classList.contains('open')) {
|
||||||
_incrementBadge();
|
_incrementBadge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,14 +309,15 @@ const notifications = (() => {
|
|||||||
const body = $('notif-panel-body');
|
const body = $('notif-panel-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
// Remove all notif-items
|
// Remove all notif-items
|
||||||
body.querySelectorAll('.notif-item').forEach(el => el.remove());
|
body.querySelectorAll('.notif-item').forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
_clearBadge();
|
_clearBadge();
|
||||||
_showEmptyIfNeeded();
|
_showEmptyIfNeeded();
|
||||||
// automatically close notification center on clear
|
// automatically close notification center on clear
|
||||||
setTimeout(
|
setTimeout(() => {
|
||||||
()=> { close(); },
|
close();
|
||||||
600
|
}, 600);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── util ───────────────────────────────────────────────── */
|
/* ── util ───────────────────────────────────────────────── */
|
||||||
@@ -352,7 +347,7 @@ const notifications = (() => {
|
|||||||
fileCompleted,
|
fileCompleted,
|
||||||
finishBatch,
|
finishBatch,
|
||||||
addNotification,
|
addNotification,
|
||||||
clear,
|
clear
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
// Service Worker registration — runs after page load.
|
// Service Worker registration — runs after page load.
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', () => {
|
||||||
navigator.serviceWorker.register('/sw.js')
|
navigator.serviceWorker
|
||||||
.then(function () { /* registered */ })
|
.register('/sw.js')
|
||||||
.catch(function (err) { console.log('Service Worker registration failed:', err); });
|
.then(() => {
|
||||||
|
/* registered */
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('Service Worker registration failed:', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+266
-63
@@ -107,52 +107,242 @@ const LANGUAGE_TEXTS = {
|
|||||||
moreLanguages: '더 많은 언어...',
|
moreLanguages: '더 많은 언어...',
|
||||||
modalTitle: '언어 선택',
|
modalTitle: '언어 선택',
|
||||||
searchPlaceholder: '언어 검색...'
|
searchPlaceholder: '언어 검색...'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Complete language registry — add new languages here, they'll appear automatically
|
// Complete language registry — add new languages here, they'll appear automatically
|
||||||
// `popular: true` languages show as cards on the main screen, the rest in the modal
|
// `popular: true` languages show as cards on the main screen, the rest in the modal
|
||||||
const ALL_LANGUAGES = [
|
const ALL_LANGUAGES = [
|
||||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧', popular: true },
|
{
|
||||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸', popular: true },
|
code: 'en',
|
||||||
{ code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳', popular: true },
|
name: 'English',
|
||||||
{ code: 'fa', name: 'Persian', nativeName: 'فارسی', flag: '🇮🇷', popular: true },
|
nativeName: 'English',
|
||||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷', popular: true },
|
flag: '🇬🇧',
|
||||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', popular: true },
|
popular: true
|
||||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷', popular: true },
|
},
|
||||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', popular: true },
|
{
|
||||||
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺', popular: true },
|
code: 'es',
|
||||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵', popular: true },
|
name: 'Spanish',
|
||||||
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷', popular: true },
|
nativeName: 'Español',
|
||||||
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', popular: true },
|
flag: '🇪🇸',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'zh',
|
||||||
|
name: 'Chinese',
|
||||||
|
nativeName: '中文',
|
||||||
|
flag: '🇨🇳',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fa',
|
||||||
|
name: 'Persian',
|
||||||
|
nativeName: 'فارسی',
|
||||||
|
flag: '🇮🇷',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fr',
|
||||||
|
name: 'French',
|
||||||
|
nativeName: 'Français',
|
||||||
|
flag: '🇫🇷',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'de',
|
||||||
|
name: 'German',
|
||||||
|
nativeName: 'Deutsch',
|
||||||
|
flag: '🇩🇪',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pt',
|
||||||
|
name: 'Portuguese',
|
||||||
|
nativeName: 'Português',
|
||||||
|
flag: '🇧🇷',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'it',
|
||||||
|
name: 'Italian',
|
||||||
|
nativeName: 'Italiano',
|
||||||
|
flag: '🇮🇹',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ru',
|
||||||
|
name: 'Russian',
|
||||||
|
nativeName: 'Русский',
|
||||||
|
flag: '🇷🇺',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ja',
|
||||||
|
name: 'Japanese',
|
||||||
|
nativeName: '日本語',
|
||||||
|
flag: '🇯🇵',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ko',
|
||||||
|
name: 'Korean',
|
||||||
|
nativeName: '한국어',
|
||||||
|
flag: '🇰🇷',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ar',
|
||||||
|
name: 'Arabic',
|
||||||
|
nativeName: 'العربية',
|
||||||
|
flag: '🇸🇦',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', popular: true },
|
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', popular: true },
|
||||||
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷', popular: false },
|
{
|
||||||
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱', popular: false },
|
code: 'tr',
|
||||||
{ code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱', popular: false },
|
name: 'Turkish',
|
||||||
{ code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪', popular: false },
|
nativeName: 'Türkçe',
|
||||||
{ code: 'da', name: 'Danish', nativeName: 'Dansk', flag: '🇩🇰', popular: false },
|
flag: '🇹🇷',
|
||||||
{ code: 'fi', name: 'Finnish', nativeName: 'Suomi', flag: '🇫🇮', popular: false },
|
popular: false
|
||||||
{ code: 'no', name: 'Norwegian', nativeName: 'Norsk', flag: '🇳🇴', popular: false },
|
},
|
||||||
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦', popular: false },
|
{
|
||||||
{ code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿', popular: false },
|
code: 'nl',
|
||||||
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷', popular: false },
|
name: 'Dutch',
|
||||||
{ code: 'he', name: 'Hebrew', nativeName: 'עברית', flag: '🇮🇱', popular: false },
|
nativeName: 'Nederlands',
|
||||||
|
flag: '🇳🇱',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pl',
|
||||||
|
name: 'Polish',
|
||||||
|
nativeName: 'Polski',
|
||||||
|
flag: '🇵🇱',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'sv',
|
||||||
|
name: 'Swedish',
|
||||||
|
nativeName: 'Svenska',
|
||||||
|
flag: '🇸🇪',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'da',
|
||||||
|
name: 'Danish',
|
||||||
|
nativeName: 'Dansk',
|
||||||
|
flag: '🇩🇰',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fi',
|
||||||
|
name: 'Finnish',
|
||||||
|
nativeName: 'Suomi',
|
||||||
|
flag: '🇫🇮',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'no',
|
||||||
|
name: 'Norwegian',
|
||||||
|
nativeName: 'Norsk',
|
||||||
|
flag: '🇳🇴',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'uk',
|
||||||
|
name: 'Ukrainian',
|
||||||
|
nativeName: 'Українська',
|
||||||
|
flag: '🇺🇦',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'cs',
|
||||||
|
name: 'Czech',
|
||||||
|
nativeName: 'Čeština',
|
||||||
|
flag: '🇨🇿',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'el',
|
||||||
|
name: 'Greek',
|
||||||
|
nativeName: 'Ελληνικά',
|
||||||
|
flag: '🇬🇷',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'he',
|
||||||
|
name: 'Hebrew',
|
||||||
|
nativeName: 'עברית',
|
||||||
|
flag: '🇮🇱',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭', popular: false },
|
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭', popular: false },
|
||||||
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳', popular: false },
|
{
|
||||||
{ code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩', popular: false },
|
code: 'vi',
|
||||||
{ code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu', flag: '🇲🇾', popular: false },
|
name: 'Vietnamese',
|
||||||
{ code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴', popular: false },
|
nativeName: 'Tiếng Việt',
|
||||||
{ code: 'hu', name: 'Hungarian', nativeName: 'Magyar', flag: '🇭🇺', popular: false },
|
flag: '🇻🇳',
|
||||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '🏴', popular: false },
|
popular: false
|
||||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '🏴', popular: false },
|
},
|
||||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '🏴', popular: false },
|
{
|
||||||
|
code: 'id',
|
||||||
|
name: 'Indonesian',
|
||||||
|
nativeName: 'Bahasa Indonesia',
|
||||||
|
flag: '🇮🇩',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ms',
|
||||||
|
name: 'Malay',
|
||||||
|
nativeName: 'Bahasa Melayu',
|
||||||
|
flag: '🇲🇾',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ro',
|
||||||
|
name: 'Romanian',
|
||||||
|
nativeName: 'Română',
|
||||||
|
flag: '🇷🇴',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'hu',
|
||||||
|
name: 'Hungarian',
|
||||||
|
nativeName: 'Magyar',
|
||||||
|
flag: '🇭🇺',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ca',
|
||||||
|
name: 'Catalan',
|
||||||
|
nativeName: 'Català',
|
||||||
|
flag: '🏴',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'eu',
|
||||||
|
name: 'Basque',
|
||||||
|
nativeName: 'Euskara',
|
||||||
|
flag: '🏴',
|
||||||
|
popular: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'gl',
|
||||||
|
name: 'Galician',
|
||||||
|
nativeName: 'Galego',
|
||||||
|
flag: '🏴',
|
||||||
|
popular: false
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Panel visibility helpers ---
|
// --- Panel visibility helpers ---
|
||||||
// The `.hidden` CSS class uses `display: none !important`, so inline
|
// The `.hidden` CSS class uses `display: none !important`, so inline
|
||||||
// `style.display` can never override it. Always toggle the class instead.
|
// `style.display` can never override it. Always toggle the class instead.
|
||||||
function showPanel(el) { if (el) el.classList.remove('hidden'); }
|
function showPanel(el) {
|
||||||
function hidePanel(el) { if (el) el.classList.add('hidden'); }
|
if (el) el.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function hidePanel(el) {
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a first run (no locale saved)
|
// Check if this is a first run (no locale saved)
|
||||||
function isFirstRun() {
|
function isFirstRun() {
|
||||||
@@ -179,7 +369,7 @@ function detectBrowserLanguage() {
|
|||||||
const browserLangs = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
|
const browserLangs = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
|
||||||
for (const bl of browserLangs) {
|
for (const bl of browserLangs) {
|
||||||
const code = bl.substring(0, 2).toLowerCase();
|
const code = bl.substring(0, 2).toLowerCase();
|
||||||
const match = ALL_LANGUAGES.find(l => l.code === code);
|
const match = ALL_LANGUAGES.find((l) => l.code === code);
|
||||||
if (match) return match;
|
if (match) return match;
|
||||||
}
|
}
|
||||||
return ALL_LANGUAGES[0]; // fallback to English
|
return ALL_LANGUAGES[0]; // fallback to English
|
||||||
@@ -188,7 +378,7 @@ function detectBrowserLanguage() {
|
|||||||
// Build a language option element (card style)
|
// Build a language option element (card style)
|
||||||
function buildLanguageCard(lang, isSelected) {
|
function buildLanguageCard(lang, isSelected) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'lang-picker-item' + (isSelected ? ' selected' : '');
|
item.className = `lang-picker-item${isSelected ? ' selected' : ''}`;
|
||||||
item.setAttribute('data-lang', lang.code);
|
item.setAttribute('data-lang', lang.code);
|
||||||
item.setAttribute('role', 'option');
|
item.setAttribute('role', 'option');
|
||||||
item.setAttribute('aria-selected', isSelected);
|
item.setAttribute('aria-selected', isSelected);
|
||||||
@@ -207,7 +397,6 @@ function initLanguageSelector() {
|
|||||||
const continueBtn = document.getElementById('language-continue');
|
const continueBtn = document.getElementById('language-continue');
|
||||||
const picker = document.getElementById('lang-picker');
|
const picker = document.getElementById('lang-picker');
|
||||||
const pickerSelected = document.getElementById('lang-picker-selected');
|
const pickerSelected = document.getElementById('lang-picker-selected');
|
||||||
const pickerDropdown = document.getElementById('lang-picker-dropdown');
|
|
||||||
const pickerList = document.getElementById('lang-picker-list');
|
const pickerList = document.getElementById('lang-picker-list');
|
||||||
const pickerFlag = document.getElementById('lang-picker-flag');
|
const pickerFlag = document.getElementById('lang-picker-flag');
|
||||||
const pickerName = document.getElementById('lang-picker-name');
|
const pickerName = document.getElementById('lang-picker-name');
|
||||||
@@ -229,11 +418,13 @@ function initLanguageSelector() {
|
|||||||
pickerList.innerHTML = '';
|
pickerList.innerHTML = '';
|
||||||
const filterLower = filter.toLowerCase();
|
const filterLower = filter.toLowerCase();
|
||||||
|
|
||||||
const filtered = ALL_LANGUAGES.filter(lang => {
|
const filtered = ALL_LANGUAGES.filter((lang) => {
|
||||||
if (!filter) return true;
|
if (!filter) return true;
|
||||||
return lang.name.toLowerCase().includes(filterLower) ||
|
return (
|
||||||
|
lang.name.toLowerCase().includes(filterLower) ||
|
||||||
lang.nativeName.toLowerCase().includes(filterLower) ||
|
lang.nativeName.toLowerCase().includes(filterLower) ||
|
||||||
lang.code.toLowerCase().includes(filterLower);
|
lang.code.toLowerCase().includes(filterLower)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
@@ -241,7 +432,7 @@ function initLanguageSelector() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered.forEach(lang => {
|
filtered.forEach((lang) => {
|
||||||
const item = buildLanguageCard(lang, lang.code === selectedLanguage);
|
const item = buildLanguageCard(lang, lang.code === selectedLanguage);
|
||||||
item.addEventListener('click', (e) => {
|
item.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -291,7 +482,8 @@ function initLanguageSelector() {
|
|||||||
pickerSelected.addEventListener('keydown', (e) => {
|
pickerSelected.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (picker.classList.contains('open')) closePicker(); else openPicker();
|
if (picker.classList.contains('open')) closePicker();
|
||||||
|
else openPicker();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
closePicker();
|
closePicker();
|
||||||
}
|
}
|
||||||
@@ -317,7 +509,7 @@ function initLanguageSelector() {
|
|||||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||||
|
|
||||||
// Update i18n if available
|
// Update i18n if available
|
||||||
if (window.i18n && window.i18n.setLocale) {
|
if (window.i18n?.setLocale) {
|
||||||
await window.i18n.setLocale(selectedLanguage);
|
await window.i18n.setLocale(selectedLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,9 +632,7 @@ async function configureOidcLoginUI() {
|
|||||||
// Update button text with provider name
|
// Update button text with provider name
|
||||||
const btnTextEl = oidcBtn.querySelector('span');
|
const btnTextEl = oidcBtn.querySelector('span');
|
||||||
if (btnTextEl && oidcInfo.provider_name) {
|
if (btnTextEl && oidcInfo.provider_name) {
|
||||||
const template = (window.i18n && window.i18n.t)
|
const template = window.i18n?.t ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
||||||
? window.i18n.t('auth.sso_login_provider')
|
|
||||||
: 'Sign in with {{provider}}';
|
|
||||||
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +657,7 @@ async function configureOidcLoginUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
let loginPanel, registerPanel, adminSetupPanel, languagePanel;
|
let loginPanel, registerPanel, adminSetupPanel;
|
||||||
let loginForm, registerForm, adminSetupForm;
|
let loginForm, registerForm, adminSetupForm;
|
||||||
let loginError, registerError, registerSuccess, adminSetupError;
|
let loginError, registerError, registerSuccess, adminSetupError;
|
||||||
|
|
||||||
@@ -534,7 +724,7 @@ let authInitialized = false;
|
|||||||
// and clear auth data to break the loop
|
// and clear auth data to break the loop
|
||||||
(() => {
|
(() => {
|
||||||
// Check if we're being redirected in a loop
|
// Check if we're being redirected in a loop
|
||||||
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0');
|
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0', 10);
|
||||||
const redirectSource = new URLSearchParams(window.location.search).get('source');
|
const redirectSource = new URLSearchParams(window.location.search).get('source');
|
||||||
|
|
||||||
// Case 1: High refresh attempts
|
// Case 1: High refresh attempts
|
||||||
@@ -558,10 +748,11 @@ let authInitialized = false;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Case 3: Multiple redirects in short time
|
// Case 3: Multiple redirects in short time
|
||||||
const lastCleanup = parseInt(localStorage.getItem('last_emergency_clean') || '0');
|
const lastCleanup = parseInt(localStorage.getItem('last_emergency_clean') || '0', 10);
|
||||||
const timeSinceCleanup = Date.now() - lastCleanup;
|
const timeSinceCleanup = Date.now() - lastCleanup;
|
||||||
|
|
||||||
if (lastCleanup > 0 && timeSinceCleanup < 10000) { // Less than 10 seconds
|
if (lastCleanup > 0 && timeSinceCleanup < 10000) {
|
||||||
|
// Less than 10 seconds
|
||||||
console.warn('Multiple auth problems in short time, clearing auth data');
|
console.warn('Multiple auth problems in short time, clearing auth data');
|
||||||
localStorage.removeItem(USER_DATA_KEY);
|
localStorage.removeItem(USER_DATA_KEY);
|
||||||
}
|
}
|
||||||
@@ -624,9 +815,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Show appropriate panel (language selector on first run, login otherwise, or admin setup if no admin)
|
// Show appropriate panel (language selector on first run, login otherwise, or admin setup if no admin)
|
||||||
// This is async so we call it and let it run
|
// This is async so we call it and let it run
|
||||||
showInitialPanel().then(() => {
|
showInitialPanel()
|
||||||
|
.then(() => {
|
||||||
console.log('Initial panel shown based on system status');
|
console.log('Initial panel shown based on system status');
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
console.error('Error showing initial panel:', err);
|
console.error('Error showing initial panel:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -641,7 +834,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Check if we already have a valid session (cookie-based).
|
// Check if we already have a valid session (cookie-based).
|
||||||
// The HttpOnly cookie is sent automatically — just probe /api/auth/me.
|
// The HttpOnly cookie is sent automatically — just probe /api/auth/me.
|
||||||
try {
|
try {
|
||||||
const meResp = await fetch(ME_ENDPOINT, { method: 'GET', credentials: 'same-origin' });
|
const meResp = await fetch(ME_ENDPOINT, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (meResp.ok) {
|
if (meResp.ok) {
|
||||||
console.log('Session still valid, redirecting to app');
|
console.log('Session still valid, redirecting to app');
|
||||||
const userData = await meResp.json();
|
const userData = await meResp.json();
|
||||||
@@ -734,7 +930,7 @@ if (isLoginPage && registerForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await register(username, email, password);
|
await register(username, email, password);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||||
@@ -795,7 +991,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
const err = await response.json().catch(() => ({}));
|
const err = await response.json().catch(() => ({}));
|
||||||
throw new Error(err.message || 'Setup failed');
|
throw new Error(err.message || 'Setup failed');
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
await response.json();
|
||||||
|
|
||||||
// Show success message in the GUI instead of alert
|
// Show success message in the GUI instead of alert
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||||
@@ -811,7 +1007,6 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
hidePanel(adminSetupPanel);
|
hidePanel(adminSetupPanel);
|
||||||
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
||||||
adminSetupError.textContent = error.message || errorMsg;
|
adminSetupError.textContent = error.message || errorMsg;
|
||||||
@@ -852,7 +1047,7 @@ async function login(username, password) {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Authentication failed');
|
throw new Error(errorData.error || 'Authentication failed');
|
||||||
} catch (jsonError) {
|
} catch (_jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
|
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -861,7 +1056,7 @@ async function login(username, password) {
|
|||||||
// Parse the JSON response
|
// Parse the JSON response
|
||||||
try {
|
try {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Login successful, received data");
|
console.log('Login successful, received data');
|
||||||
return data;
|
return data;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error('Error parsing login response:', jsonError);
|
console.error('Error parsing login response:', jsonError);
|
||||||
@@ -896,7 +1091,7 @@ async function register(username, email, password, role = 'user') {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Registration error');
|
throw new Error(errorData.error || 'Registration error');
|
||||||
} catch (jsonError) {
|
} catch (_jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
|
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -905,7 +1100,7 @@ async function register(username, email, password, role = 'user') {
|
|||||||
// Parse the JSON response
|
// Parse the JSON response
|
||||||
try {
|
try {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Registration successful, received data");
|
console.log('Registration successful, received data');
|
||||||
return data;
|
return data;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error('Error parsing registration response:', jsonError);
|
console.error('Error parsing registration response:', jsonError);
|
||||||
@@ -920,6 +1115,8 @@ async function register(username, email, password, role = 'user') {
|
|||||||
/**
|
/**
|
||||||
* Fetch current user data — relies on HttpOnly cookie (auto-sent).
|
* Fetch current user data — relies on HttpOnly cookie (auto-sent).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
async function fetchUserData() {
|
async function fetchUserData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(ME_ENDPOINT, {
|
const response = await fetch(ME_ENDPOINT, {
|
||||||
@@ -946,7 +1143,7 @@ async function fetchUserData() {
|
|||||||
async function refreshAuthToken() {
|
async function refreshAuthToken() {
|
||||||
try {
|
try {
|
||||||
// Loop-breaker
|
// Loop-breaker
|
||||||
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0');
|
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0', 10);
|
||||||
localStorage.setItem('refresh_attempts', (refreshAttempts + 1).toString());
|
localStorage.setItem('refresh_attempts', (refreshAttempts + 1).toString());
|
||||||
|
|
||||||
if (refreshAttempts > 3) {
|
if (refreshAttempts > 3) {
|
||||||
@@ -1003,7 +1200,7 @@ async function refreshAuthToken() {
|
|||||||
*/
|
*/
|
||||||
async function checkFirstRun() {
|
async function checkFirstRun() {
|
||||||
try {
|
try {
|
||||||
console.log("Checking if this is first run");
|
console.log('Checking if this is first run');
|
||||||
|
|
||||||
// Skip the actual check - we'll assume it's not the first run
|
// Skip the actual check - we'll assume it's not the first run
|
||||||
// This avoids making the test request that's getting 403 Forbidden
|
// This avoids making the test request that's getting 403 Forbidden
|
||||||
@@ -1036,9 +1233,15 @@ function redirectToMainApp() {
|
|||||||
/**
|
/**
|
||||||
* Logout — tell the server to clear HttpOnly cookies, then redirect.
|
* Logout — tell the server to clear HttpOnly cookies, then redirect.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
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 (e) {
|
} catch (e) {
|
||||||
console.warn('Logout request failed:', e);
|
console.warn('Logout request failed:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ const contextMenus = {
|
|||||||
if (!label) return;
|
if (!label) return;
|
||||||
label.textContent = window.i18n
|
label.textContent = window.i18n
|
||||||
? window.i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite')
|
? window.i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite')
|
||||||
: (isFavorite ? 'Remove from favorites' : 'Add to favorites');
|
: isFavorite
|
||||||
|
? 'Remove from favorites'
|
||||||
|
: 'Add to favorites';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,13 +25,10 @@ const contextMenus = {
|
|||||||
const wopiEditTab = document.getElementById('wopi-edit-file-tab-option');
|
const wopiEditTab = document.getElementById('wopi-edit-file-tab-option');
|
||||||
if (!wopiEdit || !wopiEditTab) return;
|
if (!wopiEdit || !wopiEditTab) return;
|
||||||
|
|
||||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
const targetFile = window.app?.contextMenuTargetFile;
|
||||||
// Don't show WOPI editor for image files - they should use inline preview
|
// 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 isImage = targetFile?.mime_type?.startsWith('image/');
|
||||||
const show = targetFile &&
|
const show = targetFile && !isImage && window.wopiEditor && (await window.wopiEditor.canEdit(targetFile.name));
|
||||||
!isImage &&
|
|
||||||
window.wopiEditor &&
|
|
||||||
await window.wopiEditor.canEdit(targetFile.name);
|
|
||||||
|
|
||||||
wopiEdit.classList.toggle('hidden', !show);
|
wopiEdit.classList.toggle('hidden', !show);
|
||||||
wopiEditTab.classList.toggle('hidden', !show);
|
wopiEditTab.classList.toggle('hidden', !show);
|
||||||
@@ -38,8 +37,8 @@ const contextMenus = {
|
|||||||
syncFavoriteOptionLabels() {
|
syncFavoriteOptionLabels() {
|
||||||
if (!window.favorites) return;
|
if (!window.favorites) return;
|
||||||
|
|
||||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
const targetFile = window.app?.contextMenuTargetFile;
|
||||||
const targetFolder = window.app && window.app.contextMenuTargetFolder;
|
const targetFolder = window.app?.contextMenuTargetFolder;
|
||||||
|
|
||||||
if (targetFile) {
|
if (targetFile) {
|
||||||
const isFav = window.favorites.isFavorite(targetFile.id, 'file');
|
const isFav = window.favorites.isFavorite(targetFile.id, 'file');
|
||||||
@@ -59,10 +58,7 @@ const contextMenus = {
|
|||||||
// Folder context menu options
|
// Folder context menu options
|
||||||
document.getElementById('download-folder-option').addEventListener('click', () => {
|
document.getElementById('download-folder-option').addEventListener('click', () => {
|
||||||
if (window.app.contextMenuTargetFolder) {
|
if (window.app.contextMenuTargetFolder) {
|
||||||
window.fileOps.downloadFolder(
|
window.fileOps.downloadFolder(window.app.contextMenuTargetFolder.id, window.app.contextMenuTargetFolder.name);
|
||||||
window.app.contextMenuTargetFolder.id,
|
|
||||||
window.app.contextMenuTargetFolder.name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
window.ui.closeContextMenu();
|
window.ui.closeContextMenu();
|
||||||
});
|
});
|
||||||
@@ -72,7 +68,7 @@ const contextMenus = {
|
|||||||
const folder = window.app.contextMenuTargetFolder;
|
const folder = window.app.contextMenuTargetFolder;
|
||||||
|
|
||||||
// Check if folder is already in favorites to toggle
|
// Check if folder is already in favorites to toggle
|
||||||
if (window.favorites && window.favorites.isFavorite(folder.id, 'folder')) {
|
if (window.favorites?.isFavorite(folder.id, 'folder')) {
|
||||||
// Remove from favorites
|
// Remove from favorites
|
||||||
const ok = await window.favorites.removeFromFavorites(folder.id, 'folder');
|
const ok = await window.favorites.removeFromFavorites(folder.id, 'folder');
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
@@ -80,12 +76,7 @@ const contextMenus = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add to favorites
|
// Add to favorites
|
||||||
const ok = await window.favorites.addToFavorites(
|
const ok = await window.favorites.addToFavorites(folder.id, folder.name, 'folder', folder.parent_id);
|
||||||
folder.id,
|
|
||||||
folder.name,
|
|
||||||
'folder',
|
|
||||||
folder.parent_id
|
|
||||||
);
|
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
window.ui.setFavoriteVisualState(folder.id, 'folder', true);
|
window.ui.setFavoriteVisualState(folder.id, 'folder', true);
|
||||||
}
|
}
|
||||||
@@ -130,11 +121,13 @@ const contextMenus = {
|
|||||||
if (window.app.contextMenuTargetFile) {
|
if (window.app.contextMenuTargetFile) {
|
||||||
// Capture reference before context menu cleanup nullifies it
|
// Capture reference before context menu cleanup nullifies it
|
||||||
const file = window.app.contextMenuTargetFile;
|
const file = window.app.contextMenuTargetFile;
|
||||||
fetch(`/api/files/${file.id}?metadata=true`, { credentials: 'same-origin' })
|
fetch(`/api/files/${file.id}?metadata=true`, {
|
||||||
.then(response => response.json())
|
credentials: 'same-origin'
|
||||||
.then(fileDetails => {
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((fileDetails) => {
|
||||||
// Check if viewable file type (images, PDFs, text files)
|
// Check if viewable file type (images, PDFs, text files)
|
||||||
if (window.ui && window.ui.isViewableFile(fileDetails)) {
|
if (window.ui?.isViewableFile(fileDetails)) {
|
||||||
// Open with inline viewer
|
// Open with inline viewer
|
||||||
if (window.inlineViewer) {
|
if (window.inlineViewer) {
|
||||||
window.inlineViewer.openFile(fileDetails);
|
window.inlineViewer.openFile(fileDetails);
|
||||||
@@ -147,7 +140,7 @@ const contextMenus = {
|
|||||||
window.fileOps.downloadFile(file.id, file.name);
|
window.fileOps.downloadFile(file.id, file.name);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error fetching file details:', error);
|
console.error('Error fetching file details:', error);
|
||||||
// On error, fallback to download
|
// On error, fallback to download
|
||||||
window.fileOps.downloadFile(file.id, file.name);
|
window.fileOps.downloadFile(file.id, file.name);
|
||||||
@@ -174,10 +167,7 @@ const contextMenus = {
|
|||||||
|
|
||||||
document.getElementById('download-file-option').addEventListener('click', () => {
|
document.getElementById('download-file-option').addEventListener('click', () => {
|
||||||
if (window.app.contextMenuTargetFile) {
|
if (window.app.contextMenuTargetFile) {
|
||||||
window.fileOps.downloadFile(
|
window.fileOps.downloadFile(window.app.contextMenuTargetFile.id, window.app.contextMenuTargetFile.name);
|
||||||
window.app.contextMenuTargetFile.id,
|
|
||||||
window.app.contextMenuTargetFile.name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
window.ui.closeFileContextMenu();
|
window.ui.closeFileContextMenu();
|
||||||
});
|
});
|
||||||
@@ -187,7 +177,7 @@ const contextMenus = {
|
|||||||
const file = window.app.contextMenuTargetFile;
|
const file = window.app.contextMenuTargetFile;
|
||||||
|
|
||||||
// Check if file is already in favorites to toggle
|
// Check if file is already in favorites to toggle
|
||||||
if (window.favorites && window.favorites.isFavorite(file.id, 'file')) {
|
if (window.favorites?.isFavorite(file.id, 'file')) {
|
||||||
// Remove from favorites
|
// Remove from favorites
|
||||||
const ok = await window.favorites.removeFromFavorites(file.id, 'file');
|
const ok = await window.favorites.removeFromFavorites(file.id, 'file');
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
@@ -195,12 +185,7 @@ const contextMenus = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add to favorites
|
// Add to favorites
|
||||||
const ok = await window.favorites.addToFavorites(
|
const ok = await window.favorites.addToFavorites(file.id, file.name, 'file', file.folder_id);
|
||||||
file.id,
|
|
||||||
file.name,
|
|
||||||
'file',
|
|
||||||
file.folder_id
|
|
||||||
);
|
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
window.ui.setFavoriteVisualState(file.id, 'file', true);
|
window.ui.setFavoriteVisualState(file.id, 'file', true);
|
||||||
}
|
}
|
||||||
@@ -285,33 +270,27 @@ const contextMenus = {
|
|||||||
const targetId = window.app.selectedTargetFolderId;
|
const targetId = window.app.selectedTargetFolderId;
|
||||||
const items = window.app.batchMoveItems || [];
|
const items = window.app.batchMoveItems || [];
|
||||||
|
|
||||||
const fileIds = items.filter(i => i.type === 'file').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);
|
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||||
|
|
||||||
let result = await window.fileOps.batchCopy( fileIds, folderIds, targetId);
|
const result = await window.fileOps.batchCopy(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
|
|
||||||
window.multiSelect.showBatchResult( "copy", result);
|
window.multiSelect.showBatchResult('copy', result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single item copy
|
// Single item copy
|
||||||
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
||||||
const success = await window.fileOps.copyFile(
|
const success = await window.fileOps.copyFile(window.app.contextMenuTargetFile.id, window.app.selectedTargetFolderId);
|
||||||
window.app.contextMenuTargetFile.id,
|
|
||||||
window.app.selectedTargetFolderId
|
|
||||||
);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
}
|
}
|
||||||
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
||||||
const success = await window.fileOps.copyFolder(
|
const success = await window.fileOps.copyFolder(window.app.contextMenuTargetFolder.id, window.app.selectedTargetFolderId);
|
||||||
window.app.contextMenuTargetFolder.id,
|
|
||||||
window.app.selectedTargetFolderId
|
|
||||||
);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
}
|
}
|
||||||
@@ -324,32 +303,26 @@ const contextMenus = {
|
|||||||
const targetId = window.app.selectedTargetFolderId;
|
const targetId = window.app.selectedTargetFolderId;
|
||||||
const items = window.app.batchMoveItems || [];
|
const items = window.app.batchMoveItems || [];
|
||||||
|
|
||||||
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
|
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);
|
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 result = await window.fileOps.batchMove(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
window.multiSelect.showBatchResult( "move", result);
|
window.multiSelect.showBatchResult('move', result);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
if (window.app.moveDialogMode === 'file' && window.app.contextMenuTargetFile) {
|
||||||
const success = await window.fileOps.moveFile(
|
const success = await window.fileOps.moveFile(window.app.contextMenuTargetFile.id, window.app.selectedTargetFolderId);
|
||||||
window.app.contextMenuTargetFile.id,
|
|
||||||
window.app.selectedTargetFolderId
|
|
||||||
);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
}
|
}
|
||||||
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
} else if (window.app.moveDialogMode === 'folder' && window.app.contextMenuTargetFolder) {
|
||||||
const success = await window.fileOps.moveFolder(
|
const success = await window.fileOps.moveFolder(window.app.contextMenuTargetFolder.id, window.app.selectedTargetFolderId);
|
||||||
window.app.contextMenuTargetFolder.id,
|
|
||||||
window.app.selectedTargetFolderId
|
|
||||||
);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
}
|
}
|
||||||
@@ -416,7 +389,7 @@ const contextMenus = {
|
|||||||
window.app.moveDialogMode = mode;
|
window.app.moveDialogMode = mode;
|
||||||
|
|
||||||
// Reset selection
|
// Reset selection
|
||||||
window.app.selectedTargetFolderId = "";
|
window.app.selectedTargetFolderId = '';
|
||||||
|
|
||||||
// Ensure we have the home folder ID BEFORE calculating startFolderId
|
// Ensure we have the home folder ID BEFORE calculating startFolderId
|
||||||
if (!window.app.userHomeFolderId) {
|
if (!window.app.userHomeFolderId) {
|
||||||
@@ -442,7 +415,16 @@ const contextMenus = {
|
|||||||
startFolderId = window.app.userHomeFolderId || null;
|
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
|
// Store the item being moved and navigation state
|
||||||
window.app.moveDialogItemId = item.id;
|
window.app.moveDialogItemId = item.id;
|
||||||
@@ -460,9 +442,14 @@ const contextMenus = {
|
|||||||
|
|
||||||
// Update dialog title (preserve icon)
|
// Update dialog title (preserve icon)
|
||||||
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
||||||
const titleText = mode === 'file' ?
|
const titleText =
|
||||||
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') :
|
mode === 'file'
|
||||||
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder');
|
? 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>`;
|
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
||||||
|
|
||||||
// Load folders for the starting location
|
// Load folders for the starting location
|
||||||
@@ -525,7 +512,6 @@ const contextMenus = {
|
|||||||
*/
|
*/
|
||||||
async loadMoveDialogFolders(parentFolderId) {
|
async loadMoveDialogFolders(parentFolderId) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Ensure we have the home folder ID before proceeding
|
// Ensure we have the home folder ID before proceeding
|
||||||
if (!window.app.userHomeFolderId) {
|
if (!window.app.userHomeFolderId) {
|
||||||
await window.resolveHomeFolder();
|
await window.resolveHomeFolder();
|
||||||
@@ -555,7 +541,7 @@ const contextMenus = {
|
|||||||
|
|
||||||
// The contents endpoint returns an array of child folders
|
// The contents endpoint returns an array of child folders
|
||||||
// The fallback /api/folders returns root folders (home folder itself)
|
// 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);
|
console.log('[Move Dialog] Loaded folders:', folders.length, 'folders:', folders);
|
||||||
|
|
||||||
const folderSelectContainer = document.getElementById('folder-select-container');
|
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>
|
<span>${window.i18n ? window.i18n.t('dialogs.select_this_folder') : 'Select this folder'}</span>
|
||||||
`;
|
`;
|
||||||
currentFolderOption.addEventListener('click', () => {
|
currentFolderOption.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.folder-select-item').forEach(item => {
|
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||||
item.classList.remove('selected');
|
item.classList.remove('selected');
|
||||||
});
|
});
|
||||||
currentFolderOption.classList.add('selected');
|
currentFolderOption.classList.add('selected');
|
||||||
@@ -606,9 +592,7 @@ const contextMenus = {
|
|||||||
if (currentBreadcrumb.length > 0) {
|
if (currentBreadcrumb.length > 0) {
|
||||||
// Remove current folder from breadcrumb
|
// Remove current folder from breadcrumb
|
||||||
currentBreadcrumb.pop();
|
currentBreadcrumb.pop();
|
||||||
const parentFolder = currentBreadcrumb.length > 0
|
const parentFolder = currentBreadcrumb.length > 0 ? currentBreadcrumb[currentBreadcrumb.length - 1] : null;
|
||||||
? currentBreadcrumb[currentBreadcrumb.length - 1]
|
|
||||||
: null;
|
|
||||||
window.app.moveDialogBreadcrumb = currentBreadcrumb;
|
window.app.moveDialogBreadcrumb = currentBreadcrumb;
|
||||||
window.app.moveDialogCurrentFolderId = parentFolder ? parentFolder.id : null;
|
window.app.moveDialogCurrentFolderId = parentFolder ? parentFolder.id : null;
|
||||||
this.loadMoveDialogFolders(parentFolder ? parentFolder.id : null);
|
this.loadMoveDialogFolders(parentFolder ? parentFolder.id : null);
|
||||||
@@ -623,7 +607,7 @@ const contextMenus = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add subfolders (clicking navigates INTO the folder)
|
// 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)
|
// Skip the item being moved (to prevent moving a folder into itself)
|
||||||
if (mode === 'folder' && folder.id === itemId) {
|
if (mode === 'folder' && folder.id === itemId) {
|
||||||
return;
|
return;
|
||||||
@@ -661,7 +645,7 @@ const contextMenus = {
|
|||||||
<span>${window.i18n ? window.i18n.t('dialogs.move_to_home') : 'Move to Home folder'}</span>
|
<span>${window.i18n ? window.i18n.t('dialogs.move_to_home') : 'Move to Home folder'}</span>
|
||||||
`;
|
`;
|
||||||
homeOption.addEventListener('click', () => {
|
homeOption.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.folder-select-item').forEach(item => {
|
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||||
item.classList.remove('selected');
|
item.classList.remove('selected');
|
||||||
});
|
});
|
||||||
homeOption.classList.add('selected');
|
homeOption.classList.add('selected');
|
||||||
@@ -680,7 +664,7 @@ const contextMenus = {
|
|||||||
window.app.selectedTargetFolderId = parentFolderId || '';
|
window.app.selectedTargetFolderId = parentFolderId || '';
|
||||||
|
|
||||||
// Translate new elements
|
// Translate new elements
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(folderSelectContainer);
|
window.i18n.translateElement(folderSelectContainer);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -691,7 +675,7 @@ const contextMenus = {
|
|||||||
/**
|
/**
|
||||||
* Render breadcrumb navigation for move dialog
|
* Render breadcrumb navigation for move dialog
|
||||||
*/
|
*/
|
||||||
_renderMoveDialogBreadcrumb(container, breadcrumb, currentFolderId) {
|
_renderMoveDialogBreadcrumb(container, breadcrumb, _currentFolderId) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
@@ -773,6 +757,7 @@ const contextMenus = {
|
|||||||
// Use loadMoveDialogFolders which uses /api/folders/{id}/contents
|
// Use loadMoveDialogFolders which uses /api/folders/{id}/contents
|
||||||
await this.loadMoveDialogFolders(window.app.userHomeFolderId || null);
|
await this.loadMoveDialogFolders(window.app.userHomeFolderId || null);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show share dialog for files or folders
|
* Show share dialog for files or folders
|
||||||
* @param {Object} item - File or folder object
|
* @param {Object} item - File or folder object
|
||||||
@@ -791,9 +776,14 @@ const contextMenus = {
|
|||||||
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
||||||
if (dialogHeader) {
|
if (dialogHeader) {
|
||||||
const headerSpan = dialogHeader.querySelector('span');
|
const headerSpan = dialogHeader.querySelector('span');
|
||||||
const titleText = itemType === 'file' ?
|
const titleText =
|
||||||
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') :
|
itemType === 'file'
|
||||||
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder');
|
? window.i18n
|
||||||
|
? window.i18n.t('dialogs.share_file')
|
||||||
|
: 'Share file'
|
||||||
|
: window.i18n
|
||||||
|
? window.i18n.t('dialogs.share_folder')
|
||||||
|
: 'Share folder';
|
||||||
if (headerSpan) {
|
if (headerSpan) {
|
||||||
headerSpan.textContent = titleText;
|
headerSpan.textContent = titleText;
|
||||||
} else {
|
} else {
|
||||||
@@ -831,13 +821,11 @@ const contextMenus = {
|
|||||||
document.getElementById('existing-shares-section').classList.remove('hidden');
|
document.getElementById('existing-shares-section').classList.remove('hidden');
|
||||||
|
|
||||||
// Create elements for each existing share
|
// Create elements for each existing share
|
||||||
existingShares.forEach(share => {
|
existingShares.forEach((share) => {
|
||||||
const shareEl = document.createElement('div');
|
const shareEl = document.createElement('div');
|
||||||
shareEl.className = 'existing-share-item';
|
shareEl.className = 'existing-share-item';
|
||||||
|
|
||||||
const expiresText = share.expires_at ?
|
const expiresText = share.expires_at ? `Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` : 'No expiration';
|
||||||
`Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
|
|
||||||
'No expiration';
|
|
||||||
|
|
||||||
// Share URL
|
// Share URL
|
||||||
const urlDiv = document.createElement('div');
|
const urlDiv = document.createElement('div');
|
||||||
@@ -882,7 +870,7 @@ const contextMenus = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for copy and delete buttons
|
// Add event listeners for copy and delete buttons
|
||||||
document.querySelectorAll('.copy-link-btn').forEach(btn => {
|
document.querySelectorAll('.copy-link-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const url = btn.getAttribute('data-share-url');
|
const url = btn.getAttribute('data-share-url');
|
||||||
@@ -890,7 +878,7 @@ const contextMenus = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.delete-link-btn').forEach(btn => {
|
document.querySelectorAll('.delete-link-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const shareId = btn.getAttribute('data-share-id');
|
const shareId = btn.getAttribute('data-share-id');
|
||||||
@@ -898,7 +886,7 @@ const contextMenus = {
|
|||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
|
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?',
|
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',
|
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||||
}).then(async (confirmed) => {
|
}).then(async (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await window.fileSharing.removeSharedLink(shareId);
|
await window.fileSharing.removeSharedLink(shareId);
|
||||||
@@ -961,7 +949,10 @@ const contextMenus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers = { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...getCsrfHeaders()
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch('/api/shares', {
|
const response = await fetch('/api/shares', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -990,7 +981,6 @@ const contextMenus = {
|
|||||||
window.i18n ? window.i18n.t('notifications.link_created') : 'Link created',
|
window.i18n ? window.i18n.t('notifications.link_created') : 'Link created',
|
||||||
window.i18n ? window.i18n.t('notifications.share_success') : 'Shared link created successfully'
|
window.i18n ? window.i18n.t('notifications.share_success') : 'Shared link created successfully'
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating shared link:', error);
|
console.error('Error creating shared link:', error);
|
||||||
window.ui.showNotification('Error', error.message || 'Could not create shared link');
|
window.ui.showNotification('Error', error.message || 'Could not create shared link');
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ function getAuthHeaders() {
|
|||||||
|
|
||||||
// File Operations Module
|
// File Operations Module
|
||||||
const fileOps = {
|
const fileOps = {
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Upload progress — notification bell integration
|
// Upload progress — notification bell integration
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -23,9 +22,7 @@ const fileOps = {
|
|||||||
|
|
||||||
/** Start a new upload batch in the notification bell */
|
/** Start a new upload batch in the notification bell */
|
||||||
_initUploadToast(totalFiles, folderName) {
|
_initUploadToast(totalFiles, folderName) {
|
||||||
this._currentBatchId = window.notifications
|
this._currentBatchId = window.notifications ? window.notifications.addUploadBatch(totalFiles, folderName) : null;
|
||||||
? window.notifications.addUploadBatch(totalFiles, folderName)
|
|
||||||
: null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Finalise the batch in the notification bell */
|
/** Finalise the batch in the notification bell */
|
||||||
@@ -100,7 +97,9 @@ const fileOps = {
|
|||||||
const resetStallTimer = () => {
|
const resetStallTimer = () => {
|
||||||
if (stallTimer) clearTimeout(stallTimer);
|
if (stallTimer) clearTimeout(stallTimer);
|
||||||
stallTimer = setTimeout(() => {
|
stallTimer = setTimeout(() => {
|
||||||
try { xhr.abort(); } catch (_) {}
|
try {
|
||||||
|
xhr.abort();
|
||||||
|
} catch (_) {}
|
||||||
safeUpdateFile(0, 'error');
|
safeUpdateFile(0, 'error');
|
||||||
finalize({
|
finalize({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -112,7 +111,9 @@ const fileOps = {
|
|||||||
|
|
||||||
resetStallTimer();
|
resetStallTimer();
|
||||||
hardTimer = setTimeout(() => {
|
hardTimer = setTimeout(() => {
|
||||||
try { xhr.abort(); } catch (_) {}
|
try {
|
||||||
|
xhr.abort();
|
||||||
|
} catch (_) {}
|
||||||
safeUpdateFile(0, 'error');
|
safeUpdateFile(0, 'error');
|
||||||
finalize({
|
finalize({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -144,7 +145,9 @@ const fileOps = {
|
|||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
safeUpdateFile(100, 'done');
|
safeUpdateFile(100, 'done');
|
||||||
let data = null;
|
let data = null;
|
||||||
try { data = JSON.parse(xhr.responseText); } catch (_) {}
|
try {
|
||||||
|
data = JSON.parse(xhr.responseText);
|
||||||
|
} catch (_) {}
|
||||||
finalize({ ok: true, data });
|
finalize({ ok: true, data });
|
||||||
} else {
|
} else {
|
||||||
safeUpdateFile(0, 'error');
|
safeUpdateFile(0, 'error');
|
||||||
@@ -167,12 +170,20 @@ const fileOps = {
|
|||||||
|
|
||||||
xhr.addEventListener('abort', () => {
|
xhr.addEventListener('abort', () => {
|
||||||
safeUpdateFile(0, 'error');
|
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', () => {
|
xhr.addEventListener('timeout', () => {
|
||||||
safeUpdateFile(0, 'error');
|
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');
|
xhr.open('POST', '/api/files/upload');
|
||||||
@@ -218,18 +229,20 @@ const fileOps = {
|
|||||||
|
|
||||||
// Read body as text first (always consume the response fully)
|
// Read body as text first (always consume the response fully)
|
||||||
let rawText = '';
|
let rawText = '';
|
||||||
try { rawText = await response.text(); } catch (_) {}
|
try {
|
||||||
|
rawText = await response.text();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
let body = null;
|
let body = null;
|
||||||
try { body = JSON.parse(rawText); } catch (_) {}
|
try {
|
||||||
|
body = JSON.parse(rawText);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { ok: true, data: body };
|
return { ok: true, data: body };
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMsg = body && typeof body === 'object'
|
const errorMsg = body && typeof body === 'object' ? body.error || null : rawText || null;
|
||||||
? (body.error || null)
|
|
||||||
: (rawText || null);
|
|
||||||
const isQuotaError = (body && typeof body === 'object' && body.error_type === 'QuotaExceeded') || response.status === 507;
|
const isQuotaError = (body && typeof body === 'object' && body.error_type === 'QuotaExceeded') || response.status === 507;
|
||||||
return { ok: false, errorMsg, isQuotaError };
|
return { ok: false, errorMsg, isQuotaError };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -237,9 +250,7 @@ const fileOps = {
|
|||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
isTimeout,
|
isTimeout,
|
||||||
errorMsg: isTimeout
|
errorMsg: isTimeout ? `Timeout after ${Math.round(timeoutMs / 1000)}s` : `Fetch upload failed: ${e?.message || 'network error'}`
|
||||||
? `Timeout after ${Math.round(timeoutMs / 1000)}s`
|
|
||||||
: `Fetch upload failed: ${e?.message || 'network error'}`
|
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -269,8 +280,12 @@ const fileOps = {
|
|||||||
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
||||||
const progressBar = document.querySelector('.progress-fill');
|
const progressBar = document.querySelector('.progress-fill');
|
||||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
if (uploadProgressDiv) {
|
||||||
if (progressBar) { progressBar.style.width = '0%'; }
|
uploadProgressDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out unreadable entries (typically dropped folders/placeholders)
|
// Filter out unreadable entries (typically dropped folders/placeholders)
|
||||||
const readableFiles = [];
|
const readableFiles = [];
|
||||||
@@ -321,7 +336,8 @@ const fileOps = {
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
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:
|
// Scale stall timeout with file size:
|
||||||
@@ -334,7 +350,7 @@ const fileOps = {
|
|||||||
|
|
||||||
// Legacy dropzone bar
|
// Legacy dropzone bar
|
||||||
if (progressBar) {
|
if (progressBar) {
|
||||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||||
}
|
}
|
||||||
// Notify bell of per-file completion
|
// Notify bell of per-file completion
|
||||||
if (window.notifications && batchId) {
|
if (window.notifications && batchId) {
|
||||||
@@ -378,7 +394,9 @@ const fileOps = {
|
|||||||
|
|
||||||
// Refresh storage usage display
|
// Refresh storage usage display
|
||||||
if (typeof window.refreshUserData === 'function') {
|
if (typeof window.refreshUserData === 'function') {
|
||||||
try { await window.refreshUserData(); } catch (_) {}
|
try {
|
||||||
|
await window.refreshUserData();
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -425,8 +443,12 @@ const fileOps = {
|
|||||||
|
|
||||||
const progressBar = document.querySelector('.progress-fill');
|
const progressBar = document.querySelector('.progress-fill');
|
||||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
if (uploadProgressDiv) {
|
||||||
if (progressBar) { progressBar.style.width = '0%'; }
|
uploadProgressDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter unreadable entries
|
// Filter unreadable entries
|
||||||
@@ -460,9 +482,7 @@ const fileOps = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedPaths = [...folderPaths].sort((a, b) =>
|
const sortedPaths = [...folderPaths].sort((a, b) => a.split('/').length - b.split('/').length);
|
||||||
a.split('/').length - b.split('/').length
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create folders first
|
// Create folders first
|
||||||
for (const folderPath of sortedPaths) {
|
for (const folderPath of sortedPaths) {
|
||||||
@@ -498,16 +518,23 @@ const fileOps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect root folder(s) from entry paths
|
// Detect root folder(s) from entry paths
|
||||||
const rootFolderNames = [...new Set(validEntries.map((entry) => {
|
const rootFolderNames = [
|
||||||
|
...new Set(
|
||||||
|
validEntries
|
||||||
|
.map((entry) => {
|
||||||
const rel = entry.relativePath || entry.file.name;
|
const rel = entry.relativePath || entry.file.name;
|
||||||
return rel.split('/')[0] || '';
|
return rel.split('/')[0] || '';
|
||||||
}).filter(Boolean))];
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
];
|
||||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
||||||
const rootFolderLabel = rootFolderNames.length <= 1
|
const rootFolderLabel =
|
||||||
? (rootFolderNames[0] || '')
|
rootFolderNames.length <= 1
|
||||||
: (locale.startsWith('es')
|
? rootFolderNames[0] || ''
|
||||||
|
: locale.startsWith('es')
|
||||||
? `${rootFolderNames.length} carpetas`
|
? `${rootFolderNames.length} carpetas`
|
||||||
: `${rootFolderNames.length} folders`);
|
: `${rootFolderNames.length} folders`;
|
||||||
|
|
||||||
// Upload files — pass folder name for folder-level progress display
|
// Upload files — pass folder name for folder-level progress display
|
||||||
this._initUploadToast(totalFiles, rootFolderLabel);
|
this._initUploadToast(totalFiles, rootFolderLabel);
|
||||||
@@ -547,8 +574,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const buf = await Promise.race([
|
const buf = await Promise.race([
|
||||||
file.arrayBuffer(),
|
file.arrayBuffer(),
|
||||||
new Promise((_, rej) =>
|
new Promise((_, rej) => setTimeout(() => rej(new Error('read-timeout')), 2000))
|
||||||
setTimeout(() => rej(new Error('read-timeout')), 2000))
|
|
||||||
]);
|
]);
|
||||||
uploadFile = new Blob([buf], {
|
uploadFile = new Blob([buf], {
|
||||||
type: file.type || 'application/octet-stream'
|
type: file.type || 'application/octet-stream'
|
||||||
@@ -558,7 +584,9 @@ const fileOps = {
|
|||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
successCount++;
|
successCount++;
|
||||||
if (window.notifications && batchId) {
|
if (window.notifications && batchId) {
|
||||||
try { window.notifications.fileCompleted(batchId, true); } catch (_) {}
|
try {
|
||||||
|
window.notifications.fileCompleted(batchId, true);
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -573,7 +601,7 @@ const fileOps = {
|
|||||||
|
|
||||||
result = await this._uploadFileFetch(formData, thisTimeout);
|
result = await this._uploadFileFetch(formData, thisTimeout);
|
||||||
|
|
||||||
console.log(`[UPLOAD END] #${idx} ${rel} ok=${result.ok}${result.errorMsg ? ' err=' + result.errorMsg : ''}`);
|
console.log(`[UPLOAD END] #${idx} ${rel} ok=${result.ok}${result.errorMsg ? ` err=${result.errorMsg}` : ''}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = {
|
result = {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -585,10 +613,12 @@ const fileOps = {
|
|||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
|
|
||||||
if (window.notifications && batchId) {
|
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) {
|
if (progressBar && uploadedCount % 10 === 0) {
|
||||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||||
}
|
}
|
||||||
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
||||||
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
||||||
@@ -627,7 +657,9 @@ const fileOps = {
|
|||||||
this._finishUploadToast(successCount, totalFiles);
|
this._finishUploadToast(successCount, totalFiles);
|
||||||
|
|
||||||
if (typeof window.refreshUserData === 'function') {
|
if (typeof window.refreshUserData === 'function') {
|
||||||
try { await window.refreshUserData(); } catch (_) {}
|
try {
|
||||||
|
await window.refreshUserData();
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -702,7 +734,7 @@ const fileOps = {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
folder_id: targetFolderId === "" ? null : targetFolderId
|
folder_id: targetFolderId === '' ? null : targetFolderId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -716,7 +748,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||||
@@ -744,7 +776,7 @@ const fileOps = {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
parent_id: targetFolderId === "" ? null : targetFolderId
|
parent_id: targetFolderId === '' ? null : targetFolderId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -758,7 +790,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||||
@@ -785,9 +817,9 @@ const fileOps = {
|
|||||||
* @returns {Promise<BatchResult>} - Success status
|
* @returns {Promise<BatchResult>} - Success status
|
||||||
*/
|
*/
|
||||||
async batchMove(fileIds, folderIds, targetFolderId) {
|
async batchMove(fileIds, folderIds, targetFolderId) {
|
||||||
|
|
||||||
// TODO ensure not moving a folder into itself
|
// TODO ensure not moving a folder into itself
|
||||||
let success = 0, errors = 0;
|
let success = 0,
|
||||||
|
errors = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Batch move files in a single request
|
// Batch move files in a single request
|
||||||
@@ -795,7 +827,10 @@ const fileOps = {
|
|||||||
const res = await fetch('/api/batch/files/move', {
|
const res = await fetch('/api/batch/files/move', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
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();
|
const data = await res.json();
|
||||||
success += data.stats?.successful || 0;
|
success += data.stats?.successful || 0;
|
||||||
@@ -807,14 +842,16 @@ const fileOps = {
|
|||||||
const res = await fetch('/api/batch/folders/move', {
|
const res = await fetch('/api/batch/folders/move', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
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();
|
const data = await res.json();
|
||||||
success += data.stats?.successful || 0;
|
success += data.stats?.successful || 0;
|
||||||
errors += data.stats?.failed || 0;
|
errors += data.stats?.failed || 0;
|
||||||
}
|
}
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
console.error('Batch move error:', err);
|
console.error('Batch move error:', err);
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
@@ -841,12 +878,12 @@ const fileOps = {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
file_ids: [fileId],
|
file_ids: [fileId],
|
||||||
target_folder_id: targetFolderId === "" ? null : targetFolderId
|
target_folder_id: targetFolderId === '' ? null : targetFolderId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
await response.json();
|
||||||
// Reload files after copying
|
// Reload files after copying
|
||||||
await window.loadFiles();
|
await window.loadFiles();
|
||||||
window.ui.showNotification('File copied', 'File copied successfully');
|
window.ui.showNotification('File copied', 'File copied successfully');
|
||||||
@@ -856,7 +893,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
||||||
@@ -890,17 +927,20 @@ const fileOps = {
|
|||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
*/
|
*/
|
||||||
async batchCopy(fileIds, folderIds, targetFolderId) {
|
async batchCopy(fileIds, folderIds, targetFolderId) {
|
||||||
|
|
||||||
// FIXME ensure not moving a folder into itself
|
// FIXME ensure not moving a folder into itself
|
||||||
|
|
||||||
let success = 0, errors = 0;
|
let success = 0,
|
||||||
|
errors = 0;
|
||||||
try {
|
try {
|
||||||
// Batch copy files
|
// Batch copy files
|
||||||
if (fileIds.length > 0) {
|
if (fileIds.length > 0) {
|
||||||
const res = await fetch('/api/batch/files/copy', {
|
const res = await fetch('/api/batch/files/copy', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
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();
|
const data = await res.json();
|
||||||
success += data.stats?.successful || 0;
|
success += data.stats?.successful || 0;
|
||||||
@@ -957,7 +997,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorMessage = errorData.error || response.statusText;
|
errorMessage = errorData.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||||
@@ -1003,7 +1043,7 @@ const fileOps = {
|
|||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorMessage = errorData.error || response.statusText;
|
errorMessage = errorData.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// If not JSON, use text as is
|
// If not JSON, use text as is
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
@@ -1027,8 +1067,10 @@ const fileOps = {
|
|||||||
async deleteFile(fileId, fileName) {
|
async deleteFile(fileId, fileName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
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?`,
|
message: window.i18n
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
? 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;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1075,8 +1117,10 @@ const fileOps = {
|
|||||||
async deleteFolder(folderId, folderName) {
|
async deleteFolder(folderId, folderName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
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?`,
|
message: window.i18n
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
? 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;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1184,8 +1228,10 @@ const fileOps = {
|
|||||||
async deletePermanently(trashId) {
|
async deletePermanently(trashId) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
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.',
|
message: window.i18n
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently',
|
? 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;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1216,8 +1262,10 @@ const fileOps = {
|
|||||||
async emptyTrash() {
|
async emptyTrash() {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
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.',
|
message: window.i18n
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash',
|
? 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;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class InlineViewer {
|
|||||||
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
||||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
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);
|
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
||||||
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
|
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||||
try {
|
try {
|
||||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
return;
|
return;
|
||||||
@@ -135,8 +135,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create image viewer using a blob URL
|
// Create image viewer using a blob URL
|
||||||
this.createBlobUrlViewer(file, 'image', container, loader);
|
this.createBlobUrlViewer(file, 'image', container, loader);
|
||||||
}
|
} else if (file.mime_type && file.mime_type === 'application/pdf') {
|
||||||
else if (file.mime_type && file.mime_type === 'application/pdf') {
|
|
||||||
// Hide zoom controls for PDFs
|
// Hide zoom controls for PDFs
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -148,8 +147,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create PDF viewer using object tag with blob URL
|
// Create PDF viewer using object tag with blob URL
|
||||||
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
||||||
}
|
} else if (file.mime_type && this.isTextViewable(file.mime_type)) {
|
||||||
else if (file.mime_type && this.isTextViewable(file.mime_type)) {
|
|
||||||
// Hide zoom controls for text files
|
// Hide zoom controls for text files
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -161,8 +159,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create text viewer using authenticated fetch
|
// Create text viewer using authenticated fetch
|
||||||
this.createTextViewer(file, container, loader);
|
this.createTextViewer(file, container, loader);
|
||||||
}
|
} else if (file.mime_type?.startsWith('audio/')) {
|
||||||
else if (file.mime_type && file.mime_type.startsWith('audio/')) {
|
|
||||||
// Hide zoom controls for audio
|
// Hide zoom controls for audio
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -174,8 +171,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create audio player
|
// Create audio player
|
||||||
this.createMediaViewer(file, 'audio', container, loader);
|
this.createMediaViewer(file, 'audio', container, loader);
|
||||||
}
|
} else if (file.mime_type?.startsWith('video/')) {
|
||||||
else if (file.mime_type && file.mime_type.startsWith('video/')) {
|
|
||||||
// Hide zoom controls for video
|
// Hide zoom controls for video
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -187,8 +183,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create video player
|
// Create video player
|
||||||
this.createMediaViewer(file, 'video', container, loader);
|
this.createMediaViewer(file, 'video', container, loader);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Hide zoom controls for unsupported files
|
// Hide zoom controls for unsupported files
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -219,7 +214,9 @@ class InlineViewer {
|
|||||||
try {
|
try {
|
||||||
console.log('Creating text viewer for:', file.name);
|
console.log('Creating text viewer for:', file.name);
|
||||||
|
|
||||||
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
|
const response = await fetch(`/api/files/${file.id}?inline=true`, {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||||
@@ -228,7 +225,7 @@ class InlineViewer {
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +240,7 @@ class InlineViewer {
|
|||||||
console.error('Error creating text viewer:', error);
|
console.error('Error creating text viewer:', error);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +256,8 @@ class InlineViewer {
|
|||||||
// Update loader to show progress bar for large files
|
// Update loader to show progress bar for large files
|
||||||
let progressBar = null;
|
let progressBar = null;
|
||||||
let progressText = null;
|
let progressText = null;
|
||||||
if (loader && file.size > 10 * 1024 * 1024) { // Show progress for files > 10MB
|
if (loader && file.size > 10 * 1024 * 1024) {
|
||||||
|
// Show progress for files > 10MB
|
||||||
loader.innerHTML = `
|
loader.innerHTML = `
|
||||||
<div class="inline-viewer-progress">
|
<div class="inline-viewer-progress">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
@@ -286,8 +284,8 @@ class InlineViewer {
|
|||||||
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
||||||
const progress = e.loaded / e.total;
|
const progress = e.loaded / e.total;
|
||||||
const pct = Math.round(progress * 100);
|
const pct = Math.round(progress * 100);
|
||||||
progressBar.style.width = pct + '%';
|
progressBar.style.width = `${pct}%`;
|
||||||
progressText.textContent = pct + '%';
|
progressText.textContent = `${pct}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +299,7 @@ class InlineViewer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = function() {
|
xhr.onerror = () => {
|
||||||
reject(new Error('Network error'));
|
reject(new Error('Network error'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -312,10 +310,10 @@ class InlineViewer {
|
|||||||
const blob = response;
|
const blob = response;
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
console.log('Created blob URL:', blobUrl.substring(0, 30) + '...');
|
console.log('Created blob URL:', `${blobUrl.substring(0, 30)}...`);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,8 +338,7 @@ class InlineViewer {
|
|||||||
container.removeChild(img);
|
container.removeChild(img);
|
||||||
this.showErrorMessage(container);
|
this.showErrorMessage(container);
|
||||||
};
|
};
|
||||||
}
|
} else if (type === 'pdf') {
|
||||||
else if (type === 'pdf') {
|
|
||||||
console.log('Creating PDF viewer');
|
console.log('Creating PDF viewer');
|
||||||
|
|
||||||
// Create iframe for PDF (more reliable than object tag)
|
// Create iframe for PDF (more reliable than object tag)
|
||||||
@@ -353,8 +350,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Monitor iframe for loading issues
|
// Monitor iframe for loading issues
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!iframe.contentDocument ||
|
if (!iframe.contentDocument || iframe.contentDocument.body.innerHTML === '') {
|
||||||
iframe.contentDocument.body.innerHTML === '') {
|
|
||||||
console.warn('PDF viewer might be having issues, adding fallback');
|
console.warn('PDF viewer might be having issues, adding fallback');
|
||||||
|
|
||||||
// Add fallback embed
|
// Add fallback embed
|
||||||
@@ -369,12 +365,11 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Store blob URL for cleaning up later
|
// Store blob URL for cleaning up later
|
||||||
this.currentBlobUrl = blobUrl;
|
this.currentBlobUrl = blobUrl;
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
console.error('Error creating blob URL viewer:', error);
|
console.error('Error creating blob URL viewer:', error);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +383,9 @@ class InlineViewer {
|
|||||||
console.log(`Creating ${mediaType} player for:`, file.name);
|
console.log(`Creating ${mediaType} player for:`, file.name);
|
||||||
|
|
||||||
// Fetch file (cookie auto-sent)
|
// Fetch file (cookie auto-sent)
|
||||||
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
|
const response = await fetch(`/api/files/${file.id}?inline=true`, {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||||
@@ -398,7 +395,7 @@ class InlineViewer {
|
|||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +472,7 @@ class InlineViewer {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error creating ${mediaType} viewer:`, error);
|
console.error(`Error creating ${mediaType} viewer:`, error);
|
||||||
|
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,11 +522,11 @@ class InlineViewer {
|
|||||||
|
|
||||||
downloadFile(file) {
|
downloadFile(file) {
|
||||||
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
|
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return res.blob();
|
return res.blob();
|
||||||
})
|
})
|
||||||
.then(blob => {
|
.then((blob) => {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -539,7 +536,7 @@ class InlineViewer {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Download error:', err));
|
.catch((err) => console.error('Download error:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomImage(factor) {
|
zoomImage(factor) {
|
||||||
|
|||||||
@@ -23,11 +23,21 @@ const multiSelect = {
|
|||||||
|
|
||||||
// ── Public API ──────────────────────────────────────────
|
// ── Public API ──────────────────────────────────────────
|
||||||
|
|
||||||
get count() { return this._selected.size; },
|
get count() {
|
||||||
get items() { return Array.from(this._selected.values()); },
|
return this._selected.size;
|
||||||
get hasSelection() { return this._selected.size > 0; },
|
},
|
||||||
get files() { return this.items.filter(i => i.type === 'file'); },
|
get items() {
|
||||||
get folders() { return this.items.filter(i => i.type === 'folder'); },
|
return Array.from(this._selected.values());
|
||||||
|
},
|
||||||
|
get hasSelection() {
|
||||||
|
return this._selected.size > 0;
|
||||||
|
},
|
||||||
|
get files() {
|
||||||
|
return this.items.filter((i) => i.type === 'file');
|
||||||
|
},
|
||||||
|
get folders() {
|
||||||
|
return this.items.filter((i) => i.type === 'folder');
|
||||||
|
},
|
||||||
|
|
||||||
// ── Helpers for i18n ────────────────────────────────────
|
// ── Helpers for i18n ────────────────────────────────────
|
||||||
|
|
||||||
@@ -43,7 +53,10 @@ const multiSelect = {
|
|||||||
// ── Selection state management ──────────────────────────
|
// ── Selection state management ──────────────────────────
|
||||||
|
|
||||||
toggle(id, name, type, parentId) {
|
toggle(id, name, type, parentId) {
|
||||||
if (this._selected.has(id)) { this._selected.delete(id); return false; }
|
if (this._selected.has(id)) {
|
||||||
|
this._selected.delete(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this._selected.set(id, { id, name, type, parentId });
|
this._selected.set(id, { id, name, type, parentId });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -52,14 +65,19 @@ const multiSelect = {
|
|||||||
this._selected.set(id, { id, name, type, parentId });
|
this._selected.set(id, { id, name, type, parentId });
|
||||||
},
|
},
|
||||||
|
|
||||||
deselect(id) { this._selected.delete(id); },
|
deselect(id) {
|
||||||
|
this._selected.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this._selected.clear();
|
this._selected.clear();
|
||||||
this._lastClickedIndex = -1;
|
this._lastClickedIndex = -1;
|
||||||
document.querySelectorAll('.file-item.selected')
|
document.querySelectorAll('.file-item.selected').forEach((el) => {
|
||||||
.forEach(el => el.classList.remove('selected'));
|
el.classList.remove('selected');
|
||||||
document.querySelectorAll('.item-checkbox').forEach(cb => cb.checked = false);
|
});
|
||||||
|
document.querySelectorAll('.item-checkbox').forEach((cb) => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
this._syncUI();
|
this._syncUI();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -89,24 +107,22 @@ const multiSelect = {
|
|||||||
* @return {ItemSelection}
|
* @return {ItemSelection}
|
||||||
*/
|
*/
|
||||||
getSelection(targtFolderId) {
|
getSelection(targtFolderId) {
|
||||||
let fileIds=[];
|
const fileIds = [];
|
||||||
let folderIds=[];
|
const folderIds = [];
|
||||||
|
|
||||||
// TODO optimize & check if _selected is a better use
|
// TODO optimize & check if _selected is a better use
|
||||||
document.querySelectorAll(`div.file-item.selected`).forEach((item) => {
|
document.querySelectorAll(`div.file-item.selected`).forEach((item) => {
|
||||||
if (item.dataset.fileId) {
|
if (item.dataset.fileId) {
|
||||||
fileIds.push(item.dataset.fileId);
|
fileIds.push(item.dataset.fileId);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// ignore selectedItem if this is the target
|
// ignore selectedItem if this is the target
|
||||||
if (targtFolderId && targtFolderId !== item.dataset.folderId)
|
if (targtFolderId && targtFolderId !== item.dataset.folderId) folderIds.push(item.dataset.folderId);
|
||||||
folderIds.push(item.dataset.folderId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"fileIds": fileIds,
|
fileIds: fileIds,
|
||||||
"folderIds": folderIds,
|
folderIds: folderIds
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -115,19 +131,17 @@ const multiSelect = {
|
|||||||
* @param {BatchResult} result result of batch
|
* @param {BatchResult} result result of batch
|
||||||
*/
|
*/
|
||||||
showBatchResult(action, result) {
|
showBatchResult(action, result) {
|
||||||
if (action === "copy") {
|
if (action === 'copy') {
|
||||||
if (result.errors > 0) {
|
if (result.errors > 0) {
|
||||||
window.ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
window.ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Items copied',
|
window.ui.showNotification('Items copied', `${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
||||||
`${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (result.errors > 0) {
|
if (result.errors > 0) {
|
||||||
window.ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
window.ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Items moved',
|
window.ui.showNotification('Items moved', `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
||||||
`${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +159,9 @@ const multiSelect = {
|
|||||||
_selectAllInContainer(containerId, selector) {
|
_selectAllInContainer(containerId, selector) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.querySelectorAll(selector).forEach(el => this._selectElement(el));
|
container.querySelectorAll(selector).forEach((el) => {
|
||||||
|
this._selectElement(el);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getAllVisibleItems() {
|
_getAllVisibleItems() {
|
||||||
@@ -154,10 +170,20 @@ const multiSelect = {
|
|||||||
|
|
||||||
_extractInfo(el) {
|
_extractInfo(el) {
|
||||||
if (el.dataset.folderId && el.dataset.folderName !== undefined) {
|
if (el.dataset.folderId && el.dataset.folderName !== undefined) {
|
||||||
return { id: el.dataset.folderId, name: el.dataset.folderName, type: 'folder', parentId: el.dataset.parentId || '' };
|
return {
|
||||||
|
id: el.dataset.folderId,
|
||||||
|
name: el.dataset.folderName,
|
||||||
|
type: 'folder',
|
||||||
|
parentId: el.dataset.parentId || ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (el.dataset.fileId) {
|
if (el.dataset.fileId) {
|
||||||
return { id: el.dataset.fileId, name: el.dataset.fileName, type: 'file', parentId: el.dataset.folderId || '' };
|
return {
|
||||||
|
id: el.dataset.fileId,
|
||||||
|
name: el.dataset.fileName,
|
||||||
|
type: 'file',
|
||||||
|
parentId: el.dataset.folderId || ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -170,30 +196,26 @@ const multiSelect = {
|
|||||||
const info = this._extractInfo(el);
|
const info = this._extractInfo(el);
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
|
|
||||||
if (event && event.shiftKey && this._lastClickedIndex >= 0 && index >= 0) {
|
if (event?.shiftKey && this._lastClickedIndex >= 0 && index >= 0) {
|
||||||
const start = Math.min(this._lastClickedIndex, index);
|
const start = Math.min(this._lastClickedIndex, index);
|
||||||
const end = Math.max(this._lastClickedIndex, index);
|
const end = Math.max(this._lastClickedIndex, index);
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
this._selectElement(items[i]);
|
this._selectElement(items[i]);
|
||||||
const iInfo = this._extractInfo(items[i]);
|
const iInfo = this._extractInfo(items[i]);
|
||||||
if (iInfo) {
|
if (iInfo) {
|
||||||
const sel = iInfo.type === 'folder'
|
const sel = iInfo.type === 'folder' ? `[data-folder-id="${iInfo.id}"]` : `[data-file-id="${iInfo.id}"]`;
|
||||||
? `[data-folder-id="${iInfo.id}"]`
|
|
||||||
: `[data-file-id="${iInfo.id}"]`;
|
|
||||||
document.querySelectorAll(sel).forEach((e) => {
|
document.querySelectorAll(sel).forEach((e) => {
|
||||||
e.classList.add('selected');
|
e.classList.add('selected');
|
||||||
let checkbox = e.querySelector('input[type="checkbox"]');
|
const checkbox = e.querySelector('input[type="checkbox"]');
|
||||||
if (checkbox)
|
if (checkbox) checkbox.checked = true;
|
||||||
checkbox.checked = true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const nowSelected = this.toggle(info.id, info.name, info.type, info.parentId);
|
const nowSelected = this.toggle(info.id, info.name, info.type, info.parentId);
|
||||||
el.classList.toggle('selected', nowSelected);
|
el.classList.toggle('selected', nowSelected);
|
||||||
let checkbox = el.querySelector('input[type="checkbox"]');
|
const checkbox = el.querySelector('input[type="checkbox"]');
|
||||||
if (checkbox)
|
if (checkbox) checkbox.checked = nowSelected;
|
||||||
checkbox.checked = nowSelected;
|
|
||||||
}
|
}
|
||||||
this._lastClickedIndex = index;
|
this._lastClickedIndex = index;
|
||||||
this._syncUI();
|
this._syncUI();
|
||||||
@@ -249,23 +271,19 @@ const multiSelect = {
|
|||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
this._barVisible = true;
|
this._barVisible = true;
|
||||||
|
|
||||||
const countText = n === 1
|
const countText = n === 1 ? this._t('batch.one_selected') || '1 item selected' : this._t('batch.n_selected', { count: n }) || `${n} items selected`;
|
||||||
? (this._t('batch.one_selected') || '1 item selected')
|
document.getElementById('batch-bar-count').innerText = countText;
|
||||||
: (this._t('batch.n_selected', { count: n }) || `${n} items selected`);
|
|
||||||
document.getElementById("batch-bar-count").innerText = countText;
|
|
||||||
|
|
||||||
actionsBar.classList.add('hidden');
|
actionsBar.classList.add('hidden');
|
||||||
|
|
||||||
batchSelectionBar.classList.remove('hidden');
|
batchSelectionBar.classList.remove('hidden');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this._barVisible = false;
|
this._barVisible = false;
|
||||||
|
|
||||||
// Hide grid bar
|
// Hide grid bar
|
||||||
batchSelectionBar.classList.add('hidden');
|
batchSelectionBar.classList.add('hidden');
|
||||||
|
|
||||||
if (actionsBar.dataset.mode !== "hidden")
|
if (actionsBar.dataset.mode !== 'hidden') actionsBar.classList.remove('hidden');
|
||||||
actionsBar.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync individual item checkboxes
|
// Sync individual item checkboxes
|
||||||
@@ -289,7 +307,7 @@ const multiSelect = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_syncItemCheckboxes() {
|
_syncItemCheckboxes() {
|
||||||
document.querySelectorAll('.file-item').forEach(el => {
|
document.querySelectorAll('.file-item').forEach((el) => {
|
||||||
const cb = el.querySelector('.item-checkbox');
|
const cb = el.querySelector('.item-checkbox');
|
||||||
if (cb) cb.checked = el.classList.contains('selected');
|
if (cb) cb.checked = el.classList.contains('selected');
|
||||||
});
|
});
|
||||||
@@ -322,21 +340,20 @@ const multiSelect = {
|
|||||||
if (items.length === 0) return;
|
if (items.length === 0) return;
|
||||||
|
|
||||||
const n = items.length;
|
const n = items.length;
|
||||||
const msg = n === 1
|
const msg =
|
||||||
? (this._t('dialogs.confirm_delete_file', { name: items[0].name })
|
n === 1
|
||||||
|| `Are you sure you want to move "${items[0].name}" to trash?`)
|
? this._t('dialogs.confirm_delete_file', { name: items[0].name }) || `Are you sure you want to move "${items[0].name}" to trash?`
|
||||||
: (this._t('batch.confirm_delete', { count: n })
|
: this._t('batch.confirm_delete', { count: n }) || `Are you sure you want to move ${n} items to trash?`;
|
||||||
|| `Are you sure you want to move ${n} items to trash?`);
|
|
||||||
|
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: this._t('dialogs.confirm_delete') || 'Move to trash',
|
title: this._t('dialogs.confirm_delete') || 'Move to trash',
|
||||||
message: msg,
|
message: msg,
|
||||||
confirmText: this._t('actions.delete') || 'Delete',
|
confirmText: this._t('actions.delete') || 'Delete'
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
const fileIds = items.filter(i => i.type === 'file').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);
|
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/batch/trash', {
|
const response = await fetch('/api/batch/trash', {
|
||||||
@@ -371,16 +388,15 @@ const multiSelect = {
|
|||||||
|
|
||||||
window.app.moveDialogMode = 'batch';
|
window.app.moveDialogMode = 'batch';
|
||||||
window.app.batchMoveItems = items;
|
window.app.batchMoveItems = items;
|
||||||
window.app.selectedTargetFolderId = "";
|
window.app.selectedTargetFolderId = '';
|
||||||
|
|
||||||
const dialog = document.getElementById('move-file-dialog');
|
const dialog = document.getElementById('move-file-dialog');
|
||||||
const dialogHeader = dialog.querySelector('.rename-dialog-header');
|
const dialogHeader = dialog.querySelector('.rename-dialog-header');
|
||||||
const n = items.length;
|
const n = items.length;
|
||||||
const titleText = this._t('batch.move_title', { count: n })
|
const titleText = this._t('batch.move_title', { count: n }) || `Move ${n} item${n !== 1 ? 's' : ''}`;
|
||||||
|| `Move ${n} item${n !== 1 ? 's' : ''}`;
|
|
||||||
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
||||||
|
|
||||||
const excludeIds = items.filter(i => i.type === 'folder').map(i => i.id);
|
const excludeIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||||
await contextMenus.loadAllFolders(excludeIds[0] || null, 'batch');
|
await contextMenus.loadAllFolders(excludeIds[0] || null, 'batch');
|
||||||
dialog.style.display = 'flex';
|
dialog.style.display = 'flex';
|
||||||
},
|
},
|
||||||
@@ -393,8 +409,8 @@ const multiSelect = {
|
|||||||
window.ui.showNotification('Preparing download', 'Creating ZIP archive...');
|
window.ui.showNotification('Preparing download', 'Creating ZIP archive...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileIds = items.filter(i => i.type === 'file').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);
|
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||||
|
|
||||||
const response = await fetch('/api/batch/download', {
|
const response = await fetch('/api/batch/download', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -425,13 +441,10 @@ const multiSelect = {
|
|||||||
if (items.length === 0 || !window.favorites) return;
|
if (items.length === 0 || !window.favorites) return;
|
||||||
|
|
||||||
// Filter out items already in favourites
|
// Filter out items already in favourites
|
||||||
const toAdd = items.filter(i => !window.favorites.isFavorite(i.id, i.type));
|
const toAdd = items.filter((i) => !window.favorites.isFavorite(i.id, i.type));
|
||||||
if (toAdd.length === 0) {
|
if (toAdd.length === 0) {
|
||||||
this.clear();
|
this.clear();
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||||
this._t('favorites.add') || 'Favorites',
|
|
||||||
'All selected items are already favorites'
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +453,7 @@ const multiSelect = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
items: toAdd.map(i => ({ item_id: i.id, item_type: i.type }))
|
items: toAdd.map((i) => ({ item_id: i.id, item_type: i.type }))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -460,15 +473,9 @@ const multiSelect = {
|
|||||||
if (typeof window.loadFiles === 'function') window.loadFiles();
|
if (typeof window.loadFiles === 'function') window.loadFiles();
|
||||||
|
|
||||||
if (inserted > 0) {
|
if (inserted > 0) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(this._t('favorites.add') || 'Added to favorites', `${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`);
|
||||||
this._t('favorites.add') || 'Added to favorites',
|
|
||||||
`${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||||
this._t('favorites.add') || 'Favorites',
|
|
||||||
'All selected items are already favorites'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Batch favorites error:', e);
|
console.error('Batch favorites error:', e);
|
||||||
@@ -489,15 +496,13 @@ const multiSelect = {
|
|||||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||||
// ctrl+a cmd+a
|
// ctrl+a cmd+a
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||||
if (selectAllCheckbox)
|
if (selectAllCheckbox) selectAllCheckbox.checked = true;
|
||||||
selectAllCheckbox.checked = true;
|
|
||||||
this.selectAll();
|
this.selectAll();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape' && this.hasSelection) {
|
if (e.key === 'Escape' && this.hasSelection) {
|
||||||
this.clear();
|
this.clear();
|
||||||
if (selectAllCheckbox)
|
if (selectAllCheckbox) selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.checked = false;
|
|
||||||
}
|
}
|
||||||
if (e.key === 'Delete' && this.hasSelection) this.batchDelete();
|
if (e.key === 'Delete' && this.hasSelection) this.batchDelete();
|
||||||
});
|
});
|
||||||
@@ -505,21 +510,18 @@ const multiSelect = {
|
|||||||
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||||
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(batchSelectionBar);
|
window.i18n.translateElement(batchSelectionBar);
|
||||||
}
|
}
|
||||||
this._wireBarButtons();
|
this._wireBarButtons();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: competition with _
|
// FIXME: competition with _
|
||||||
_injectListHeaderCheckbox() {
|
_injectListHeaderCheckbox() {
|
||||||
const self = this;
|
|
||||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||||
if (!selectAllCheckbox) return;
|
if (!selectAllCheckbox) return;
|
||||||
selectAllCheckbox.addEventListener('change', () => self.toggleAll());
|
selectAllCheckbox.addEventListener('change', () => this.toggleAll());
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose globally
|
// Expose globally
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const search = {
|
|||||||
try {
|
try {
|
||||||
const errorJson = await response.json();
|
const errorJson = await response.json();
|
||||||
errorText = errorJson.error || response.statusText;
|
errorText = errorJson.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorText = response.statusText;
|
errorText = response.statusText;
|
||||||
}
|
}
|
||||||
console.error(`Search error: ${errorText}`);
|
console.error(`Search error: ${errorText}`);
|
||||||
@@ -56,7 +56,13 @@ const search = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error performing search:', error);
|
console.error('Error performing search:', error);
|
||||||
window.ui.showNotification('Error', 'Error performing search');
|
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');
|
const filesList = document.getElementById('files-list');
|
||||||
|
|
||||||
// Search results header with query time and sort controls
|
// Search results header with query time and sort controls
|
||||||
const totalCount = results.total_count || (results.files.length + results.folders.length);
|
const totalCount = results.total_count || results.files.length + results.folders.length;
|
||||||
const queryTimeText = results.query_time_ms !== undefined
|
const queryTimeText = results.query_time_ms !== undefined ? ` <span class="search-time">(${results.query_time_ms}ms)</span>` : '';
|
||||||
? ` <span class="search-time">(${results.query_time_ms}ms)</span>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const searchHeader = document.createElement('div');
|
const searchHeader = document.createElement('div');
|
||||||
searchHeader.className = 'search-results-header';
|
searchHeader.className = 'search-results-header';
|
||||||
@@ -138,7 +142,7 @@ const search = {
|
|||||||
if (sortSelect) {
|
if (sortSelect) {
|
||||||
sortSelect.addEventListener('change', () => {
|
sortSelect.addEventListener('change', () => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const searchInput = document.querySelector('.search-container input');
|
||||||
if (searchInput && searchInput.value.trim()) {
|
if (searchInput?.value.trim()) {
|
||||||
const event = new CustomEvent('search-resort', {
|
const event = new CustomEvent('search-resort', {
|
||||||
detail: { sort_by: sortSelect.value }
|
detail: { sort_by: sortSelect.value }
|
||||||
});
|
});
|
||||||
@@ -169,12 +173,12 @@ const search = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render folders (server-provided enriched data)
|
// Render folders (server-provided enriched data)
|
||||||
results.folders.forEach(folder => {
|
results.folders.forEach((folder) => {
|
||||||
window.ui.addFolderToView(folder);
|
window.ui.addFolderToView(folder);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render files (server-provided enriched data)
|
// Render files (server-provided enriched data)
|
||||||
results.files.forEach(file => {
|
results.files.forEach((file) => {
|
||||||
window.ui.addFileToView(file);
|
window.ui.addFileToView(file);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class WopiEditor {
|
|||||||
* Fetches supported extensions from the server (cached after first call).
|
* Fetches supported extensions from the server (cached after first call).
|
||||||
*/
|
*/
|
||||||
async canEdit(filename) {
|
async canEdit(filename) {
|
||||||
var ext = filename.split('.').pop().toLowerCase();
|
const ext = filename.split('.').pop().toLowerCase();
|
||||||
var supported = await this._getSupportedExtensions();
|
const supported = await this._getSupportedExtensions();
|
||||||
return supported.includes(ext);
|
return supported.includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +38,8 @@ class WopiEditor {
|
|||||||
async openInTab(fileId, fileName, action) {
|
async openInTab(fileId, fileName, action) {
|
||||||
action = action || 'edit';
|
action = action || 'edit';
|
||||||
try {
|
try {
|
||||||
var data = await this._getEditorUrlWithFallback(fileId, fileName, action);
|
const data = await this._getEditorUrlWithFallback(fileId, fileName, action);
|
||||||
var hostUrl = '/wopi/edit/' + encodeURIComponent(fileId)
|
const hostUrl = `/wopi/edit/${encodeURIComponent(fileId)}?access_token=${encodeURIComponent(data.access_token)}`;
|
||||||
+ '?access_token=' + encodeURIComponent(data.access_token);
|
|
||||||
window.open(hostUrl, '_blank');
|
window.open(hostUrl, '_blank');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open WOPI editor in tab:', error);
|
console.error('Failed to open WOPI editor in tab:', error);
|
||||||
@@ -54,13 +53,12 @@ class WopiEditor {
|
|||||||
* Fetch editor URL and WOPI token from the backend.
|
* Fetch editor URL and WOPI token from the backend.
|
||||||
*/
|
*/
|
||||||
async _getEditorUrl(fileId, action) {
|
async _getEditorUrl(fileId, action) {
|
||||||
var response = await fetch(
|
const response = await fetch(`/api/wopi/editor-url?file_id=${encodeURIComponent(fileId)}&action=${encodeURIComponent(action)}`, {
|
||||||
'/api/wopi/editor-url?file_id=' + encodeURIComponent(fileId) + '&action=' + encodeURIComponent(action),
|
credentials: 'same-origin'
|
||||||
{ credentials: 'same-origin' }
|
});
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
var text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error('Editor URL request failed: ' + response.status + ' ' + text);
|
throw new Error(`Editor URL request failed: ${response.status} ${text}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -96,61 +94,62 @@ class WopiEditor {
|
|||||||
_showModal(editorData, fileName) {
|
_showModal(editorData, fileName) {
|
||||||
this.closeEditor();
|
this.closeEditor();
|
||||||
|
|
||||||
var modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.id = 'wopi-editor-modal';
|
modal.id = 'wopi-editor-modal';
|
||||||
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;background:#fff;';
|
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;background:#fff;';
|
||||||
|
|
||||||
var header = document.createElement('div');
|
const 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');
|
const title = document.createElement('span');
|
||||||
title.textContent = fileName;
|
title.textContent = fileName;
|
||||||
header.appendChild(title);
|
header.appendChild(title);
|
||||||
|
|
||||||
var closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.textContent = '\u2715';
|
closeBtn.textContent = '\u2715';
|
||||||
closeBtn.style.cssText = 'background:none;border:none;color:#fff;cursor:pointer;font-size:18px;padding:4px 8px;';
|
closeBtn.style.cssText = 'background:none;border:none;color:#fff;cursor:pointer;font-size:18px;padding:4px 8px;';
|
||||||
closeBtn.onclick = this.closeEditor.bind(this);
|
closeBtn.onclick = this.closeEditor.bind(this);
|
||||||
header.appendChild(closeBtn);
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
var form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.id = 'wopi_form';
|
form.id = 'wopi_form';
|
||||||
form.target = 'wopi_frame';
|
form.target = 'wopi_frame';
|
||||||
form.action = editorData.editor_url;
|
form.action = editorData.editor_url;
|
||||||
form.method = 'post';
|
form.method = 'post';
|
||||||
form.style.display = 'none';
|
form.style.display = 'none';
|
||||||
|
|
||||||
var tokenInput = document.createElement('input');
|
const tokenInput = document.createElement('input');
|
||||||
tokenInput.name = 'access_token';
|
tokenInput.name = 'access_token';
|
||||||
tokenInput.value = editorData.access_token;
|
tokenInput.value = editorData.access_token;
|
||||||
tokenInput.type = 'hidden';
|
tokenInput.type = 'hidden';
|
||||||
form.appendChild(tokenInput);
|
form.appendChild(tokenInput);
|
||||||
|
|
||||||
var ttlInput = document.createElement('input');
|
const ttlInput = document.createElement('input');
|
||||||
ttlInput.name = 'access_token_ttl';
|
ttlInput.name = 'access_token_ttl';
|
||||||
ttlInput.value = editorData.access_token_ttl;
|
ttlInput.value = editorData.access_token_ttl;
|
||||||
ttlInput.type = 'hidden';
|
ttlInput.type = 'hidden';
|
||||||
form.appendChild(ttlInput);
|
form.appendChild(ttlInput);
|
||||||
|
|
||||||
var frameHolder = document.createElement('div');
|
const frameHolder = document.createElement('div');
|
||||||
frameHolder.style.cssText = 'position:absolute;top:40px;left:0;right:0;bottom:0;';
|
frameHolder.style.cssText = 'position:absolute;top:40px;left:0;right:0;bottom:0;';
|
||||||
|
|
||||||
// Loading spinner (removed once the editor signals ready)
|
// Loading spinner (removed once the editor signals ready)
|
||||||
var spinner = document.createElement('div');
|
const spinner = document.createElement('div');
|
||||||
spinner.id = 'wopi-loading-spinner';
|
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>';
|
spinner.innerHTML = '<i class="fas fa-spinner fa-spin empty-state-icon spinner"></i>';
|
||||||
frameHolder.appendChild(spinner);
|
frameHolder.appendChild(spinner);
|
||||||
|
|
||||||
var iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.name = 'wopi_frame';
|
iframe.name = 'wopi_frame';
|
||||||
iframe.title = 'Document Editor';
|
iframe.title = 'Document Editor';
|
||||||
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
||||||
iframe.setAttribute('allowfullscreen', 'true');
|
iframe.setAttribute('allowfullscreen', 'true');
|
||||||
// Fix 9: allow clipboard access for copy/paste inside the editor
|
// Fix 9: allow clipboard access for copy/paste inside the editor
|
||||||
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
||||||
iframe.setAttribute('sandbox',
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox');
|
||||||
'allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox');
|
|
||||||
frameHolder.appendChild(iframe);
|
frameHolder.appendChild(iframe);
|
||||||
|
|
||||||
modal.appendChild(header);
|
modal.appendChild(header);
|
||||||
@@ -176,9 +175,9 @@ class WopiEditor {
|
|||||||
if (msgId === 'UI_Close' || msgId === 'close') {
|
if (msgId === 'UI_Close' || msgId === 'close') {
|
||||||
this.closeEditor();
|
this.closeEditor();
|
||||||
} else if (msgId === 'App_LoadingStatus') {
|
} else if (msgId === 'App_LoadingStatus') {
|
||||||
var status = data.Values && data.Values.Status;
|
const status = data.Values?.Status;
|
||||||
if (status === 'Document_Loaded' || status === 'Frame_Ready') {
|
if (status === 'Document_Loaded' || status === 'Frame_Ready') {
|
||||||
var sp = document.getElementById('wopi-loading-spinner');
|
const sp = document.getElementById('wopi-loading-spinner');
|
||||||
if (sp) sp.remove();
|
if (sp) sp.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,9 +225,9 @@ class WopiEditor {
|
|||||||
*/
|
*/
|
||||||
async _fetchSupportedExtensions() {
|
async _fetchSupportedExtensions() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/wopi/supported-extensions');
|
const response = await fetch('/wopi/supported-extensions');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
var exts = await response.json();
|
const exts = await response.json();
|
||||||
if (Array.isArray(exts) && exts.length > 0) {
|
if (Array.isArray(exts) && exts.length > 0) {
|
||||||
this._supportedExtensions = exts;
|
this._supportedExtensions = exts;
|
||||||
return exts;
|
return exts;
|
||||||
@@ -238,11 +237,7 @@ class WopiEditor {
|
|||||||
// Ignore — fall through to hardcoded list
|
// Ignore — fall through to hardcoded list
|
||||||
}
|
}
|
||||||
// Fallback hardcoded list
|
// Fallback hardcoded list
|
||||||
this._supportedExtensions = [
|
this._supportedExtensions = ['docx', 'doc', 'odt', 'rtf', 'txt', 'xlsx', 'xls', 'ods', 'csv', 'pptx', 'ppt', 'odp'];
|
||||||
'docx', 'doc', 'odt', 'rtf', 'txt',
|
|
||||||
'xlsx', 'xls', 'ods', 'csv',
|
|
||||||
'pptx', 'ppt', 'odp',
|
|
||||||
];
|
|
||||||
return this._supportedExtensions;
|
return this._supportedExtensions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const favorites = {
|
|||||||
await this._fetchFromServer();
|
await this._fetchFromServer();
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
||||||
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
||||||
@@ -139,7 +139,7 @@ const favorites = {
|
|||||||
// Remove from local cache
|
// Remove from local cache
|
||||||
this._cache.delete(this._cacheKey(id, type));
|
this._cache.delete(this._cacheKey(id, type));
|
||||||
|
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
||||||
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
||||||
@@ -208,7 +208,7 @@ const favorites = {
|
|||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) window.ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying favorites:', error);
|
console.error('Error displaying favorites:', error);
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading favorite items');
|
window.ui.showNotification('Error', 'Error loading favorite items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,14 +202,12 @@ const photosView = {
|
|||||||
for (const file of files) tilesHtml += this._renderTile(file);
|
for (const file of files) tilesHtml += this._renderTile(file);
|
||||||
|
|
||||||
// Does this date-group already exist in the DOM?
|
// Does this date-group already exist in the DOM?
|
||||||
const existingHeader = this._container.querySelector(
|
const existingHeader = this._container.querySelector(`.photos-day-header[data-group="${CSS.escape(label)}"]`);
|
||||||
`.photos-day-header[data-group="${CSS.escape(label)}"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingHeader) {
|
if (existingHeader) {
|
||||||
// Append tiles to existing grid and update count badge
|
// Append tiles to existing grid and update count badge
|
||||||
const grid = existingHeader.nextElementSibling;
|
const grid = existingHeader.nextElementSibling;
|
||||||
if (grid && grid.classList.contains('photos-grid')) {
|
if (grid?.classList.contains('photos-grid')) {
|
||||||
grid.insertAdjacentHTML('beforeend', tilesHtml);
|
grid.insertAdjacentHTML('beforeend', tilesHtml);
|
||||||
const countSpan = existingHeader.querySelector('.photos-day-count');
|
const countSpan = existingHeader.querySelector('.photos-day-count');
|
||||||
if (countSpan) countSpan.textContent = grid.children.length;
|
if (countSpan) countSpan.textContent = grid.children.length;
|
||||||
@@ -230,10 +228,9 @@ const photosView = {
|
|||||||
|
|
||||||
/** Generate HTML for a single photo/video tile */
|
/** Generate HTML for a single photo/video tile */
|
||||||
_renderTile(file) {
|
_renderTile(file) {
|
||||||
const isVideo = file.mime_type && file.mime_type.startsWith('video/');
|
const isVideo = file.mime_type?.startsWith('video/');
|
||||||
const selected = this.selected.has(file.id) ? ' selected' : '';
|
const selected = this.selected.has(file.id) ? ' selected' : '';
|
||||||
const cachedThumb = isVideo && this._videoThumbCache.has(file.id)
|
const cachedThumb = isVideo && this._videoThumbCache.has(file.id) ? this._videoThumbCache.get(file.id) : null;
|
||||||
? this._videoThumbCache.get(file.id) : null;
|
|
||||||
const thumbUrl = cachedThumb || `/api/files/${file.id}/thumbnail/preview`;
|
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)}">`;
|
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>`;
|
h += `<div class="photo-check"><i class="fas fa-check"></i></div>`;
|
||||||
@@ -248,9 +245,12 @@ const photosView = {
|
|||||||
this._destroyObserver();
|
this._destroyObserver();
|
||||||
const sentinel = this._container?.querySelector('.photos-sentinel');
|
const sentinel = this._container?.querySelector('.photos-sentinel');
|
||||||
if (sentinel && !this.exhausted) {
|
if (sentinel && !this.exhausted) {
|
||||||
this._observer = new IntersectionObserver((entries) => {
|
this._observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
if (entries[0].isIntersecting) this._loadPage();
|
if (entries[0].isIntersecting) this._loadPage();
|
||||||
}, { rootMargin: '400px' });
|
},
|
||||||
|
{ rootMargin: '400px' }
|
||||||
|
);
|
||||||
this._observer.observe(sentinel);
|
this._observer.observe(sentinel);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -266,9 +266,7 @@ const photosView = {
|
|||||||
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
||||||
_setupVideoThumbnails(startIndex = 0) {
|
_setupVideoThumbnails(startIndex = 0) {
|
||||||
const tiles = this._container.querySelectorAll('.photo-tile[data-mime^="video/"]');
|
const tiles = this._container.querySelectorAll('.photo-tile[data-mime^="video/"]');
|
||||||
const newIds = startIndex > 0
|
const newIds = startIndex > 0 ? new Set(this.items.slice(startIndex).map((f) => f.id)) : null;
|
||||||
? new Set(this.items.slice(startIndex).map(f => f.id))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
for (const tile of tiles) {
|
for (const tile of tiles) {
|
||||||
const fileId = tile.dataset.id;
|
const fileId = tile.dataset.id;
|
||||||
@@ -278,9 +276,13 @@ const photosView = {
|
|||||||
const img = tile.querySelector('img');
|
const img = tile.querySelector('img');
|
||||||
if (!img) continue;
|
if (!img) continue;
|
||||||
|
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => {
|
||||||
this._enqueueVideoThumbnail(tile, img);
|
this._enqueueVideoThumbnail(tile, img);
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -315,17 +317,22 @@ const photosView = {
|
|||||||
// Auth is handled via HttpOnly cookie — direct URL works
|
// Auth is handled via HttpOnly cookie — direct URL works
|
||||||
video.src = `/api/files/${fileId}`;
|
video.src = `/api/files/${fileId}`;
|
||||||
|
|
||||||
video.addEventListener('loadeddata', () => {
|
video.addEventListener(
|
||||||
|
'loadeddata',
|
||||||
|
() => {
|
||||||
// Seek to 25 % of duration, clamped between 0.5 s and 5 s
|
// 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));
|
video.currentTime = Math.min(5, Math.max(0.5, video.duration * 0.25));
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
video.addEventListener('seeked', () => {
|
video.addEventListener(
|
||||||
|
'seeked',
|
||||||
|
() => {
|
||||||
// Pre-scale to thumbnail size in the browser — saves ~22× RAM,
|
// Pre-scale to thumbnail size in the browser — saves ~22× RAM,
|
||||||
// ~15× bandwidth, and lets the server skip resize entirely.
|
// ~15× bandwidth, and lets the server skip resize entirely.
|
||||||
const MAX_THUMB = 400; // must match ThumbnailSize::Preview
|
const MAX_THUMB = 400; // must match ThumbnailSize::Preview
|
||||||
const scale = Math.min(MAX_THUMB / video.videoWidth,
|
const scale = Math.min(MAX_THUMB / video.videoWidth, MAX_THUMB / video.videoHeight, 1);
|
||||||
MAX_THUMB / video.videoHeight, 1);
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = Math.round(video.videoWidth * scale);
|
canvas.width = Math.round(video.videoWidth * scale);
|
||||||
canvas.height = Math.round(video.videoHeight * scale);
|
canvas.height = Math.round(video.videoHeight * scale);
|
||||||
@@ -336,7 +343,8 @@ const photosView = {
|
|||||||
// and server stores as-is when dimensions fit (zero re-encode).
|
// and server stores as-is when dimensions fit (zero re-encode).
|
||||||
const mimeType = 'image/jpeg';
|
const mimeType = 'image/jpeg';
|
||||||
|
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
this._drainDecodeQueue();
|
this._drainDecodeQueue();
|
||||||
return;
|
return;
|
||||||
@@ -349,17 +357,17 @@ const photosView = {
|
|||||||
this._videoThumbCache.set(fileId, url);
|
this._videoThumbCache.set(fileId, url);
|
||||||
|
|
||||||
// Upload to server for permanent caching
|
// Upload to server for permanent caching
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||||
|| sessionStorage.getItem('token');
|
|
||||||
const headers = { 'Content-Type': blob.type, ...getCsrfHeaders() };
|
const headers = { 'Content-Type': blob.type, ...getCsrfHeaders() };
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers,
|
headers,
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: blob,
|
body: blob
|
||||||
}).then((resp) => {
|
})
|
||||||
|
.then((resp) => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
// Switch from blob URL to server URL so the blob
|
// Switch from blob URL to server URL so the blob
|
||||||
// can be garbage-collected and future loads use
|
// can be garbage-collected and future loads use
|
||||||
@@ -367,26 +375,38 @@ const photosView = {
|
|||||||
const serverUrl = `/api/files/${fileId}/thumbnail/preview?v=1`;
|
const serverUrl = `/api/files/${fileId}/thumbnail/preview?v=1`;
|
||||||
this._videoThumbCache.set(fileId, serverUrl);
|
this._videoThumbCache.set(fileId, serverUrl);
|
||||||
}
|
}
|
||||||
}).catch(() => { /* best-effort */ });
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* best-effort */
|
||||||
|
});
|
||||||
|
|
||||||
// Release video resources
|
// Release video resources
|
||||||
video.src = '';
|
video.src = '';
|
||||||
video.load();
|
video.load();
|
||||||
this._drainDecodeQueue();
|
this._drainDecodeQueue();
|
||||||
}, mimeType, 0.80);
|
},
|
||||||
}, { once: true });
|
mimeType,
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
// If the video can't be loaded at all, keep the generic play badge
|
// If the video can't be loaded at all, keep the generic play badge
|
||||||
video.addEventListener('error', () => {
|
video.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => {
|
||||||
video.src = '';
|
video.src = '';
|
||||||
video.load();
|
video.load();
|
||||||
this._drainDecodeQueue();
|
this._drainDecodeQueue();
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Render the group mode toolbar */
|
/** Render the group mode toolbar */
|
||||||
_renderToolbar() {
|
_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 = [
|
const modes = [
|
||||||
['daily', t('photos.view_daily', 'Day')],
|
['daily', t('photos.view_daily', 'Day')],
|
||||||
['monthly', t('photos.view_monthly', 'Month')],
|
['monthly', t('photos.view_monthly', 'Month')],
|
||||||
@@ -403,7 +423,7 @@ const photosView = {
|
|||||||
|
|
||||||
/** Render empty state */
|
/** Render empty state */
|
||||||
_renderEmpty() {
|
_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 = `
|
this._container.innerHTML = `
|
||||||
<div class="photos-empty">
|
<div class="photos-empty">
|
||||||
<i class="fas fa-images"></i>
|
<i class="fas fa-images"></i>
|
||||||
@@ -422,10 +442,16 @@ const photosView = {
|
|||||||
if (this.groupMode === 'yearly') {
|
if (this.groupMode === 'yearly') {
|
||||||
key = String(d.getFullYear());
|
key = String(d.getFullYear());
|
||||||
} else if (this.groupMode === 'monthly') {
|
} else if (this.groupMode === 'monthly') {
|
||||||
key = d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' });
|
key = d.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
key = d.toLocaleDateString(undefined, {
|
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, []);
|
if (!map.has(key)) map.set(key, []);
|
||||||
@@ -456,7 +482,7 @@ const photosView = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise open lightbox
|
// 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) {
|
if (idx >= 0 && window.photosLightbox) {
|
||||||
window.photosLightbox.open(this.items, idx);
|
window.photosLightbox.open(this.items, idx);
|
||||||
}
|
}
|
||||||
@@ -490,7 +516,7 @@ const photosView = {
|
|||||||
document.body.appendChild(bar);
|
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;
|
const count = this.selected.size;
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
|
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
|
||||||
@@ -501,7 +527,9 @@ const photosView = {
|
|||||||
|
|
||||||
bar.querySelector('#photos-sel-clear').onclick = () => {
|
bar.querySelector('#photos-sel-clear').onclick = () => {
|
||||||
this.selected.clear();
|
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();
|
this._hideSelectionBar();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -518,7 +546,7 @@ const photosView = {
|
|||||||
console.error('Delete failed:', fid, err);
|
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.selected.clear();
|
||||||
this._hideSelectionBar();
|
this._hideSelectionBar();
|
||||||
this._renderedCount = 0;
|
this._renderedCount = 0;
|
||||||
@@ -571,7 +599,9 @@ const photosView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_escAttr(s) {
|
_escAttr(s) {
|
||||||
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
return String(s || '')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,11 @@ const photosLightbox = {
|
|||||||
// Format date
|
// Format date
|
||||||
const ts = (item.sort_date || item.created_at) * 1000;
|
const ts = (item.sort_date || item.created_at) * 1000;
|
||||||
const dateStr = new Date(ts).toLocaleDateString(undefined, {
|
const dateStr = new Date(ts).toLocaleDateString(undefined, {
|
||||||
year: 'numeric', month: 'short', day: 'numeric',
|
year: 'numeric',
|
||||||
hour: '2-digit', minute: '2-digit'
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
});
|
});
|
||||||
meta.textContent = `${dateStr} · ${item.size_formatted || ''}`;
|
meta.textContent = `${dateStr} · ${item.size_formatted || ''}`;
|
||||||
|
|
||||||
@@ -136,7 +139,7 @@ const photosLightbox = {
|
|||||||
content.innerHTML = '<div class="photos-loading"><i class="fas fa-spinner"></i></div>';
|
content.innerHTML = '<div class="photos-loading"><i class="fas fa-spinner"></i></div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isVideo = item.mime_type && item.mime_type.startsWith('video/');
|
const isVideo = item.mime_type?.startsWith('video/');
|
||||||
const res = await fetch(`/api/files/${item.id}`, {
|
const res = await fetch(`/api/files/${item.id}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: this._headers()
|
headers: this._headers()
|
||||||
@@ -169,7 +172,7 @@ const photosLightbox = {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
let parts = [dateStr];
|
const parts = [dateStr];
|
||||||
if (sizeStr) parts.push(sizeStr);
|
if (sizeStr) parts.push(sizeStr);
|
||||||
if (data.camera_make || data.camera_model) {
|
if (data.camera_make || data.camera_model) {
|
||||||
parts.push([data.camera_make, data.camera_model].filter(Boolean).join(' '));
|
parts.push([data.camera_make, data.camera_model].filter(Boolean).join(' '));
|
||||||
@@ -179,7 +182,7 @@ const photosLightbox = {
|
|||||||
}
|
}
|
||||||
metaEl.textContent = parts.join(' · ');
|
metaEl.textContent = parts.join(' · ');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Non-critical, keep existing meta
|
// Non-critical, keep existing meta
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,7 +236,7 @@ const photosLightbox = {
|
|||||||
});
|
});
|
||||||
// Remove from photosView items too
|
// Remove from photosView items too
|
||||||
if (window.photosView) {
|
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);
|
this.items.splice(this.index, 1);
|
||||||
if (this.items.length === 0) {
|
if (this.items.length === 0) {
|
||||||
@@ -274,7 +277,9 @@ const photosLightbox = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_escAttr(s) {
|
_escAttr(s) {
|
||||||
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
return String(s || '')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const recent = {
|
|||||||
*/
|
*/
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
document.addEventListener('file-accessed', (event) => {
|
document.addEventListener('file-accessed', (event) => {
|
||||||
if (event.detail && event.detail.file) {
|
if (event.detail?.file) {
|
||||||
const file = event.detail.file;
|
const file = event.detail.file;
|
||||||
const itemType = file.item_type || 'file';
|
const itemType = file.item_type || 'file';
|
||||||
this._recordAccess(file.id, itemType);
|
this._recordAccess(file.id, itemType);
|
||||||
@@ -144,7 +144,7 @@ const recent = {
|
|||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) window.ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying recent files:', error);
|
console.error('Error displaying recent files:', error);
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading recent files');
|
window.ui.showNotification('Error', 'Error loading recent files');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ const fileSharing = {
|
|||||||
item_name: options.name || null,
|
item_name: options.name || null,
|
||||||
item_type: itemType,
|
item_type: itemType,
|
||||||
password: options.password || null,
|
password: options.password || null,
|
||||||
expires_at: options.expirationDate
|
expires_at: options.expirationDate ? Math.floor(new Date(options.expirationDate).getTime() / 1000) : null,
|
||||||
? Math.floor(new Date(options.expirationDate).getTime() / 1000)
|
permissions: options.permissions || {
|
||||||
: null,
|
read: true,
|
||||||
permissions: options.permissions || { read: true, write: false, reshare: false }
|
write: false,
|
||||||
|
reshare: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch('/api/shares', {
|
const res = await fetch('/api/shares', {
|
||||||
@@ -71,13 +73,16 @@ const fileSharing = {
|
|||||||
*/
|
*/
|
||||||
async getSharedLinksForItem(itemId, itemType) {
|
async getSharedLinksForItem(itemId, itemType) {
|
||||||
try {
|
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}`, {
|
const res = await fetch(`/api/shares?${params}`, {
|
||||||
headers: this._headers(false)
|
headers: this._headers(false)
|
||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return Array.isArray(data) ? data : (data.items || []);
|
return Array.isArray(data) ? data : data.items || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting shared links for item:', error);
|
console.error('Error getting shared links for item:', error);
|
||||||
return [];
|
return [];
|
||||||
@@ -184,7 +189,7 @@ const fileSharing = {
|
|||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
console.log('File sharing module initialized (API-backed)');
|
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');
|
const span = item.querySelector('span');
|
||||||
if (span && span.getAttribute('data-i18n') === 'nav.shared') {
|
if (span && span.getAttribute('data-i18n') === 'nav.shared') {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
|
|||||||
+377
-120
@@ -14,8 +14,8 @@ function t(key, params) {
|
|||||||
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
||||||
function _escJs(s) {
|
function _escJs(s) {
|
||||||
if (typeof s !== 'string') return '';
|
if (typeof s !== 'string') return '';
|
||||||
return s.replace(/[^\w .\-]/g, function(c) {
|
return s.replace(/[^\w .-]/g, (c) => {
|
||||||
return '\\x' + c.charCodeAt(0).toString(16).padStart(2, '0');
|
return `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +43,10 @@ function headers() {
|
|||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const k = 1024,
|
||||||
|
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
@@ -62,11 +63,11 @@ function timeAgo(dateStr) {
|
|||||||
|
|
||||||
/* ── Custom confirm modal ── */
|
/* ── Custom confirm modal ── */
|
||||||
function showConfirm(message) {
|
function showConfirm(message) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise((resolve) => {
|
||||||
var overlay = document.getElementById('confirm-modal');
|
const overlay = document.getElementById('confirm-modal');
|
||||||
var msgEl = document.getElementById('confirm-message');
|
const msgEl = document.getElementById('confirm-message');
|
||||||
var yesBtn = document.getElementById('confirm-yes');
|
const yesBtn = document.getElementById('confirm-yes');
|
||||||
var noBtn = document.getElementById('confirm-cancel');
|
const noBtn = document.getElementById('confirm-cancel');
|
||||||
msgEl.textContent = message;
|
msgEl.textContent = message;
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
overlay.classList.add('show-flex');
|
overlay.classList.add('show-flex');
|
||||||
@@ -79,9 +80,15 @@ function showConfirm(message) {
|
|||||||
overlay.removeEventListener('click', onOverlay);
|
overlay.removeEventListener('click', onOverlay);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
}
|
||||||
function onYes() { cleanup(true); }
|
function onYes() {
|
||||||
function onNo() { cleanup(false); }
|
cleanup(true);
|
||||||
function onOverlay(e) { if (e.target === overlay) cleanup(false); }
|
}
|
||||||
|
function onNo() {
|
||||||
|
cleanup(false);
|
||||||
|
}
|
||||||
|
function onOverlay(e) {
|
||||||
|
if (e.target === overlay) cleanup(false);
|
||||||
|
}
|
||||||
yesBtn.addEventListener('click', onYes);
|
yesBtn.addEventListener('click', onYes);
|
||||||
noBtn.addEventListener('click', onNo);
|
noBtn.addEventListener('click', onNo);
|
||||||
overlay.addEventListener('click', onOverlay);
|
overlay.addEventListener('click', onOverlay);
|
||||||
@@ -93,10 +100,12 @@ let activeTabName = 'dashboard';
|
|||||||
|
|
||||||
function switchTab(name, el) {
|
function switchTab(name, el) {
|
||||||
if (name === activeTabName) return;
|
if (name === activeTabName) return;
|
||||||
var oldTab = document.getElementById('tab-' + activeTabName);
|
var oldTab = document.getElementById(`tab-${activeTabName}`);
|
||||||
var newTab = document.getElementById('tab-' + name);
|
var newTab = document.getElementById(`tab-${name}`);
|
||||||
|
|
||||||
document.querySelectorAll('.admin-tab').forEach(function(b) { b.classList.remove('active'); });
|
document.querySelectorAll('.admin-tab').forEach((b) => {
|
||||||
|
b.classList.remove('active');
|
||||||
|
});
|
||||||
if (el) el.classList.add('active');
|
if (el) el.classList.add('active');
|
||||||
|
|
||||||
// Fade-out old tab
|
// Fade-out old tab
|
||||||
@@ -129,19 +138,22 @@ function switchTab(name, el) {
|
|||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/dashboard', { headers: headers(), credentials: 'same-origin' });
|
const resp = await fetch(`${API}/admin/dashboard`, {
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const d = await resp.json();
|
const d = await resp.json();
|
||||||
document.getElementById('ds-total-users').textContent = d.total_users;
|
document.getElementById('ds-total-users').textContent = d.total_users;
|
||||||
document.getElementById('ds-active-users').textContent = d.active_users;
|
document.getElementById('ds-active-users').textContent = d.active_users;
|
||||||
document.getElementById('ds-admin-users').textContent = d.admin_users;
|
document.getElementById('ds-admin-users').textContent = d.admin_users;
|
||||||
document.getElementById('ds-version').textContent = 'v' + d.server_version;
|
document.getElementById('ds-version').textContent = `v${d.server_version}`;
|
||||||
document.getElementById('ds-used').textContent = formatBytes(d.total_used_bytes);
|
document.getElementById('ds-used').textContent = formatBytes(d.total_used_bytes);
|
||||||
document.getElementById('ds-quota').textContent = formatBytes(d.total_quota_bytes);
|
document.getElementById('ds-quota').textContent = formatBytes(d.total_quota_bytes);
|
||||||
document.getElementById('ds-usage-pct').textContent = d.storage_usage_percent.toFixed(1) + '%';
|
document.getElementById('ds-usage-pct').textContent = `${d.storage_usage_percent.toFixed(1)}%`;
|
||||||
const bar = document.getElementById('ds-bar');
|
const bar = document.getElementById('ds-bar');
|
||||||
bar.style.width = Math.min(d.storage_usage_percent, 100) + '%';
|
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`;
|
||||||
bar.className = 'progress-fill ' + (d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green');
|
bar.className = `progress-fill ${d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green'}`;
|
||||||
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
|
||||||
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
|
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
|
||||||
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
|
||||||
@@ -160,55 +172,150 @@ async function loadDashboard() {
|
|||||||
showElement('ds-danger-card');
|
showElement('ds-danger-card');
|
||||||
document.getElementById('ds-overquota').textContent = d.users_over_quota;
|
document.getElementById('ds-overquota').textContent = d.users_over_quota;
|
||||||
}
|
}
|
||||||
} catch (e) { console.error('Dashboard error', e); }
|
} catch (e) {
|
||||||
|
console.error('Dashboard error', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const tbody = document.getElementById('users-tbody');
|
const tbody = document.getElementById('users-tbody');
|
||||||
tbody.innerHTML = '<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.loading_users')) + '</td></tr>';
|
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.loading_users'))}</td></tr>`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users?limit=' + PAGE_SIZE + '&offset=' + (usersPage * PAGE_SIZE), { headers: headers(), credentials: 'same-origin' });
|
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
|
||||||
if (!resp.ok) { tbody.innerHTML = '<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('admin.failed_load_users')) + '</td></tr>'; return; }
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
tbody.innerHTML =
|
||||||
|
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
|
escapeHtml(t('admin.failed_load_users')) +
|
||||||
|
'</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
totalUsers = data.total;
|
totalUsers = data.total;
|
||||||
const users = data.users;
|
const users = data.users;
|
||||||
if (users.length === 0) { tbody.innerHTML = '<tr><td colspan="7" class="table-status-empty">' + escapeHtml(t('admin.no_users_found')) + '</td></tr>'; return; }
|
if (users.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(t('admin.no_users_found'))}</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tbody.innerHTML = users.map(u => {
|
tbody.innerHTML = users
|
||||||
const quotaPct = u.storage_quota_bytes > 0 ? ((u.storage_used_bytes / u.storage_quota_bytes) * 100) : 0;
|
.map((u) => {
|
||||||
|
const quotaPct = u.storage_quota_bytes > 0 ? (u.storage_used_bytes / u.storage_quota_bytes) * 100 : 0;
|
||||||
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
|
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
|
||||||
const quotaText = u.storage_quota_bytes > 0 ? formatBytes(u.storage_used_bytes) + ' / ' + formatBytes(u.storage_quota_bytes) : formatBytes(u.storage_used_bytes) + ' / ∞';
|
const quotaText =
|
||||||
|
u.storage_quota_bytes > 0
|
||||||
|
? `${formatBytes(u.storage_used_bytes)} / ${formatBytes(u.storage_quota_bytes)}`
|
||||||
|
: `${formatBytes(u.storage_used_bytes)} / ∞`;
|
||||||
const isSelf = u.id === currentAdminId;
|
const isSelf = u.id === currentAdminId;
|
||||||
const isOidc = u.auth_provider && u.auth_provider !== 'local';
|
const isOidc = u.auth_provider && u.auth_provider !== 'local';
|
||||||
const authBadge = isOidc
|
const authBadge = isOidc
|
||||||
? '<span class="badge badge-oidc" title="Authenticated via ' + escapeHtml(u.auth_provider) + '"><i class="fas fa-key badge-admin-icon-small"></i> ' + escapeHtml(u.auth_provider) + '</span>'
|
? '<span class="badge badge-oidc" title="Authenticated via ' +
|
||||||
: '<span class="badge badge-local">' + escapeHtml(t('admin.local')) + '</span>';
|
escapeHtml(u.auth_provider) +
|
||||||
return '<tr>' +
|
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
|
||||||
'<td><div class="user-info"><span class="user-name">' + escapeHtml(u.username) + (isSelf ? ' <span class="user-self-badge">' + escapeHtml(t('admin.you_badge')) + '</span>' : '') + '</span><span class="user-email">' + escapeHtml(u.email) + '</span></div></td>' +
|
escapeHtml(u.auth_provider) +
|
||||||
'<td><span class="badge badge-' + escapeHtml(u.role) + '">' + (u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') + escapeHtml(u.role) + '</span></td>' +
|
'</span>'
|
||||||
'<td>' + authBadge + '</td>' +
|
: `<span class="badge badge-local">${escapeHtml(t('admin.local'))}</span>`;
|
||||||
'<td><span class="badge badge-' + (u.active ? 'active' : 'inactive') + '">' + (u.active ? escapeHtml(t('admin.active')) : escapeHtml(t('admin.inactive'))) + '</span></td>' +
|
return (
|
||||||
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" data-width="' + Math.min(quotaPct, 100) + '"></div></div><span class="quota-text">' + quotaText + '</span></div></td>' +
|
'<tr>' +
|
||||||
'<td class="user-last-login-cell">' + timeAgo(u.last_login_at) + '</td>' +
|
'<td><div class="user-info"><span class="user-name">' +
|
||||||
|
escapeHtml(u.username) +
|
||||||
|
(isSelf ? ` <span class="user-self-badge">${escapeHtml(t('admin.you_badge'))}</span>` : '') +
|
||||||
|
'</span><span class="user-email">' +
|
||||||
|
escapeHtml(u.email) +
|
||||||
|
'</span></div></td>' +
|
||||||
|
'<td><span class="badge badge-' +
|
||||||
|
escapeHtml(u.role) +
|
||||||
|
'">' +
|
||||||
|
(u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') +
|
||||||
|
escapeHtml(u.role) +
|
||||||
|
'</span></td>' +
|
||||||
|
'<td>' +
|
||||||
|
authBadge +
|
||||||
|
'</td>' +
|
||||||
|
'<td><span class="badge badge-' +
|
||||||
|
(u.active ? 'active' : 'inactive') +
|
||||||
|
'">' +
|
||||||
|
(u.active ? escapeHtml(t('admin.active')) : escapeHtml(t('admin.inactive'))) +
|
||||||
|
'</span></td>' +
|
||||||
|
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' +
|
||||||
|
quotaColor +
|
||||||
|
'" data-width="' +
|
||||||
|
Math.min(quotaPct, 100) +
|
||||||
|
'"></div></div><span class="quota-text">' +
|
||||||
|
quotaText +
|
||||||
|
'</span></div></td>' +
|
||||||
|
'<td class="user-last-login-cell">' +
|
||||||
|
timeAgo(u.last_login_at) +
|
||||||
|
'</td>' +
|
||||||
'<td><div class="actions-row">' +
|
'<td><div class="actions-row">' +
|
||||||
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="quota" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" data-quota="' + u.storage_quota_bytes + '" title="' + escapeHtml(t('admin.edit_quota_title')) + '"><i class="fas fa-box"></i></button>' +
|
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="quota" data-uid="' +
|
||||||
(isOidc ? '' : '<button class="btn btn-sm btn-secondary admin-action-btn" data-action="reset-pw" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="' + escapeHtml(t('admin.reset_password_title')) + '"><i class="fas fa-key"></i></button>') +
|
_escJs(u.id) +
|
||||||
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' + _escJs(u.id) + '" data-role="' + _escJs(u.role) + '" title="' + escapeHtml(t('admin.toggle_role_title')) + '"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
|
'" data-uname="' +
|
||||||
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + ' admin-action-btn" data-action="toggle-active" data-uid="' + _escJs(u.id) + '" data-active="' + u.active + '" title="' + (u.active ? escapeHtml(t('admin.deactivate_title')) : escapeHtml(t('admin.activate_title'))) + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
|
_escJs(u.username) +
|
||||||
'<button class="btn btn-sm btn-danger admin-action-btn" data-action="delete" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="' + escapeHtml(t('admin.delete_title')) + '"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
|
'" data-quota="' +
|
||||||
'</div></td></tr>';
|
u.storage_quota_bytes +
|
||||||
}).join('');
|
'" title="' +
|
||||||
|
escapeHtml(t('admin.edit_quota_title')) +
|
||||||
|
'"><i class="fas fa-box"></i></button>' +
|
||||||
|
(isOidc
|
||||||
|
? ''
|
||||||
|
: '<button class="btn btn-sm btn-secondary admin-action-btn" data-action="reset-pw" data-uid="' +
|
||||||
|
_escJs(u.id) +
|
||||||
|
'" data-uname="' +
|
||||||
|
_escJs(u.username) +
|
||||||
|
'" title="' +
|
||||||
|
escapeHtml(t('admin.reset_password_title')) +
|
||||||
|
'"><i class="fas fa-key"></i></button>') +
|
||||||
|
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' +
|
||||||
|
_escJs(u.id) +
|
||||||
|
'" data-role="' +
|
||||||
|
_escJs(u.role) +
|
||||||
|
'" title="' +
|
||||||
|
escapeHtml(t('admin.toggle_role_title')) +
|
||||||
|
'"' +
|
||||||
|
(isSelf ? ' disabled' : '') +
|
||||||
|
'><i class="fas fa-' +
|
||||||
|
(u.role === 'admin' ? 'user' : 'crown') +
|
||||||
|
'"></i></button>' +
|
||||||
|
'<button class="btn btn-sm ' +
|
||||||
|
(u.active ? 'btn-danger' : 'btn-success') +
|
||||||
|
' admin-action-btn" data-action="toggle-active" data-uid="' +
|
||||||
|
_escJs(u.id) +
|
||||||
|
'" data-active="' +
|
||||||
|
u.active +
|
||||||
|
'" title="' +
|
||||||
|
(u.active ? escapeHtml(t('admin.deactivate_title')) : escapeHtml(t('admin.activate_title'))) +
|
||||||
|
'"' +
|
||||||
|
(isSelf && u.active ? ' disabled' : '') +
|
||||||
|
'><i class="fas fa-' +
|
||||||
|
(u.active ? 'ban' : 'check') +
|
||||||
|
'"></i></button>' +
|
||||||
|
'<button class="btn btn-sm btn-danger admin-action-btn" data-action="delete" data-uid="' +
|
||||||
|
_escJs(u.id) +
|
||||||
|
'" data-uname="' +
|
||||||
|
_escJs(u.username) +
|
||||||
|
'" title="' +
|
||||||
|
escapeHtml(t('admin.delete_title')) +
|
||||||
|
'"' +
|
||||||
|
(isSelf ? ' disabled' : '') +
|
||||||
|
'><i class="fas fa-trash-alt"></i></button>' +
|
||||||
|
'</div></td></tr>'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
// Set dynamic progress bar widths (CSP-safe via JS property)
|
// Set dynamic progress bar widths (CSP-safe via JS property)
|
||||||
document.querySelectorAll('.progress-fill[data-width]').forEach(function(el) {
|
document.querySelectorAll('.progress-fill[data-width]').forEach((el) => {
|
||||||
el.style.width = el.dataset.width + '%';
|
el.style.width = `${el.dataset.width}%`;
|
||||||
el.removeAttribute('data-width');
|
el.removeAttribute('data-width');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wire up admin action buttons (replaces inline onclick handlers)
|
// Wire up admin action buttons (replaces inline onclick handlers)
|
||||||
document.querySelectorAll('.admin-action-btn').forEach(function(btn) {
|
document.querySelectorAll('.admin-action-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', () => {
|
||||||
var action = btn.dataset.action;
|
const action = btn.dataset.action;
|
||||||
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
|
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
|
||||||
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
|
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
|
||||||
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
|
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
|
||||||
@@ -223,23 +330,45 @@ async function loadUsers() {
|
|||||||
document.getElementById('prev-btn').disabled = usersPage === 0;
|
document.getElementById('prev-btn').disabled = usersPage === 0;
|
||||||
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tbody.innerHTML = '<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('admin.error_network', { message: e.message })) + '</td></tr>';
|
tbody.innerHTML =
|
||||||
|
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
|
escapeHtml(t('admin.error_network', { message: e.message })) +
|
||||||
|
'</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevPage() { if (usersPage > 0) { usersPage--; loadUsers(); } }
|
function prevPage() {
|
||||||
function nextPage() { if ((usersPage + 1) * PAGE_SIZE < totalUsers) { usersPage++; loadUsers(); } }
|
if (usersPage > 0) {
|
||||||
|
usersPage--;
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function nextPage() {
|
||||||
|
if ((usersPage + 1) * PAGE_SIZE < totalUsers) {
|
||||||
|
usersPage++;
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleRole(userId, currentRole) {
|
async function toggleRole(userId, currentRole) {
|
||||||
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
||||||
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
|
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId + '/role', {
|
const resp = await fetch(`${API}/admin/users/${userId}/role`, {
|
||||||
method: 'PUT', headers: headers(), credentials: 'same-origin', body: JSON.stringify({ role: newRole })
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ role: newRole })
|
||||||
});
|
});
|
||||||
if (resp.ok) loadUsers(); else { const e = await resp.json(); alert(e.message || t('admin.error_generic')); }
|
if (resp.ok) loadUsers();
|
||||||
} catch (e) { alert(t('admin.error_network', { message: e.message })); }
|
else {
|
||||||
|
const e = await resp.json();
|
||||||
|
alert(e.message || t('admin.error_generic'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(t('admin.error_network', { message: e.message }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(userId, currentActive) {
|
async function toggleActive(userId, currentActive) {
|
||||||
@@ -247,20 +376,41 @@ async function toggleActive(userId, currentActive) {
|
|||||||
const ok = await showConfirm(msg);
|
const ok = await showConfirm(msg);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId + '/active', {
|
const resp = await fetch(`${API}/admin/users/${userId}/active`, {
|
||||||
method: 'PUT', headers: headers(), credentials: 'same-origin', body: JSON.stringify({ active: !currentActive })
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ active: !currentActive })
|
||||||
});
|
});
|
||||||
if (resp.ok) loadUsers(); else { const e = await resp.json(); alert(e.message || t('admin.error_generic')); }
|
if (resp.ok) loadUsers();
|
||||||
} catch (e) { alert(t('admin.error_network', { message: e.message })); }
|
else {
|
||||||
|
const e = await resp.json();
|
||||||
|
alert(e.message || t('admin.error_generic'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(t('admin.error_network', { message: e.message }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(userId, username) {
|
async function deleteUser(userId, username) {
|
||||||
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
|
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId, { method: 'DELETE', headers: headers(), credentials: 'same-origin' });
|
const resp = await fetch(`${API}/admin/users/${userId}`, {
|
||||||
if (resp.ok) { loadUsers(); loadDashboard(); } else { const e = await resp.json(); alert(e.message || t('admin.error_generic')); }
|
method: 'DELETE',
|
||||||
} catch (e) { alert(t('admin.error_network', { message: e.message })); }
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
loadUsers();
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
const e = await resp.json();
|
||||||
|
alert(e.message || t('admin.error_generic'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(t('admin.error_network', { message: e.message }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let quotaUserId = '';
|
let quotaUserId = '';
|
||||||
@@ -272,19 +422,32 @@ function openQuotaModal(userId, username, currentQuota) {
|
|||||||
document.getElementById('qm-value').value = gb > 0 ? Math.round(gb * 10) / 10 : 0;
|
document.getElementById('qm-value').value = gb > 0 ? Math.round(gb * 10) / 10 : 0;
|
||||||
showElement('quota-modal', 'flex');
|
showElement('quota-modal', 'flex');
|
||||||
}
|
}
|
||||||
function closeQuotaModal() { hideElement('quota-modal'); }
|
function closeQuotaModal() {
|
||||||
|
hideElement('quota-modal');
|
||||||
|
}
|
||||||
|
|
||||||
async function saveQuota() {
|
async function saveQuota() {
|
||||||
const val = parseFloat(document.getElementById('qm-value').value) || 0;
|
const val = parseFloat(document.getElementById('qm-value').value) || 0;
|
||||||
const unit = parseInt(document.getElementById('qm-unit').value);
|
const unit = parseInt(document.getElementById('qm-unit').value, 10);
|
||||||
const bytes = Math.round(val * unit);
|
const bytes = Math.round(val * unit);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + quotaUserId + '/quota', {
|
const resp = await fetch(`${API}/admin/users/${quotaUserId}/quota`, {
|
||||||
method: 'PUT', headers: headers(), credentials: 'same-origin', body: JSON.stringify({ quota_bytes: bytes })
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ quota_bytes: bytes })
|
||||||
});
|
});
|
||||||
if (resp.ok) { closeQuotaModal(); loadUsers(); loadDashboard(); }
|
if (resp.ok) {
|
||||||
else { const e = await resp.json(); alert(e.message || t('admin.error_generic')); }
|
closeQuotaModal();
|
||||||
} catch (e) { alert(t('admin.error_network', { message: e.message })); }
|
loadUsers();
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
const e = await resp.json();
|
||||||
|
alert(e.message || t('admin.error_generic'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(t('admin.error_network', { message: e.message }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateUserModal() {
|
function openCreateUserModal() {
|
||||||
@@ -299,7 +462,9 @@ function openCreateUserModal() {
|
|||||||
showElement('create-user-modal', 'flex');
|
showElement('create-user-modal', 'flex');
|
||||||
setTimeout(() => document.getElementById('cu-username').focus(), 100);
|
setTimeout(() => document.getElementById('cu-username').focus(), 100);
|
||||||
}
|
}
|
||||||
function closeCreateUserModal() { hideElement('create-user-modal'); }
|
function closeCreateUserModal() {
|
||||||
|
hideElement('create-user-modal');
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCreateUser() {
|
async function submitCreateUser() {
|
||||||
const username = document.getElementById('cu-username').value.trim();
|
const username = document.getElementById('cu-username').value.trim();
|
||||||
@@ -307,19 +472,36 @@ async function submitCreateUser() {
|
|||||||
const email = document.getElementById('cu-email').value.trim() || null;
|
const email = document.getElementById('cu-email').value.trim() || null;
|
||||||
const role = document.getElementById('cu-role').value;
|
const role = document.getElementById('cu-role').value;
|
||||||
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
||||||
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value);
|
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value, 10);
|
||||||
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
||||||
|
|
||||||
const errorEl = document.getElementById('cu-error');
|
const errorEl = document.getElementById('cu-error');
|
||||||
if (username.length < 3) { errorEl.textContent = t('admin.error_username_short'); errorEl.className = 'alert alert-error'; return; }
|
if (username.length < 3) {
|
||||||
if (password.length < 8) { errorEl.textContent = t('admin.error_password_short'); errorEl.className = 'alert alert-error'; return; }
|
errorEl.textContent = t('admin.error_username_short');
|
||||||
|
errorEl.className = 'alert alert-error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
errorEl.textContent = t('admin.error_password_short');
|
||||||
|
errorEl.className = 'alert alert-error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('cu-submit');
|
const btn = document.getElementById('cu-submit');
|
||||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.creating'));
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.creating'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users', {
|
const resp = await fetch(`${API}/admin/users`, {
|
||||||
method: 'POST', headers: headers(), credentials: 'same-origin',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, password, email, role, quota_bytes: quotaBytes })
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
quota_bytes: quotaBytes
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
closeCreateUserModal();
|
closeCreateUserModal();
|
||||||
@@ -334,7 +516,8 @@ async function submitCreateUser() {
|
|||||||
errorEl.textContent = t('admin.error_network', { message: e.message });
|
errorEl.textContent = t('admin.error_network', { message: e.message });
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus"></i> ' + escapeHtml(t('admin.create_user'));
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(t('admin.create_user'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resetPwUserId = '';
|
let resetPwUserId = '';
|
||||||
@@ -347,32 +530,52 @@ function openResetPasswordModal(userId, username) {
|
|||||||
showElement('reset-pw-modal', 'flex');
|
showElement('reset-pw-modal', 'flex');
|
||||||
setTimeout(() => document.getElementById('rp-password').focus(), 100);
|
setTimeout(() => document.getElementById('rp-password').focus(), 100);
|
||||||
}
|
}
|
||||||
function closeResetPasswordModal() { hideElement('reset-pw-modal'); }
|
function closeResetPasswordModal() {
|
||||||
|
hideElement('reset-pw-modal');
|
||||||
|
}
|
||||||
|
|
||||||
async function submitResetPassword() {
|
async function submitResetPassword() {
|
||||||
const password = document.getElementById('rp-password').value;
|
const password = document.getElementById('rp-password').value;
|
||||||
const errorEl = document.getElementById('rp-error');
|
const errorEl = document.getElementById('rp-error');
|
||||||
if (password.length < 8) { errorEl.textContent = t('admin.error_password_short'); errorEl.className = 'alert alert-error'; return; }
|
if (password.length < 8) {
|
||||||
|
errorEl.textContent = t('admin.error_password_short');
|
||||||
|
errorEl.className = 'alert alert-error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('rp-submit');
|
const btn = document.getElementById('rp-submit');
|
||||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.resetting'));
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.resetting'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + resetPwUserId + '/password', {
|
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
|
||||||
method: 'PUT', headers: headers(), credentials: 'same-origin',
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({ new_password: password })
|
body: JSON.stringify({ new_password: password })
|
||||||
});
|
});
|
||||||
if (resp.ok) { closeResetPasswordModal(); }
|
if (resp.ok) {
|
||||||
else { const e = await resp.json().catch(() => ({})); errorEl.textContent = e.message || t('admin.error_generic'); errorEl.className = 'alert alert-error'; }
|
closeResetPasswordModal();
|
||||||
} catch (e) { errorEl.textContent = t('admin.error_network', { message: e.message }); errorEl.className = 'alert alert-error'; }
|
} else {
|
||||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('admin.reset_btn'));
|
const e = await resp.json().catch(() => ({}));
|
||||||
|
errorEl.textContent = e.message || t('admin.error_generic');
|
||||||
|
errorEl.className = 'alert alert-error';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorEl.textContent = t('admin.error_network', { message: e.message });
|
||||||
|
errorEl.className = 'alert alert-error';
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.reset_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleRegistration(enabled) {
|
async function toggleRegistration(enabled) {
|
||||||
if (enabled) hideElement('registration-warning');
|
if (enabled) hideElement('registration-warning');
|
||||||
else showElement('registration-warning', 'flex');
|
else showElement('registration-warning', 'flex');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/registration', {
|
const resp = await fetch(`${API}/admin/settings/registration`, {
|
||||||
method: 'PUT', headers: headers(), credentials: 'same-origin',
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({ registration_enabled: enabled })
|
body: JSON.stringify({ registration_enabled: enabled })
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
@@ -402,7 +605,7 @@ document.getElementById('disable-password').addEventListener('change', function(
|
|||||||
function showOidcStatus(msg, type) {
|
function showOidcStatus(msg, type) {
|
||||||
const el = document.getElementById('oidc-status');
|
const el = document.getElementById('oidc-status');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
el.className = 'alert alert-' + type;
|
el.className = `alert alert-${type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyCallback() {
|
function copyCallback() {
|
||||||
@@ -412,26 +615,47 @@ function copyCallback() {
|
|||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
const url = document.getElementById('issuer-url').value.trim();
|
const url = document.getElementById('issuer-url').value.trim();
|
||||||
if (!url) { showOidcStatus('Enter an Issuer URL first', 'error'); return; }
|
if (!url) {
|
||||||
|
showOidcStatus('Enter an Issuer URL first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const btn = document.getElementById('discover-btn');
|
const btn = document.getElementById('discover-btn');
|
||||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.discovering'));
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.discovering'))}`;
|
||||||
const resultDiv = document.getElementById('discovery-result');
|
const resultDiv = document.getElementById('discovery-result');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/oidc/test', { method: 'POST', headers: headers(), credentials: 'same-origin', body: JSON.stringify({ issuer_url: url }) });
|
const resp = await fetch(`${API}/admin/settings/oidc/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ issuer_url: url })
|
||||||
|
});
|
||||||
const r = await resp.json();
|
const r = await resp.json();
|
||||||
if (r.success) {
|
if (r.success) {
|
||||||
resultDiv.innerHTML = '<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ' + escapeHtml(r.message) + '</strong><dl><dt>Issuer</dt><dd>' + escapeHtml(r.issuer||'—') + '</dd><dt>Auth Endpoint</dt><dd>' + escapeHtml(r.authorization_endpoint||'—') + '</dd></dl></div>';
|
resultDiv.innerHTML =
|
||||||
if (!document.getElementById('provider-name').value && r.provider_name_suggestion) document.getElementById('provider-name').value = r.provider_name_suggestion;
|
'<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ' +
|
||||||
|
escapeHtml(r.message) +
|
||||||
|
'</strong><dl><dt>Issuer</dt><dd>' +
|
||||||
|
escapeHtml(r.issuer || '—') +
|
||||||
|
'</dd><dt>Auth Endpoint</dt><dd>' +
|
||||||
|
escapeHtml(r.authorization_endpoint || '—') +
|
||||||
|
'</dd></dl></div>';
|
||||||
|
if (!document.getElementById('provider-name').value && r.provider_name_suggestion)
|
||||||
|
document.getElementById('provider-name').value = r.provider_name_suggestion;
|
||||||
} else {
|
} else {
|
||||||
resultDiv.innerHTML = '<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ' + escapeHtml(r.message) + '</strong></div>';
|
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(r.message)}</strong></div>`;
|
||||||
}
|
}
|
||||||
} catch (e) { resultDiv.innerHTML = '<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ' + escapeHtml(e.message) + '</div>'; }
|
} catch (e) {
|
||||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-search"></i> ' + escapeHtml(t('admin.auto_discover'));
|
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(t('admin.auto_discover'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveOidcSettings() {
|
async function saveOidcSettings() {
|
||||||
const btn = document.getElementById('save-btn');
|
const btn = document.getElementById('save-btn');
|
||||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.saving'));
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
|
||||||
const body = {
|
const body = {
|
||||||
enabled: document.getElementById('oidc-enabled').checked,
|
enabled: document.getElementById('oidc-enabled').checked,
|
||||||
issuer_url: document.getElementById('issuer-url').value.trim(),
|
issuer_url: document.getElementById('issuer-url').value.trim(),
|
||||||
@@ -441,29 +665,51 @@ async function saveOidcSettings() {
|
|||||||
auto_provision: document.getElementById('auto-provision').checked,
|
auto_provision: document.getElementById('auto-provision').checked,
|
||||||
admin_groups: document.getElementById('admin-groups').value.trim() || null,
|
admin_groups: document.getElementById('admin-groups').value.trim() || null,
|
||||||
disable_password_login: document.getElementById('disable-password').checked,
|
disable_password_login: document.getElementById('disable-password').checked,
|
||||||
provider_name: document.getElementById('provider-name').value.trim() || null,
|
provider_name: document.getElementById('provider-name').value.trim() || null
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/oidc', { method: 'PUT', headers: headers(), credentials: 'same-origin', body: JSON.stringify(body) });
|
const resp = await fetch(`${API}/admin/settings/oidc`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const status = body.enabled ? t('admin.active').toLowerCase() : t('admin.disabled').toLowerCase();
|
const status = body.enabled ? t('admin.active').toLowerCase() : t('admin.disabled').toLowerCase();
|
||||||
showOidcStatus(t('admin.settings_saved', { status: status }), 'success');
|
showOidcStatus(t('admin.settings_saved', { status: status }), 'success');
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
const e = await resp.json().catch(() => ({}));
|
||||||
|
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
else { const e = await resp.json().catch(()=>({})); showOidcStatus('Error: ' + (e.message || resp.statusText), 'error'); }
|
} catch (e) {
|
||||||
} catch (e) { showOidcStatus(t('admin.error_network', { message: e.message }), 'error'); }
|
showOidcStatus(t('admin.error_network', { message: e.message }), 'error');
|
||||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('admin.save_btn'));
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.save_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const me = await fetch(API + '/auth/me', { headers: headers(), credentials: 'same-origin' });
|
const me = await fetch(`${API}/auth/me`, {
|
||||||
if (!me.ok) { showAccessDenied(); return; }
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (!me.ok) {
|
||||||
|
showAccessDenied();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const user = await me.json();
|
const user = await me.json();
|
||||||
if (user.role !== 'admin') { showAccessDenied(); return; }
|
if (user.role !== 'admin') {
|
||||||
|
showAccessDenied();
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentAdminId = user.id;
|
currentAdminId = user.id;
|
||||||
|
|
||||||
const oidcResp = await fetch(API + '/admin/settings/oidc', { headers: headers(), credentials: 'same-origin' });
|
const oidcResp = await fetch(`${API}/admin/settings/oidc`, {
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (oidcResp.ok) {
|
if (oidcResp.ok) {
|
||||||
const s = await oidcResp.json();
|
const s = await oidcResp.json();
|
||||||
document.getElementById('oidc-enabled').checked = s.enabled;
|
document.getElementById('oidc-enabled').checked = s.enabled;
|
||||||
@@ -480,8 +726,8 @@ async function init() {
|
|||||||
else hideElement('password-warning');
|
else hideElement('password-warning');
|
||||||
document.getElementById('callback-url').textContent = s.callback_url;
|
document.getElementById('callback-url').textContent = s.callback_url;
|
||||||
if (s.client_secret_set) showElement('secret-hint');
|
if (s.client_secret_set) showElement('secret-hint');
|
||||||
(s.env_overrides || []).forEach(field => {
|
(s.env_overrides || []).forEach((field) => {
|
||||||
const badge = document.getElementById('badge-' + field);
|
const badge = document.getElementById(`badge-${field}`);
|
||||||
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -489,7 +735,10 @@ async function init() {
|
|||||||
await loadDashboard();
|
await loadDashboard();
|
||||||
hideElement('loading');
|
hideElement('loading');
|
||||||
showElement('main-content');
|
showElement('main-content');
|
||||||
} catch (e) { console.error(e); showAccessDenied(); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAccessDenied();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAccessDenied() {
|
function showAccessDenied() {
|
||||||
@@ -498,14 +747,14 @@ function showAccessDenied() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Apply i18n when translations load / change ── */
|
/* ── Apply i18n when translations load / change ── */
|
||||||
document.addEventListener('translationsLoaded', function() {
|
document.addEventListener('translationsLoaded', () => {
|
||||||
if (window.i18n && window.i18n.translatePage) window.i18n.translatePage();
|
if (window.i18n?.translatePage) window.i18n.translatePage();
|
||||||
// Re-render dynamic content that uses t()
|
// Re-render dynamic content that uses t()
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
document.addEventListener('localeChanged', function() {
|
document.addEventListener('localeChanged', () => {
|
||||||
if (window.i18n && window.i18n.translatePage) window.i18n.translatePage();
|
if (window.i18n?.translatePage) window.i18n.translatePage();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
@@ -513,11 +762,19 @@ document.addEventListener('localeChanged', function() {
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
/* ── Event-listener wiring (replaces inline onclick/onchange) ── */
|
/* ── Event-listener wiring (replaces inline onclick/onchange) ── */
|
||||||
document.getElementById('tab-btn-dashboard').addEventListener('click', function(){ switchTab('dashboard', this); });
|
document.getElementById('tab-btn-dashboard').addEventListener('click', function () {
|
||||||
document.getElementById('tab-btn-users').addEventListener('click', function(){ switchTab('users', this); });
|
switchTab('dashboard', this);
|
||||||
document.getElementById('tab-btn-oidc').addEventListener('click', function(){ switchTab('oidc', this); });
|
});
|
||||||
|
document.getElementById('tab-btn-users').addEventListener('click', function () {
|
||||||
|
switchTab('users', this);
|
||||||
|
});
|
||||||
|
document.getElementById('tab-btn-oidc').addEventListener('click', function () {
|
||||||
|
switchTab('oidc', this);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('ds-registration').addEventListener('change', function(){ toggleRegistration(this.checked); });
|
document.getElementById('ds-registration').addEventListener('change', function () {
|
||||||
|
toggleRegistration(this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-create-user').addEventListener('click', openCreateUserModal);
|
document.getElementById('btn-create-user').addEventListener('click', openCreateUserModal);
|
||||||
document.getElementById('prev-btn').addEventListener('click', prevPage);
|
document.getElementById('prev-btn').addEventListener('click', prevPage);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// device-verify.js — Extracted from inline <script> in device-verify.html
|
// device-verify.js — Extracted from inline <script> in device-verify.html
|
||||||
(function () {
|
(() => {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var API_BASE = window.location.origin;
|
var API_BASE = window.location.origin;
|
||||||
var codeInput = document.getElementById('user-code');
|
var codeInput = document.getElementById('user-code');
|
||||||
var deviceInfo = document.getElementById('device-info');
|
var deviceInfo = document.getElementById('device-info');
|
||||||
@@ -20,11 +18,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-insert hyphen and lookup on input
|
// Auto-insert hyphen and lookup on input
|
||||||
codeInput.addEventListener('input', function (e) {
|
codeInput.addEventListener('input', (e) => {
|
||||||
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9\-]/g, '');
|
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
||||||
// Auto-insert hyphen after 4 chars
|
// Auto-insert hyphen after 4 chars
|
||||||
if (val.length === 4 && val.indexOf('-') === -1) {
|
if (val.length === 4 && val.indexOf('-') === -1) {
|
||||||
val = val + '-';
|
val = `${val}-`;
|
||||||
}
|
}
|
||||||
e.target.value = val;
|
e.target.value = val;
|
||||||
errorText.classList.add('hidden');
|
errorText.classList.add('hidden');
|
||||||
@@ -32,7 +30,9 @@
|
|||||||
// Debounce lookup
|
// Debounce lookup
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
if (val.length >= 9) {
|
if (val.length >= 9) {
|
||||||
debounceTimer = setTimeout(function () { lookupCode(val); }, 300);
|
debounceTimer = setTimeout(() => {
|
||||||
|
lookupCode(val);
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
deviceInfo.classList.add('hidden');
|
deviceInfo.classList.add('hidden');
|
||||||
actionButtons.classList.add('hidden');
|
actionButtons.classList.add('hidden');
|
||||||
@@ -40,12 +40,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wire up approve / deny buttons (replaces inline onclick)
|
// Wire up approve / deny buttons (replaces inline onclick)
|
||||||
btnApprove.addEventListener('click', function () { handleAction('approve'); });
|
btnApprove.addEventListener('click', () => {
|
||||||
btnDeny.addEventListener('click', function () { handleAction('deny'); });
|
handleAction('approve');
|
||||||
|
});
|
||||||
|
btnDeny.addEventListener('click', () => {
|
||||||
|
handleAction('deny');
|
||||||
|
});
|
||||||
|
|
||||||
async function lookupCode(code) {
|
async function lookupCode(code) {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
|
const resp = await fetch(`${API_BASE}/api/auth/device/verify?code=${encodeURIComponent(code)}`, {
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (resp.status === 401) {
|
if (resp.status === 401) {
|
||||||
@@ -53,7 +57,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!resp.ok) throw new Error('Lookup failed');
|
if (!resp.ok) throw new Error('Lookup failed');
|
||||||
var data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
if (data.valid) {
|
if (data.valid) {
|
||||||
currentCode = code;
|
currentCode = code;
|
||||||
@@ -77,7 +81,7 @@
|
|||||||
btnDeny.disabled = true;
|
btnDeny.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
|
const resp = await fetch(`${API_BASE}/api/auth/device/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
||||||
@@ -85,7 +89,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
var err = await resp.json().catch(function () { return {}; });
|
const err = await resp.json().catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
throw new Error(err.message || 'Action failed');
|
throw new Error(err.message || 'Action failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,23 +11,31 @@ switch(errorType) {
|
|||||||
errorTitle.textContent = 'Login Failed';
|
errorTitle.textContent = 'Login Failed';
|
||||||
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
||||||
errorAction.textContent = 'Try Again';
|
errorAction.textContent = 'Try Again';
|
||||||
errorAction.addEventListener('click', function() { history.back(); });
|
errorAction.addEventListener('click', () => {
|
||||||
|
history.back();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'session-expired':
|
case 'session-expired':
|
||||||
errorTitle.textContent = 'Session Expired';
|
errorTitle.textContent = 'Session Expired';
|
||||||
errorMessage.textContent = 'Your session has expired. Please try again.';
|
errorMessage.textContent = 'Your session has expired. Please try again.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function() { window.close(); });
|
errorAction.addEventListener('click', () => {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'not-found':
|
case 'not-found':
|
||||||
errorTitle.textContent = 'Not Found';
|
errorTitle.textContent = 'Not Found';
|
||||||
errorMessage.textContent = 'The requested page was not found.';
|
errorMessage.textContent = 'The requested page was not found.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function() { window.close(); });
|
errorAction.addEventListener('click', () => {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
errorTitle.textContent = 'Error';
|
errorTitle.textContent = 'Error';
|
||||||
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function() { window.close(); });
|
errorAction.addEventListener('click', () => {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ if (!/^[0-9a-fA-F]+$/.test(token)) {
|
|||||||
document.body.innerHTML = '<p>Invalid session token.</p>';
|
document.body.innerHTML = '<p>Invalid session token.</p>';
|
||||||
throw new Error('Invalid token format');
|
throw new Error('Invalid token format');
|
||||||
}
|
}
|
||||||
document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
|
document.getElementById('login-flow-form').action = `/login/v2/flow/${token}`;
|
||||||
|
|
||||||
// Check if OIDC is available and configure SSO button
|
// Check if OIDC is available and configure SSO button
|
||||||
(async function() {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch('/api/auth/oidc/providers');
|
const resp = await fetch('/api/auth/oidc/providers');
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
var info = await resp.json();
|
const info = await resp.json();
|
||||||
if (!info.enabled) return;
|
if (!info.enabled) return;
|
||||||
|
|
||||||
// Show OIDC section
|
// Show OIDC section
|
||||||
document.getElementById('oidc-section').classList.remove('hidden');
|
document.getElementById('oidc-section').classList.remove('hidden');
|
||||||
|
|
||||||
// Update button text with provider name
|
// Update button text with provider name
|
||||||
var btn = document.getElementById('oidc-button');
|
const btn = document.getElementById('oidc-button');
|
||||||
btn.textContent = 'Sign in with ' + (info.provider_name || 'SSO');
|
btn.textContent = `Sign in with ${info.provider_name || 'SSO'}`;
|
||||||
|
|
||||||
// If password login is disabled, hide the password form
|
// If password login is disabled, hide the password form
|
||||||
if (!info.password_login_enabled) {
|
if (!info.password_login_enabled) {
|
||||||
@@ -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
|
// SSO button redirects to the OIDC flow for this NC token
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', () => {
|
||||||
window.location.href = '/login/v2/flow/' + token + '/oidc';
|
window.location.href = `/login/v2/flow/${token}/oidc`;
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (_e) {
|
||||||
// OIDC not available — silently keep password-only mode
|
// 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', () => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
// Auto-close after 3 seconds
|
// Auto-close after 3 seconds
|
||||||
setTimeout(function() {
|
setTimeout(() => {
|
||||||
window.close();
|
window.close();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ function headers() {
|
|||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const k = 1024,
|
||||||
|
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
@@ -31,8 +32,14 @@ function timeAgo(dateStr) {
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/me', { headers: headers(), credentials: 'same-origin' });
|
const resp = await fetch(`${API}/auth/me`, {
|
||||||
if (!resp.ok) { showError(); return; }
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
showError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const user = await resp.json();
|
const user = await resp.json();
|
||||||
|
|
||||||
const initials = (user.username || '?').substring(0, 2).toUpperCase();
|
const initials = (user.username || '?').substring(0, 2).toUpperCase();
|
||||||
@@ -43,10 +50,10 @@ async function init() {
|
|||||||
const badge = document.getElementById('p-role-badge');
|
const badge = document.getElementById('p-role-badge');
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
badge.className = 'role-badge role-badge-admin';
|
badge.className = 'role-badge role-badge-admin';
|
||||||
badge.innerHTML = '<i class="fas fa-shield-alt"></i> ' + t('profile.role_admin');
|
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${t('profile.role_admin')}`;
|
||||||
} else {
|
} else {
|
||||||
badge.className = 'role-badge role-badge-user';
|
badge.className = 'role-badge role-badge-user';
|
||||||
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_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-username').textContent = user.username;
|
||||||
@@ -60,12 +67,12 @@ async function init() {
|
|||||||
|
|
||||||
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
||||||
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
||||||
document.getElementById('p-storage-pct').textContent = quota > 0 ? pct + '%' : '—';
|
document.getElementById('p-storage-pct').textContent = quota > 0 ? `${pct}%` : '—';
|
||||||
|
|
||||||
const bar = document.getElementById('p-storage-bar');
|
const bar = document.getElementById('p-storage-bar');
|
||||||
bar.style.width = pct + '%';
|
bar.style.width = `${pct}%`;
|
||||||
bar.className = 'storage-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green');
|
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'));
|
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : t('profile.unlimited')}`;
|
||||||
|
|
||||||
if (user.auth_provider && user.auth_provider !== 'local') {
|
if (user.auth_provider && user.auth_provider !== 'local') {
|
||||||
document.getElementById('password-section').classList.add('hidden');
|
document.getElementById('password-section').classList.add('hidden');
|
||||||
@@ -74,15 +81,16 @@ async function init() {
|
|||||||
loadAppPasswords();
|
loadAppPasswords();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oidcResp = await fetch(API + '/auth/oidc/providers', { credentials: 'same-origin' });
|
const oidcResp = await fetch(`${API}/auth/oidc/providers`, {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (oidcResp.ok) {
|
if (oidcResp.ok) {
|
||||||
const oidcInfo = await oidcResp.json();
|
const oidcInfo = await oidcResp.json();
|
||||||
if (!oidcInfo.password_login_enabled) {
|
if (!oidcInfo.password_login_enabled) {
|
||||||
document.getElementById('password-section').classList.add('hidden');
|
document.getElementById('password-section').classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (oidcErr) {
|
} catch (_oidcErr) {}
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('main-content').classList.remove('hidden');
|
document.getElementById('main-content').classList.remove('hidden');
|
||||||
@@ -105,40 +113,49 @@ async function changePassword(e) {
|
|||||||
const statusEl = document.getElementById('pw-status');
|
const statusEl = document.getElementById('pw-status');
|
||||||
|
|
||||||
if (newPw !== confirmPw) {
|
if (newPw !== confirmPw) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.passwords_no_match'))}</div>`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPw.length < 8) {
|
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>';
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.password_too_short'))}</div>`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('pw-submit');
|
const btn = document.getElementById('pw-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.updating'))}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/change-password', {
|
const resp = await fetch(`${API}/auth/change-password`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({ current_password: currentPw, new_password: newPw })
|
body: JSON.stringify({
|
||||||
|
current_password: currentPw,
|
||||||
|
new_password: newPw
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + escapeHtml(t('profile.password_updated')) + '</div>';
|
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();
|
document.getElementById('password-form').reset();
|
||||||
} else {
|
} else {
|
||||||
const err = await resp.json().catch(() => ({}));
|
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>';
|
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) {
|
} 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>';
|
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.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('profile.update_password'))}`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +191,9 @@ function renderPwRow(pw) {
|
|||||||
btn.className = 'btn btn-danger-sm';
|
btn.className = 'btn btn-danger-sm';
|
||||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||||
btn.title = t('profile.revoke_title');
|
btn.title = t('profile.revoke_title');
|
||||||
btn.addEventListener('click', function () { revokeAppPassword(pw.id, pw.label); });
|
btn.addEventListener('click', () => {
|
||||||
|
revokeAppPassword(pw.id, pw.label);
|
||||||
|
});
|
||||||
actions.appendChild(btn);
|
actions.appendChild(btn);
|
||||||
}
|
}
|
||||||
tr.append(label, created, lastUsed, status, actions);
|
tr.append(label, created, lastUsed, status, actions);
|
||||||
@@ -183,14 +202,19 @@ function renderPwRow(pw) {
|
|||||||
|
|
||||||
async function loadAppPasswords() {
|
async function loadAppPasswords() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords', { headers: headers(), credentials: 'same-origin' });
|
const resp = await fetch(`${API}/auth/app-passwords`, {
|
||||||
|
headers: headers(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
document.getElementById('app-passwords-section').classList.add('hidden');
|
document.getElementById('app-passwords-section').classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const passwords = data.app_passwords || data;
|
const passwords = data.app_passwords || data;
|
||||||
const userPws = passwords.filter(function (pw) { return !isAutoPassword(pw); });
|
const userPws = passwords.filter((pw) => {
|
||||||
|
return !isAutoPassword(pw);
|
||||||
|
});
|
||||||
const autoPws = passwords.filter(isAutoPassword);
|
const autoPws = passwords.filter(isAutoPassword);
|
||||||
|
|
||||||
// User-created passwords
|
// User-created passwords
|
||||||
@@ -238,16 +262,16 @@ async function createAppPassword() {
|
|||||||
const btn = document.getElementById('app-pw-generate');
|
const btn = document.getElementById('app-pw-generate');
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.error_label_required'))}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.generating'))}`;
|
||||||
statusEl.innerHTML = '';
|
statusEl.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords', {
|
const resp = await fetch(`${API}/auth/app-passwords`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -255,7 +279,10 @@ async function createAppPassword() {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const err = await resp.json().catch(() => ({}));
|
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>';
|
statusEl.innerHTML =
|
||||||
|
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
|
escapeHtml(err.message || t('profile.error_create_pw')) +
|
||||||
|
'</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await resp.json();
|
const result = await resp.json();
|
||||||
@@ -265,26 +292,28 @@ async function createAppPassword() {
|
|||||||
labelInput.value = '';
|
labelInput.value = '';
|
||||||
loadAppPasswords();
|
loadAppPasswords();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
|
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(t('profile.generate'))}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyAppPassword() {
|
function copyAppPassword() {
|
||||||
const pw = document.getElementById('app-pw-created-password').textContent;
|
const pw = document.getElementById('app-pw-created-password').textContent;
|
||||||
navigator.clipboard.writeText(pw).then(function () {
|
navigator.clipboard.writeText(pw).then(() => {
|
||||||
const btn = document.getElementById('app-pw-copy-btn');
|
const btn = document.getElementById('app-pw-copy-btn');
|
||||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
setTimeout(function () { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 1500);
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
||||||
|
}, 1500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokeAppPassword(id, label) {
|
async function revokeAppPassword(id, label) {
|
||||||
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
|
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
@@ -316,5 +345,9 @@ document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPass
|
|||||||
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
||||||
|
|
||||||
/* Re-render when language changes */
|
/* Re-render when language changes */
|
||||||
window.addEventListener('translationsLoaded', function () { init(); });
|
window.addEventListener('translationsLoaded', () => {
|
||||||
window.addEventListener('localeChanged', function () { init(); });
|
init();
|
||||||
|
});
|
||||||
|
window.addEventListener('localeChanged', () => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ const sharedView = {
|
|||||||
this.attachEventListeners();
|
this.attachEventListeners();
|
||||||
this.loadItems().then(() => this.filterAndSortItems());
|
this.loadItems().then(() => this.filterAndSortItems());
|
||||||
const c = document.getElementById('shared-container');
|
const c = document.getElementById('shared-container');
|
||||||
if (c) c.classList.remove("hidden");
|
if (c) c.classList.remove('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
const c = document.getElementById('shared-container');
|
const c = document.getElementById('shared-container');
|
||||||
if (c) c.classList.add("hidden");
|
if (c) c.classList.add('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load shared items from backend API
|
// Load shared items from backend API
|
||||||
@@ -181,7 +181,7 @@ const sharedView = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(container);
|
window.i18n.translateElement(container);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -195,7 +195,7 @@ const sharedView = {
|
|||||||
|
|
||||||
// Close dropdowns when clicking outside
|
// Close dropdowns when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
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');
|
if (!sel.contains(e.target)) sel.classList.remove('open');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -209,15 +209,23 @@ const sharedView = {
|
|||||||
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
|
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
|
||||||
const enablePw = document.getElementById('sv-enable-password');
|
const enablePw = document.getElementById('sv-enable-password');
|
||||||
const pwField = document.getElementById('sv-share-password');
|
const pwField = document.getElementById('sv-share-password');
|
||||||
if (enablePw) enablePw.addEventListener('change', () => {
|
if (enablePw)
|
||||||
if (pwField) { pwField.disabled = !enablePw.checked; if (enablePw.checked) pwField.focus(); }
|
enablePw.addEventListener('change', () => {
|
||||||
|
if (pwField) {
|
||||||
|
pwField.disabled = !enablePw.checked;
|
||||||
|
if (enablePw.checked) pwField.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const genPwBtn = document.getElementById('sv-generate-password');
|
const genPwBtn = document.getElementById('sv-generate-password');
|
||||||
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
|
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
|
||||||
const enableExp = document.getElementById('sv-enable-expiration');
|
const enableExp = document.getElementById('sv-enable-expiration');
|
||||||
const expField = document.getElementById('sv-share-expiration');
|
const expField = document.getElementById('sv-share-expiration');
|
||||||
if (enableExp) enableExp.addEventListener('change', () => {
|
if (enableExp)
|
||||||
if (expField) { expField.disabled = !enableExp.checked; if (enableExp.checked) expField.focus(); }
|
enableExp.addEventListener('change', () => {
|
||||||
|
if (expField) {
|
||||||
|
expField.disabled = !enableExp.checked;
|
||||||
|
if (enableExp.checked) expField.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const updateBtn = document.getElementById('sv-update-share-btn');
|
const updateBtn = document.getElementById('sv-update-share-btn');
|
||||||
if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem());
|
if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem());
|
||||||
@@ -253,17 +261,19 @@ const sharedView = {
|
|||||||
toggle.addEventListener('click', (e) => {
|
toggle.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Close other open selects
|
// 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');
|
if (sel !== wrapper) sel.classList.remove('open');
|
||||||
});
|
});
|
||||||
wrapper.classList.toggle('open');
|
wrapper.classList.toggle('open');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropdown.querySelectorAll('.shared-select-option').forEach(option => {
|
dropdown.querySelectorAll('.shared-select-option').forEach((option) => {
|
||||||
option.addEventListener('click', (e) => {
|
option.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Update active state
|
// 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');
|
option.classList.add('active');
|
||||||
// Update label
|
// Update label
|
||||||
const label = toggle.querySelector('.shared-select-label');
|
const label = toggle.querySelector('.shared-select-label');
|
||||||
@@ -288,7 +298,7 @@ const sharedView = {
|
|||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
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;
|
if (type !== 'all' && item.item_type !== type) return false;
|
||||||
const name = (item.item_name || item.item_id || '').toLowerCase();
|
const name = (item.item_name || item.item_id || '').toLowerCase();
|
||||||
return name.includes(searchTerm);
|
return name.includes(searchTerm);
|
||||||
@@ -321,16 +331,15 @@ const sharedView = {
|
|||||||
sharedItemsList.innerHTML = '';
|
sharedItemsList.innerHTML = '';
|
||||||
|
|
||||||
if (this.filteredItems.length === 0) {
|
if (this.filteredItems.length === 0) {
|
||||||
emptyState.classList.remove("hidden");
|
emptyState.classList.remove('hidden');
|
||||||
listContainer.classList.add("hidden");
|
listContainer.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyState.classList.add("hidden");
|
emptyState.classList.add('hidden');
|
||||||
listContainer.classList.remove("hidden");
|
listContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
this.filteredItems.forEach((item) => {
|
||||||
this.filteredItems.forEach(item => {
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const displayName = item.item_name || item.item_id || 'Unknown';
|
const displayName = item.item_name || item.item_id || 'Unknown';
|
||||||
|
|
||||||
@@ -383,7 +392,8 @@ const sharedView = {
|
|||||||
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
|
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
|
||||||
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
|
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
|
||||||
copyBtn.addEventListener('click', () => {
|
copyBtn.addEventListener('click', () => {
|
||||||
navigator.clipboard.writeText(item.url)
|
navigator.clipboard
|
||||||
|
.writeText(item.url)
|
||||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||||
});
|
});
|
||||||
@@ -392,7 +402,10 @@ const sharedView = {
|
|||||||
rmBtn.className = 'action-btn remove-btn';
|
rmBtn.className = 'action-btn remove-btn';
|
||||||
rmBtn.innerHTML = '<span class="action-icon">🗑️</span>';
|
rmBtn.innerHTML = '<span class="action-icon">🗑️</span>';
|
||||||
rmBtn.title = this.translate('shared_removeShare', 'Remove Share');
|
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);
|
actionsCell.append(editBtn, notifyBtn, copyBtn, rmBtn);
|
||||||
row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell);
|
row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell);
|
||||||
@@ -428,7 +441,10 @@ const sharedView = {
|
|||||||
|
|
||||||
if (enablePw) {
|
if (enablePw) {
|
||||||
enablePw.checked = item.has_password;
|
enablePw.checked = item.has_password;
|
||||||
if (pwField) { pwField.disabled = !enablePw.checked; pwField.value = ''; }
|
if (pwField) {
|
||||||
|
pwField.disabled = !enablePw.checked;
|
||||||
|
pwField.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (enableExp) {
|
if (enableExp) {
|
||||||
enableExp.checked = !!item.expires_at;
|
enableExp.checked = !!item.expires_at;
|
||||||
@@ -473,7 +489,8 @@ const sharedView = {
|
|||||||
copyShareLink() {
|
copyShareLink() {
|
||||||
const el = document.getElementById('sv-share-link-url');
|
const el = document.getElementById('sv-share-link-url');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
navigator.clipboard.writeText(el.value)
|
navigator.clipboard
|
||||||
|
.writeText(el.value)
|
||||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||||
},
|
},
|
||||||
@@ -514,10 +531,8 @@ const sharedView = {
|
|||||||
write: permWrite ? permWrite.checked : false,
|
write: permWrite ? permWrite.checked : false,
|
||||||
reshare: permReshare ? permReshare.checked : false
|
reshare: permReshare ? permReshare.checked : false
|
||||||
},
|
},
|
||||||
password: (enablePw && enablePw.checked && pwField && pwField.value) ? pwField.value : null,
|
password: enablePw?.checked && pwField?.value ? pwField.value : null,
|
||||||
expires_at: (enableExp && enableExp.checked && expField && expField.value)
|
expires_at: enableExp?.checked && expField?.value ? Math.floor(new Date(expField.value).getTime() / 1000) : null
|
||||||
? Math.floor(new Date(expField.value).getTime() / 1000)
|
|
||||||
: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -575,15 +590,19 @@ const sharedView = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.fileSharing && window.fileSharing.sendShareNotification) {
|
if (window.fileSharing?.sendShareNotification) {
|
||||||
window.fileSharing.sendShareNotification(this.currentItem.url, email, message)
|
window.fileSharing
|
||||||
.then(() => { this.closeNotificationDialog(); this.showNotification(this.translate('shared_notificationSent', 'Notification sent')); })
|
.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'));
|
.catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showNotification(message, type = 'success') {
|
showNotification(message, type = 'success') {
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(message, type);
|
window.ui.showNotification(message, type);
|
||||||
} else {
|
} else {
|
||||||
alert(message);
|
alert(message);
|
||||||
@@ -599,7 +618,7 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
translate(key, defaultText) {
|
translate(key, defaultText) {
|
||||||
if (window.i18n && window.i18n.t) return window.i18n.t(key, defaultText);
|
if (window.i18n?.t) return window.i18n.t(key, defaultText);
|
||||||
return defaultText;
|
return defaultText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+24
-17
@@ -16,10 +16,11 @@ const ASSETS_TO_CACHE = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Install event - cache assets
|
// Install event - cache assets
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME)
|
caches
|
||||||
.then(cache => {
|
.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
console.log('Cache opened');
|
console.log('Cache opened');
|
||||||
return cache.addAll(ASSETS_TO_CACHE);
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
})
|
})
|
||||||
@@ -28,42 +29,48 @@ self.addEventListener('install', event => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Activate event - clean old caches
|
// Activate event - clean old caches
|
||||||
self.addEventListener('activate', event => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.keys().then(cacheNames => {
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((cacheNames) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
cacheNames.filter(cacheName => {
|
cacheNames
|
||||||
|
.filter((cacheName) => {
|
||||||
return cacheName !== CACHE_NAME;
|
return cacheName !== CACHE_NAME;
|
||||||
}).map(cacheName => {
|
})
|
||||||
|
.map((cacheName) => {
|
||||||
return caches.delete(cacheName);
|
return caches.delete(cacheName);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}).then(() => self.clients.claim()) // Take control of clients
|
})
|
||||||
|
.then(() => self.clients.claim()) // Take control of clients
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch event - serve from cache, update cache from network
|
// Fetch event - serve from cache, update cache from network
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', (event) => {
|
||||||
// Don't intercept API requests - let them go straight to the network
|
// Don't intercept API requests - let them go straight to the network
|
||||||
if (event.request.url.includes('/api/')) {
|
if (event.request.url.includes('/api/')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request)
|
caches.match(event.request).then((response) => {
|
||||||
.then(response => {
|
|
||||||
// Cache hit - return the response from the cached version
|
// Cache hit - return the response from the cached version
|
||||||
if (response) {
|
if (response) {
|
||||||
// For non-core assets, still fetch from network for updates
|
// For non-core assets, still fetch from network for updates
|
||||||
if (!ASSETS_TO_CACHE.includes(new URL(event.request.url).pathname)) {
|
if (!ASSETS_TO_CACHE.includes(new URL(event.request.url).pathname)) {
|
||||||
fetch(event.request).then(networkResponse => {
|
fetch(event.request)
|
||||||
|
.then((networkResponse) => {
|
||||||
if (networkResponse && networkResponse.status === 200) {
|
if (networkResponse && networkResponse.status === 200) {
|
||||||
const clonedResponse = networkResponse.clone();
|
const clonedResponse = networkResponse.clone();
|
||||||
caches.open(CACHE_NAME).then(cache => {
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
cache.put(event.request, clonedResponse);
|
cache.put(event.request, clonedResponse);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// Ignore network fetch errors - we already have a cached version
|
// Ignore network fetch errors - we already have a cached version
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,7 +78,7 @@ self.addEventListener('fetch', event => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Not in cache - get from network and add to cache
|
// Not in cache - get from network and add to cache
|
||||||
return fetch(event.request).then(response => {
|
return fetch(event.request).then((response) => {
|
||||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -79,7 +86,7 @@ self.addEventListener('fetch', event => {
|
|||||||
// Clone the response as it's a stream and can only be consumed once
|
// Clone the response as it's a stream and can only be consumed once
|
||||||
const responseToCache = response.clone();
|
const responseToCache = response.clone();
|
||||||
|
|
||||||
caches.open(CACHE_NAME).then(cache => {
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
cache.put(event.request, responseToCache);
|
cache.put(event.request, responseToCache);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +97,7 @@ self.addEventListener('fetch', event => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Background sync for failed requests
|
// Background sync for failed requests
|
||||||
self.addEventListener('sync', event => {
|
self.addEventListener('sync', (event) => {
|
||||||
if (event.tag === 'oxicloud-sync') {
|
if (event.tag === 'oxicloud-sync') {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
// Implement background sync for pending file operations
|
// Implement background sync for pending file operations
|
||||||
|
|||||||
Reference in New Issue
Block a user