Merge pull request #291 from EdouardVanbelle/refactor/js-import-export
This commit is contained in:
+2
-1
@@ -14,7 +14,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"noUnusedVariables": "warn"
|
"noUnusedVariables": "warn",
|
||||||
|
"noUndeclaredVariables": "error"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noDescendingSpecificity": "off"
|
"noDescendingSpecificity": "off"
|
||||||
|
|||||||
@@ -113,15 +113,19 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) {
|
|||||||
// ── 4. Minify ALL individual CSS in static-dist/ ─────────────────────────
|
// ── 4. Minify ALL individual CSS in static-dist/ ─────────────────────────
|
||||||
minify_tree_css(&dist_dir.join("css"));
|
minify_tree_css(&dist_dir.join("css"));
|
||||||
|
|
||||||
// ── 5. Build JS bundle for index.html ────────────────────────────────────
|
// ── 5. Bundle all ES modules into one IIFE ───────────────────────────────
|
||||||
|
// Walk the import graph starting from every <script type="module"> in index.html,
|
||||||
|
// strip import/export syntax, wrap in an IIFE, then minify as a classic script.
|
||||||
let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html");
|
let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html");
|
||||||
let defer_scripts = extract_defer_scripts(&index_html);
|
let module_scripts = extract_module_scripts(&index_html);
|
||||||
let js_bundle = build_js_bundle(static_dir, &defer_scripts);
|
let js_raw = build_js_module_bundle(&module_scripts, static_dir);
|
||||||
|
let js_bundle = js_minify_script_safe(&js_raw);
|
||||||
let js_hash = fnv_hash(js_bundle.as_bytes());
|
let js_hash = fnv_hash(js_bundle.as_bytes());
|
||||||
let js_name = format!("app.{js_hash}.js");
|
let js_name = format!("app.{js_hash}.js");
|
||||||
|
fs::create_dir_all(dist_dir.join("js")).expect("js dir");
|
||||||
fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle");
|
fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle");
|
||||||
|
|
||||||
// ── 6. Minify ALL individual JS in static-dist/ ──────────────────────────
|
// ── 6. Minify ALL individual JS files in static-dist/ ────────────────────
|
||||||
minify_tree_js(&dist_dir.join("js"));
|
minify_tree_js(&dist_dir.join("js"));
|
||||||
|
|
||||||
// ── 7. Inline theme-init.js & rewrite index.html ──────────────────────
|
// ── 7. Inline theme-init.js & rewrite index.html ──────────────────────
|
||||||
@@ -246,15 +250,15 @@ fn minify_tree_css(dir: &Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// JS processing
|
// JS bundling (ES module → single IIFE)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/// Collect `<script defer src="…">` paths from HTML.
|
/// Collect `<script type="module" src="…">` paths from HTML.
|
||||||
fn extract_defer_scripts(html: &str) -> Vec<String> {
|
fn extract_module_scripts(html: &str) -> Vec<String> {
|
||||||
html.lines()
|
html.lines()
|
||||||
.filter_map(|l| {
|
.filter_map(|l| {
|
||||||
let t = l.trim();
|
let t = l.trim();
|
||||||
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
|
if t.starts_with("<script") && t.contains("type=\"module\"") && t.contains("src=\"") {
|
||||||
let s = t.find("src=\"")? + 5;
|
let s = t.find("src=\"")? + 5;
|
||||||
let e = t[s..].find('"')? + s;
|
let e = t[s..].find('"')? + s;
|
||||||
Some(t[s..e].to_string())
|
Some(t[s..e].to_string())
|
||||||
@@ -265,35 +269,275 @@ fn extract_defer_scripts(html: &str) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minify each script individually, then concatenate (safer than a monolith parse).
|
/// Build a single IIFE from all ES-module entry points.
|
||||||
fn build_js_bundle(static_dir: &Path, script_paths: &[String]) -> String {
|
///
|
||||||
let mut bundle = String::with_capacity(512 * 1024);
|
/// Algorithm:
|
||||||
for path in script_paths {
|
/// 1. DFS from each entry point, following `import … from '…'` edges.
|
||||||
let file = static_dir.join(path.trim_start_matches('/'));
|
/// 2. Post-order traversal ensures every dependency is emitted before its importer.
|
||||||
if file.exists() {
|
/// 3. Cycles are broken by marking files as visited before recursing.
|
||||||
let src = fs::read_to_string(&file).unwrap_or_default();
|
/// 4. Each file has its import/export syntax stripped before being appended.
|
||||||
let min = js_minify_safe(&src);
|
/// 5. The result is wrapped in `(function(){"use strict"; …})();`.
|
||||||
bundle.push_str(&min);
|
fn build_js_module_bundle(entry_scripts: &[String], static_dir: &Path) -> String {
|
||||||
bundle.push_str(";\n");
|
use std::collections::HashSet;
|
||||||
} else {
|
|
||||||
eprintln!("cargo:warning=JS not found: {}", file.display());
|
let mut order: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut seen: HashSet<PathBuf> = HashSet::new();
|
||||||
|
|
||||||
|
for script in entry_scripts {
|
||||||
|
let path = static_dir.join(script.trim_start_matches('/'));
|
||||||
|
collect_module_deps(&path, &mut order, &mut seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bundle = String::with_capacity(2 * 1024 * 1024);
|
||||||
|
bundle.push_str("(function(){\n\"use strict\";\n");
|
||||||
|
for file in &order {
|
||||||
|
match fs::read_to_string(file) {
|
||||||
|
Ok(src) => {
|
||||||
|
bundle.push_str(&strip_esm_syntax(&src));
|
||||||
|
bundle.push('\n');
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("cargo:warning=bundle: cannot read {}: {e}", file.display()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bundle.push_str("})();\n");
|
||||||
bundle
|
bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minify JS via oxc — returns original on failure.
|
/// DFS post-order: push `file` to `order` after all its imports.
|
||||||
|
/// Marks files as seen before recursing to break circular dependencies.
|
||||||
|
fn collect_module_deps(
|
||||||
|
file: &Path,
|
||||||
|
order: &mut Vec<PathBuf>,
|
||||||
|
seen: &mut std::collections::HashSet<PathBuf>,
|
||||||
|
) {
|
||||||
|
let canonical = match file.canonicalize() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("cargo:warning=JS import not found: {}", file.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !seen.insert(canonical.clone()) {
|
||||||
|
return; // already visited (or in-progress cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = fs::read_to_string(file).unwrap_or_default();
|
||||||
|
let base = file.parent().unwrap_or(Path::new("."));
|
||||||
|
|
||||||
|
for rel in extract_esm_import_paths(&src) {
|
||||||
|
if rel.starts_with('.') {
|
||||||
|
collect_module_deps(&base.join(&rel), order, seen);
|
||||||
|
}
|
||||||
|
// Non-relative (bare specifiers like 'react') are ignored — not used here.
|
||||||
|
}
|
||||||
|
|
||||||
|
order.push(file.to_path_buf());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return all relative paths found in `import … from '…'` / `export … from '…'` lines.
|
||||||
|
fn extract_esm_import_paths(source: &str) -> Vec<String> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
let mut multiline = false;
|
||||||
|
|
||||||
|
for line in source.lines() {
|
||||||
|
let t = line.trim();
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
// Waiting for the `from '…'` of a multi-line import
|
||||||
|
if let Some(p) = extract_from_clause(t) {
|
||||||
|
paths.push(p);
|
||||||
|
multiline = false;
|
||||||
|
} else if t.ends_with(';') {
|
||||||
|
multiline = false; // malformed, give up on this import
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !t.starts_with("import ") && !t.starts_with("export ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(p) = extract_from_clause(t) {
|
||||||
|
paths.push(p);
|
||||||
|
} else if t.starts_with("import ") && !t.ends_with(';') && !t.contains("//") {
|
||||||
|
// Multi-line: `import {\n X,\n Y\n} from '…'`
|
||||||
|
multiline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the path string from the `from '…'` or `from "…"` tail of a line.
|
||||||
|
fn extract_from_clause(s: &str) -> Option<String> {
|
||||||
|
let from = s.rfind(" from ")?;
|
||||||
|
let rest = s[from + 6..].trim();
|
||||||
|
let q = rest.chars().next()?;
|
||||||
|
if q != '\'' && q != '"' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let end = rest[1..].find(q)? + 1;
|
||||||
|
Some(rest[1..end].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip ES-module syntax from a single file so it can be inlined into an IIFE.
|
||||||
|
///
|
||||||
|
/// | Input | Output |
|
||||||
|
/// |------------------------------------------|------------------------------|
|
||||||
|
/// | `import { X } from './y.js';` | *(empty line)* |
|
||||||
|
/// | `import { X as Y } from './y.js';` | `const Y = X;` |
|
||||||
|
/// | `export { X, Y };` | *(empty line)* |
|
||||||
|
/// | `export { X } from './y.js';` | *(empty line)* |
|
||||||
|
/// | `export const X = …` | `const X = …` |
|
||||||
|
/// | `export function f() {…}` | `function f() {…}` |
|
||||||
|
/// | `export async function f() {…}` | `async function f() {…}` |
|
||||||
|
/// | `export class C {…}` | `class C {…}` |
|
||||||
|
/// | `export default expr;` | `const _default = expr;` |
|
||||||
|
fn strip_esm_syntax(source: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(source.len());
|
||||||
|
// True while we are inside a multi-line import/export-list that has not yet
|
||||||
|
// seen its terminating `;`.
|
||||||
|
let mut skipping = false;
|
||||||
|
|
||||||
|
for line in source.lines() {
|
||||||
|
let t = line.trim();
|
||||||
|
|
||||||
|
if skipping {
|
||||||
|
// Keep skipping until the statement ends
|
||||||
|
if t.ends_with(';') || t.contains(" from ") {
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
out.push('\n'); // preserve line count for source maps / debugging
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── import … ──────────────────────────────────────────────────────────
|
||||||
|
// Emit `const Y = X;` for any `import { X as Y }` aliases so that code
|
||||||
|
// using the aliased name still resolves inside the IIFE scope.
|
||||||
|
if t.starts_with("import ") {
|
||||||
|
if !t.ends_with(';') && !t.contains(" from ") {
|
||||||
|
skipping = true; // multi-line import
|
||||||
|
}
|
||||||
|
let aliases = collect_import_aliases(t);
|
||||||
|
if aliases.is_empty() {
|
||||||
|
out.push('\n');
|
||||||
|
} else {
|
||||||
|
out.push_str(&aliases);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── export { … } or export { … } from '…' ────────────────────────────
|
||||||
|
if t.starts_with("export {") || t.starts_with("export{") {
|
||||||
|
if !t.ends_with(';') {
|
||||||
|
skipping = true;
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── export const/let/var/function/async function/class ─────────────────
|
||||||
|
if let Some(stripped) = try_strip_export_prefix(line) {
|
||||||
|
out.push_str(&stripped);
|
||||||
|
out.push('\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── export default expr ────────────────────────────────────────────────
|
||||||
|
// Rare in our codebase; keep the value as a named variable.
|
||||||
|
if let Some(rhs) = t.strip_prefix("export default ") {
|
||||||
|
let indent = &line[..line.len() - line.trim_start().len()];
|
||||||
|
out.push_str(&format!("{indent}const _default = {rhs}"));
|
||||||
|
out.push('\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `line` (with leading whitespace preserved) begins with `export <decl-keyword>`,
|
||||||
|
/// return the same line with `export ` (7 chars) removed.
|
||||||
|
fn try_strip_export_prefix(line: &str) -> Option<String> {
|
||||||
|
const PREFIXES: &[&str] = &[
|
||||||
|
"export const ",
|
||||||
|
"export let ",
|
||||||
|
"export var ",
|
||||||
|
"export function ",
|
||||||
|
"export async function ",
|
||||||
|
"export class ",
|
||||||
|
];
|
||||||
|
let t = line.trim();
|
||||||
|
for prefix in PREFIXES {
|
||||||
|
if t.starts_with(prefix) {
|
||||||
|
let indent_len = line.len() - line.trim_start().len();
|
||||||
|
// Remove "export " (7 chars) right after the indent
|
||||||
|
return Some(format!(
|
||||||
|
"{}{}",
|
||||||
|
&line[..indent_len],
|
||||||
|
&line[indent_len + 7..]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For `import { A, B as C, D as E } from '…'` return `"const C = B;\nconst E = D;"`.
|
||||||
|
/// Returns an empty string when there are no aliases.
|
||||||
|
fn collect_import_aliases(stmt: &str) -> String {
|
||||||
|
let brace_start = match stmt.find('{') {
|
||||||
|
Some(i) => i + 1,
|
||||||
|
None => return String::new(),
|
||||||
|
};
|
||||||
|
let brace_end = match stmt.find('}') {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return String::new(),
|
||||||
|
};
|
||||||
|
let bindings = &stmt[brace_start..brace_end];
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
for binding in bindings.split(',') {
|
||||||
|
let b = binding.trim();
|
||||||
|
if let Some(as_pos) = b.find(" as ") {
|
||||||
|
let orig = b[..as_pos].trim();
|
||||||
|
let alias = b[as_pos + 4..].trim();
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(&format!("const {alias} = {orig};"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// JS minification
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Minify an ES-module file (contains import/export) — returns original on failure.
|
||||||
fn js_minify_safe(source: &str) -> String {
|
fn js_minify_safe(source: &str) -> String {
|
||||||
|
js_minify_inner(source, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minify a classic script / IIFE bundle (no import/export) — returns original on failure.
|
||||||
|
fn js_minify_script_safe(source: &str) -> String {
|
||||||
|
js_minify_inner(source, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_minify_inner(source: &str, is_module: bool) -> String {
|
||||||
if source.trim().is_empty() {
|
if source.trim().is_empty() {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
js_minify(source).unwrap_or_else(|e| {
|
js_minify(source, is_module).unwrap_or_else(|e| {
|
||||||
eprintln!("cargo:warning=JS minify failed: {e}");
|
eprintln!("cargo:warning=JS minify failed: {e}");
|
||||||
source.to_string()
|
source.to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn js_minify(source: &str) -> Result<String, String> {
|
fn js_minify(source: &str, is_module: bool) -> Result<String, String> {
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_codegen::{Codegen, CodegenOptions, CommentOptions};
|
use oxc_codegen::{Codegen, CodegenOptions, CommentOptions};
|
||||||
use oxc_minifier::{CompressOptions, CompressOptionsUnused, Minifier, MinifierOptions};
|
use oxc_minifier::{CompressOptions, CompressOptionsUnused, Minifier, MinifierOptions};
|
||||||
@@ -301,7 +545,11 @@ fn js_minify(source: &str) -> Result<String, String> {
|
|||||||
use oxc_span::SourceType;
|
use oxc_span::SourceType;
|
||||||
|
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let source_type = SourceType::cjs(); // Non-module, script mode
|
let source_type = if is_module {
|
||||||
|
SourceType::mjs()
|
||||||
|
} else {
|
||||||
|
SourceType::cjs()
|
||||||
|
};
|
||||||
let ret = Parser::new(&allocator, source, source_type).parse();
|
let ret = Parser::new(&allocator, source, source_type).parse();
|
||||||
|
|
||||||
if !ret.errors.is_empty() {
|
if !ret.errors.is_empty() {
|
||||||
@@ -311,8 +559,6 @@ fn js_minify(source: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
let mut program = ret.program;
|
let mut program = ret.program;
|
||||||
|
|
||||||
// Compress (constant-fold, dead-code) — NO mangle (globals would break)
|
|
||||||
// Keep unused top-level functions: they're called cross-file via window.*
|
|
||||||
Minifier::new(MinifierOptions {
|
Minifier::new(MinifierOptions {
|
||||||
mangle: None,
|
mangle: None,
|
||||||
compress: Some(CompressOptions {
|
compress: Some(CompressOptions {
|
||||||
@@ -337,7 +583,7 @@ fn js_minify(source: &str) -> Result<String, String> {
|
|||||||
Ok(output.code)
|
Ok(output.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk a directory and minify every `.js` in-place (skips generated bundles).
|
/// Walk a directory and minify every `.js` in-place (skips generated `app.*` bundles).
|
||||||
fn minify_tree_js(dir: &Path) {
|
fn minify_tree_js(dir: &Path) {
|
||||||
let Ok(entries) = fs::read_dir(dir) else {
|
let Ok(entries) = fs::read_dir(dir) else {
|
||||||
return;
|
return;
|
||||||
@@ -412,11 +658,15 @@ fn json_minify(source: &str) -> String {
|
|||||||
// HTML rewriting
|
// HTML rewriting
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/// Rewrite index.html: single CSS bundle, inline theme-init, single JS bundle.
|
/// Rewrite index.html for release:
|
||||||
|
/// - Collapse all `<link stylesheet href="/css/…">` into the single CSS bundle.
|
||||||
|
/// - Inline `theme-init.js` as a `<script>` block.
|
||||||
|
/// - Replace all `<script type="module" src="…">` with the single JS bundle.
|
||||||
|
/// - Leave the non-module `sw-register.js` script untouched.
|
||||||
fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js: &str) -> String {
|
fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js: &str) -> String {
|
||||||
let mut out: Vec<String> = Vec::with_capacity(html.lines().count());
|
let mut out: Vec<String> = Vec::with_capacity(html.lines().count());
|
||||||
let mut css_done = false;
|
let mut css_done = false;
|
||||||
let mut defer_done = false;
|
let mut js_done = false;
|
||||||
|
|
||||||
for line in html.lines() {
|
for line in html.lines() {
|
||||||
let t = line.trim();
|
let t = line.trim();
|
||||||
@@ -436,22 +686,19 @@ fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Replace all defer <script>s with single bundle ──────────────────
|
// ── Replace all type="module" scripts with single bundle ─────────────
|
||||||
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
|
if t.starts_with("<script") && t.contains("type=\"module\"") && t.contains("src=\"") {
|
||||||
if !defer_done {
|
if !js_done {
|
||||||
out.push(format!(" <script defer src=\"{js_path}\"></script>"));
|
out.push(format!(
|
||||||
defer_done = true;
|
" <script defer type=\"module\" src=\"{js_path}\"></script>"
|
||||||
|
));
|
||||||
|
js_done = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Drop "Service Worker Registration" comment ──────────────────────
|
// ── Drop "Styles" / "Scripts" section comments ───────────────────────
|
||||||
if t.contains("Service Worker Registration") {
|
if t.starts_with("<!--") && (t.contains("Styles") || t.contains("Scripts")) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Drop "Styles" / "Scripts" section comments ──────────────────────
|
|
||||||
if t.starts_with("<!--") && (t.contains("Styles") || t.contains("Scripts (defer")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,8 +728,6 @@ fn update_sw_cache(sw: &str, css_bundle: &str, js_bundle: &str) -> String {
|
|||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{before}const ASSETS_TO_CACHE = [\n\
|
"{before}const ASSETS_TO_CACHE = [\n\
|
||||||
\x20 '/',\n\
|
|
||||||
\x20 '/index.html',\n\
|
|
||||||
\x20 '/css/{css_bundle}',\n\
|
\x20 '/css/{css_bundle}',\n\
|
||||||
\x20 '/js/{js_bundle}',\n\
|
\x20 '/js/{js_bundle}',\n\
|
||||||
\x20 '/locales/en.json',\n\
|
\x20 '/locales/en.json',\n\
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ impl AudioMetadataService {
|
|||||||
///
|
///
|
||||||
/// All I/O is synchronous (id3 + mp3_duration crates), so this MUST
|
/// All I/O is synchronous (id3 + mp3_duration crates), so this MUST
|
||||||
/// only be called inside `spawn_blocking`.
|
/// only be called inside `spawn_blocking`.
|
||||||
fn extract_metadata_blocking(
|
fn extract_metadata_blocking(file_path: &Path) -> Option<AudioMetadataFields> {
|
||||||
file_path: &Path,
|
|
||||||
) -> Option<AudioMetadataFields> {
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
warn!("File does not exist: {:?}", file_path);
|
warn!("File does not exist: {:?}", file_path);
|
||||||
return None;
|
return None;
|
||||||
@@ -114,12 +112,13 @@ impl AudioMetadataService {
|
|||||||
|
|
||||||
// ── Sync I/O on the blocking thread pool (never stalls Tokio workers) ──
|
// ── Sync I/O on the blocking thread pool (never stalls Tokio workers) ──
|
||||||
let path = file_path.to_path_buf();
|
let path = file_path.to_path_buf();
|
||||||
let metadata = tokio::task::spawn_blocking(move || {
|
let metadata = tokio::task::spawn_blocking(move || Self::extract_metadata_blocking(&path))
|
||||||
Self::extract_metadata_blocking(&path)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::internal_error("AudioMetadataService", format!("spawn_blocking join error: {e}"))
|
DomainError::internal_error(
|
||||||
|
"AudioMetadataService",
|
||||||
|
format!("spawn_blocking join error: {e}"),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Some(m) = metadata else {
|
let Some(m) = metadata else {
|
||||||
|
|||||||
@@ -717,7 +717,11 @@ impl DedupService {
|
|||||||
// Also clean up any thumbnail files for these blob hashes
|
// Also clean up any thumbnail files for these blob hashes
|
||||||
// (thumbnails are keyed by blob_hash and live under
|
// (thumbnails are keyed by blob_hash and live under
|
||||||
// storage_root/.thumbnails/{icon,preview,large}/{hash}.jpg).
|
// storage_root/.thumbnails/{icon,preview,large}/{hash}.jpg).
|
||||||
let thumbnails_root = self.blob_root.parent().unwrap_or(&self.blob_root).join(".thumbnails");
|
let thumbnails_root = self
|
||||||
|
.blob_root
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(&self.blob_root)
|
||||||
|
.join(".thumbnails");
|
||||||
for (hash, size) in &batch {
|
for (hash, size) in &batch {
|
||||||
let blob_path = self.blob_path(hash);
|
let blob_path = self.blob_path(hash);
|
||||||
if let Err(e) = fs::remove_file(&blob_path).await {
|
if let Err(e) = fs::remove_file(&blob_path).await {
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ async fn generate_thumbnail_from_blob_path() {
|
|||||||
|
|
||||||
// The key assertion: the service can read from a blob path (not a logical path)
|
// The key assertion: the service can read from a blob path (not a logical path)
|
||||||
let result = svc
|
let result = svc
|
||||||
.get_thumbnail("test-file-id", "ab1234567890", ThumbnailSize::Icon, &blob_path)
|
.get_thumbnail(
|
||||||
|
"test-file-id",
|
||||||
|
"ab1234567890",
|
||||||
|
ThumbnailSize::Icon,
|
||||||
|
&blob_path,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let thumb_bytes = result.expect("thumbnail generation should succeed from blob path");
|
let thumb_bytes = result.expect("thumbnail generation should succeed from blob path");
|
||||||
@@ -67,7 +72,12 @@ async fn generate_thumbnail_nonexistent_path_returns_error() {
|
|||||||
|
|
||||||
let bad_path = tmp.path().join("does-not-exist.png");
|
let bad_path = tmp.path().join("does-not-exist.png");
|
||||||
let result = svc
|
let result = svc
|
||||||
.get_thumbnail("missing-id", "nonexistent-hash", ThumbnailSize::Icon, &bad_path)
|
.get_thumbnail(
|
||||||
|
"missing-id",
|
||||||
|
"nonexistent-hash",
|
||||||
|
ThumbnailSize::Icon,
|
||||||
|
&bad_path,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(result.is_err(), "should fail for nonexistent file");
|
assert!(result.is_err(), "should fail for nonexistent file");
|
||||||
|
|||||||
@@ -746,7 +746,11 @@ impl FileHandler {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
tracing::info!("🖼️ Generating thumbnails for: {}", file_id);
|
tracing::info!("🖼️ Generating thumbnails for: {}", file_id);
|
||||||
thumbnail_service.generate_all_sizes_background(file_id, blob_hash_owned, file_path);
|
thumbnail_service.generate_all_sizes_background(
|
||||||
|
file_id,
|
||||||
|
blob_hash_owned,
|
||||||
|
file_path,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ use crate::application::services::share_service::ShareService;
|
|||||||
use crate::{
|
use crate::{
|
||||||
application::{
|
application::{
|
||||||
dtos::share_dto::{CreateShareDto, UpdateShareDto},
|
dtos::share_dto::{CreateShareDto, UpdateShareDto},
|
||||||
ports::{file_ports::{FileRetrievalUseCase, OptimizedFileContent}, share_ports::ShareUseCase},
|
ports::{
|
||||||
|
file_ports::{FileRetrievalUseCase, OptimizedFileContent},
|
||||||
|
share_ports::ShareUseCase,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
common::{di::AppState, errors::ErrorKind},
|
common::{di::AppState, errors::ErrorKind},
|
||||||
domain::entities::share::ShareItemType,
|
domain::entities::share::ShareItemType,
|
||||||
@@ -290,7 +293,7 @@ pub async fn download_shared_file(
|
|||||||
"Sharing is disabled",
|
"Sharing is disabled",
|
||||||
"Disabled",
|
"Disabled",
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -243,10 +243,7 @@ pub async fn auth_middleware(
|
|||||||
if request.uri().path().starts_with("/webdav") {
|
if request.uri().path().starts_with("/webdav") {
|
||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::UNAUTHORIZED)
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
.header(
|
.header(header::WWW_AUTHENTICATE, r#"Basic realm="OxiCloud""#)
|
||||||
header::WWW_AUTHENTICATE,
|
|
||||||
r#"Basic realm="OxiCloud""#,
|
|
||||||
)
|
|
||||||
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
|
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
|
||||||
.body(axum::body::Body::from(
|
.body(axum::body::Body::from(
|
||||||
"Invalid username or app password",
|
"Invalid username or app password",
|
||||||
|
|||||||
+1
-5
@@ -5,10 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OxiCloud — Admin Panel</title>
|
<title>OxiCloud — Admin Panel</title>
|
||||||
<script src="/js/core/theme-init.js"></script>
|
<script src="/js/core/theme-init.js"></script>
|
||||||
<script src="/js/core/i18n.js" defer></script>
|
|
||||||
<script src="/js/core/icons.js" defer></script>
|
|
||||||
<script src="/js/core/formatters.js" defer></script>
|
|
||||||
<script src="/js/core/csrf.js" defer></script>
|
|
||||||
<link rel="stylesheet" href="/css/main.css" />
|
<link rel="stylesheet" href="/css/main.css" />
|
||||||
<link rel="stylesheet" href="/css/views/admin.css" />
|
<link rel="stylesheet" href="/css/views/admin.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -608,6 +604,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/views/admin/admin.js" defer></script>
|
<script type="module" src="/js/views/admin/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
<div id="status-error" class="status error hidden"></div>
|
<div id="status-error" class="status error hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/core/csrf.js"></script>
|
<script type="module" src="/js/views/device-verify/device-verify.js"></script>
|
||||||
<script src="/js/views/device-verify/device-verify.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+31
-32
@@ -19,38 +19,37 @@
|
|||||||
<link rel="stylesheet" href="/css/views/music.css">
|
<link rel="stylesheet" href="/css/views/music.css">
|
||||||
|
|
||||||
<!-- Scripts (defer: download in parallel, execute in order, after HTML parsed) -->
|
<!-- Scripts (defer: download in parallel, execute in order, after HTML parsed) -->
|
||||||
<script defer src="/js/core/i18n.js"></script>
|
<script defer type="module" src="/js/core/i18n.js"></script>
|
||||||
<script defer src="/js/core/csrf.js"></script>
|
<script defer type="module" src="/js/core/csrf.js"></script>
|
||||||
<script defer src="/js/core/languageSelector.js"></script>
|
<script defer type="module" src="/js/core/languageSelector.js"></script>
|
||||||
<script defer src="/js/core/notifications.js"></script>
|
<script defer type="module" src="/js/core/notifications.js"></script>
|
||||||
<script defer src="/js/core/modal.js"></script>
|
<script defer type="module" src="/js/core/modal.js"></script>
|
||||||
<script defer src="/js/core/formatters.js"></script>
|
<script defer type="module" src="/js/core/formatters.js"></script>
|
||||||
<script defer src="/js/app/state.js"></script>
|
<script defer type="module" src="/js/app/state.js"></script>
|
||||||
<script defer src="/js/app/uiFileTypes.js"></script>
|
<script defer type="module" src="/js/app/uiFileTypes.js"></script>
|
||||||
<script defer src="/js/app/uiNotifications.js"></script>
|
<script defer type="module" src="/js/app/uiNotifications.js"></script>
|
||||||
<script defer src="/js/app/ui.js"></script>
|
<script defer type="module" src="/js/app/ui.js"></script>
|
||||||
<script defer src="/js/features/files/contextMenus.js"></script>
|
<script defer type="module" src="/js/features/files/contextMenus.js"></script>
|
||||||
<script defer src="/js/features/files/fileOperations.js"></script>
|
<script defer type="module" src="/js/features/files/fileOperations.js"></script>
|
||||||
<script defer src="/js/features/files/multiSelect.js"></script>
|
<script defer type="module" src="/js/features/files/multiSelect.js"></script>
|
||||||
<script defer src="/js/features/files/search.js"></script>
|
<script defer type="module" src="/js/features/files/search.js"></script>
|
||||||
<script defer src="/js/features/library/favorites.js"></script>
|
<script defer type="module" src="/js/features/library/favorites.js"></script>
|
||||||
<script defer src="/js/features/library/recent.js"></script>
|
<script defer type="module" src="/js/features/library/recent.js"></script>
|
||||||
<script defer src="/js/features/library/photos.js"></script>
|
<script defer type="module" src="/js/features/library/photos.js"></script>
|
||||||
<script defer src="/js/features/library/photosLightbox.js"></script>
|
<script defer type="module" src="/js/features/library/music.js"></script>
|
||||||
<script defer src="/js/features/library/music.js"></script>
|
<script defer type="module" src="/js/features/sharing/fileSharing.js"></script>
|
||||||
<script defer src="/js/features/sharing/fileSharing.js"></script>
|
<script defer type="module" src="/js/views/shared/sharedView.js"></script>
|
||||||
<script defer src="/js/views/shared/sharedView.js"></script>
|
<script defer type="module" src="/js/features/files/inlineViewer.js"></script>
|
||||||
<script defer src="/js/features/files/inlineViewer.js"></script>
|
<script defer type="module" src="/js/features/files/wopiEditor.js"></script>
|
||||||
<script defer src="/js/features/files/wopiEditor.js"></script>
|
<script defer type="module" src="/js/core/icons.js"></script>
|
||||||
<script defer src="/js/core/icons.js"></script>
|
<script defer type="module" src="/js/app/navigation.js"></script>
|
||||||
<script defer src="/js/app/navigation.js"></script>
|
<script defer type="module" src="/js/app/authSession.js"></script>
|
||||||
<script defer src="/js/app/authSession.js"></script>
|
<script defer type="module" src="/js/app/userMenu.js"></script>
|
||||||
<script defer src="/js/app/userMenu.js"></script>
|
<script defer type="module" src="/js/app/filesView.js"></script>
|
||||||
<script defer src="/js/app/filesView.js"></script>
|
<script defer type="module" src="/js/app/trashView.js"></script>
|
||||||
<script defer src="/js/app/trashView.js"></script>
|
<script defer type="module" src="/js/app/searchView.js"></script>
|
||||||
<script defer src="/js/app/searchView.js"></script>
|
<script defer type="module" src="/js/app/main.js"></script>
|
||||||
<script defer src="/js/app/main.js"></script>
|
<script defer type="module" src="/js/app/bootstrap.js"></script>
|
||||||
<script defer src="/js/app/bootstrap.js"></script>
|
|
||||||
|
|
||||||
<!-- Service Worker Registration -->
|
<!-- Service Worker Registration -->
|
||||||
<script defer src="/js/core/sw-register.js"></script>
|
<script defer src="/js/core/sw-register.js"></script>
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
* Authentication/session bootstrap and home-folder resolution
|
* Authentication/session bootstrap and home-folder resolution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getCsrfHeaders } from '../core/csrf.js';
|
||||||
|
import { loadFiles } from './filesView.js';
|
||||||
|
import { updateStorageUsageDisplay } from './main.js';
|
||||||
|
import { app } from './state.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
async function refreshUserData() {
|
async function refreshUserData() {
|
||||||
const USER_DATA_KEY = 'oxicloud_user';
|
const USER_DATA_KEY = 'oxicloud_user';
|
||||||
|
|
||||||
@@ -24,7 +30,7 @@ async function refreshUserData() {
|
|||||||
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
|
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
|
||||||
|
|
||||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||||
window.updateStorageUsageDisplay(userData);
|
updateStorageUsageDisplay(userData);
|
||||||
return userData;
|
return userData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing user data:', error);
|
console.error('Error refreshing user data:', error);
|
||||||
@@ -89,7 +95,7 @@ async function checkAuthentication() {
|
|||||||
if (menuName) menuName.textContent = userData.username;
|
if (menuName) menuName.textContent = userData.username;
|
||||||
if (menuEmail) menuEmail.textContent = userData.email || '';
|
if (menuEmail) menuEmail.textContent = userData.email || '';
|
||||||
|
|
||||||
window.updateStorageUsageDisplay(userData);
|
updateStorageUsageDisplay(userData);
|
||||||
|
|
||||||
// Validate session BEFORE loading files to avoid 401 race condition
|
// Validate session BEFORE loading files to avoid 401 race condition
|
||||||
const freshData = await refreshUserData();
|
const freshData = await refreshUserData();
|
||||||
@@ -133,8 +139,8 @@ async function checkAuthentication() {
|
|||||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => {
|
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => {
|
||||||
el.textContent = userInitials;
|
el.textContent = userInitials;
|
||||||
});
|
});
|
||||||
window.updateStorageUsageDisplay(freshData);
|
updateStorageUsageDisplay(freshData);
|
||||||
resolveHomeFolder().then(() => window.loadFiles());
|
resolveHomeFolder().then(() => loadFiles());
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not retrieve user data, redirecting to login');
|
console.warn('Could not retrieve user data, redirecting to login');
|
||||||
localStorage.removeItem(USER_DATA_KEY);
|
localStorage.removeItem(USER_DATA_KEY);
|
||||||
@@ -154,8 +160,6 @@ async function checkAuthentication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveHomeFolder() {
|
async function resolveHomeFolder() {
|
||||||
const app = window.app;
|
|
||||||
|
|
||||||
if (app.userHomeFolderId) return;
|
if (app.userHomeFolderId) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/folders', {
|
const response = await fetch('/api/folders', {
|
||||||
@@ -173,22 +177,20 @@ async function resolveHomeFolder() {
|
|||||||
app.userHomeFolderName = home.name;
|
app.userHomeFolderName = home.name;
|
||||||
app.currentPath = home.id;
|
app.currentPath = home.id;
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
console.log(`Home folder resolved: ${home.name} (${home.id})`);
|
console.log(`Home folder resolved: ${home.name} (${home.id})`);
|
||||||
} else {
|
} else {
|
||||||
console.warn('No root folders found for user');
|
console.warn('No root folders found for user');
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resolving home folder:', error);
|
console.error('Error resolving home folder:', error);
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.refreshUserData = refreshUserData;
|
export { checkAuthentication, refreshUserData, resolveHomeFolder };
|
||||||
window.checkAuthentication = checkAuthentication;
|
|
||||||
window.resolveHomeFolder = resolveHomeFolder;
|
|
||||||
|
|||||||
Vendored
+4
-7
@@ -2,13 +2,10 @@
|
|||||||
* OxiCloud - App bootstrap
|
* OxiCloud - App bootstrap
|
||||||
* Isolated startup trigger for the main application initializer.
|
* Isolated startup trigger for the main application initializer.
|
||||||
*/
|
*/
|
||||||
|
import { initApp } from './main.js';
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
if (typeof window.initApp === 'function') {
|
} else {
|
||||||
window.initApp();
|
initApp();
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (typeof window.initApp === 'function') {
|
|
||||||
window.initApp();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-35
@@ -1,5 +1,16 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { inlineViewer } from '../features/files/inlineViewer.js';
|
||||||
|
import { multiSelect } from '../features/files/multiSelect.js';
|
||||||
|
import { resolveHomeFolder } from './authSession.js';
|
||||||
|
import { updateHistory } from './main.js';
|
||||||
|
import { app } from './state.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
import { uiNotifications } from './uiNotifications.js';
|
||||||
|
|
||||||
|
let isLoadingFiles = false;
|
||||||
|
|
||||||
// TODO move to features/files/fileOperations.js ?
|
// TODO move to features/files/fileOperations.js ?
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} FolderInfo
|
* @typedef {Object} FolderInfo
|
||||||
@@ -48,8 +59,6 @@ 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the leaf (this is the current displayed folder)
|
* Store the leaf (this is the current displayed folder)
|
||||||
* @type {FolderInfo | null}
|
* @type {FolderInfo | null}
|
||||||
@@ -87,10 +96,7 @@ async function rebuildBreadCrumb() {
|
|||||||
} 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(
|
uiNotifications.show('error: folder not found or permission denied', 'the given folder is not available or you do not have sufficient rights');
|
||||||
'error: folder not found or permission denied',
|
|
||||||
'the given folder is not available or you do not have sufficient rights'
|
|
||||||
);
|
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
id = app.userHomeFolderId;
|
id = app.userHomeFolderId;
|
||||||
app.currentPath = id;
|
app.currentPath = id;
|
||||||
@@ -109,33 +115,31 @@ async function rebuildBreadCrumb() {
|
|||||||
* @param {boolean} [options.forceRefresh] force refresh of content
|
* @param {boolean} [options.forceRefresh] force refresh of content
|
||||||
*/
|
*/
|
||||||
async function loadFiles(options = { insertHistory: true }) {
|
async function loadFiles(options = { insertHistory: true }) {
|
||||||
const app = window.app;
|
|
||||||
|
|
||||||
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 (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;
|
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
|
||||||
const 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(`
|
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>${i18n ? i18n.t('files.loading') : 'Loading files…'}</span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
if (!app.userHomeFolderId) {
|
if (!app.userHomeFolderId) {
|
||||||
await window.resolveHomeFolder();
|
await resolveHomeFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = Math.floor(Date.now() / 1000);
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
@@ -143,9 +147,9 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
await rebuildBreadCrumb();
|
await rebuildBreadCrumb();
|
||||||
|
|
||||||
// request a breadcrumb paint
|
// request a breadcrumb paint
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
window.updateHistory(options.insertHistory || false);
|
updateHistory(options.insertHistory || false);
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
@@ -154,7 +158,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
|
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
|
||||||
app.currentPath = app.userHomeFolderId;
|
app.currentPath = app.userHomeFolderId;
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
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}`;
|
||||||
@@ -193,7 +197,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
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>`);
|
ui.showError(`<p>Could not load files</p>`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,44 +207,44 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
|
|
||||||
const listing = await response.json();
|
const listing = await response.json();
|
||||||
|
|
||||||
window.ui._items.clear();
|
ui._items.clear();
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
if (window.multiSelect) {
|
if (multiSelect) {
|
||||||
window.multiSelect.clear();
|
multiSelect.clear();
|
||||||
window.multiSelect.init(); // this will wire buttons & select-all-checkbox
|
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
||||||
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
||||||
|
|
||||||
if (folderList.length === 0 && fileList.length === 0) {
|
if (folderList.length === 0 && fileList.length === 0) {
|
||||||
window.ui.showEmptyList();
|
ui.showEmptyList();
|
||||||
} else {
|
} else {
|
||||||
window.ui.renderFolders(folderList);
|
ui.renderFolders(folderList);
|
||||||
window.ui.renderFiles(fileList);
|
ui.renderFiles(fileList);
|
||||||
|
|
||||||
// check if a file was provided
|
// check if a file was provided
|
||||||
if (window.app.viewFile) {
|
if (app.viewFile) {
|
||||||
let fileFound = null;
|
let fileFound = null;
|
||||||
|
|
||||||
// lookup for the given fle
|
// lookup for the given fle
|
||||||
for (const file of fileList) {
|
for (const file of fileList) {
|
||||||
if (file.id === window.app.viewFile) {
|
if (file.id === app.viewFile) {
|
||||||
fileFound = file;
|
fileFound = file;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileFound) {
|
if (fileFound) {
|
||||||
console.log(`file ${window.app.viewFile} found, calling viewer`);
|
console.log(`file ${app.viewFile} found, calling viewer`);
|
||||||
await window.inlineViewer.openFile(fileFound);
|
await inlineViewer.openFile(fileFound);
|
||||||
} else {
|
} else {
|
||||||
// remove file
|
// remove file
|
||||||
console.log(`file ${window.app.viewFile} not found`);
|
console.log(`file ${app.viewFile} not found`);
|
||||||
window.app.viewFile = null;
|
app.viewFile = null;
|
||||||
|
|
||||||
// correct url/history as file is not found
|
// correct url/history as file is not found
|
||||||
window.updateHistory(false);
|
updateHistory(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,10 +252,10 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
|
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading folders:', error);
|
console.error('Error loading folders:', error);
|
||||||
window.ui.showNotification('Error', 'Could not load files and folders');
|
ui.showNotification('Error', 'Could not load files and folders');
|
||||||
} finally {
|
} finally {
|
||||||
window.isLoadingFiles = false;
|
isLoadingFiles = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.loadFiles = loadFiles;
|
export { loadFiles };
|
||||||
|
|||||||
+78
-70
@@ -3,8 +3,31 @@
|
|||||||
* This file contains the core functionality, initialization and state management
|
* This file contains the core functionality, initialization and state management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const app = window.app;
|
import { formatFileSize, formatQuotaSize } from '../core/formatters.js';
|
||||||
const elements = window.appElements;
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { Modal } from '../core/modal.js';
|
||||||
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
|
import { multiSelect } from '../features/files/multiSelect.js';
|
||||||
|
import { favorites } from '../features/library/favorites.js';
|
||||||
|
import { recent } from '../features/library/recent.js';
|
||||||
|
import { fileSharing } from '../features/sharing/fileSharing.js';
|
||||||
|
import { sharedView } from '../views/shared/sharedView.js';
|
||||||
|
import { checkAuthentication } from './authSession.js';
|
||||||
|
import { loadFiles } from './filesView.js';
|
||||||
|
import {
|
||||||
|
switchToFavoritesSection,
|
||||||
|
switchToFilesSection,
|
||||||
|
switchToMusicSection,
|
||||||
|
switchToPhotosSection,
|
||||||
|
switchToRecentFilesSection,
|
||||||
|
switchToSharedSection,
|
||||||
|
switchToTrashSection
|
||||||
|
} from './navigation.js';
|
||||||
|
import { performSearch } from './searchView.js';
|
||||||
|
import { app, appElements as elements } from './state.js';
|
||||||
|
import { loadTrashItems } from './trashView.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
import { setupUserMenu } from './userMenu.js';
|
||||||
|
|
||||||
// Upload dropdown listener state (prevents accumulated listeners)
|
// Upload dropdown listener state (prevents accumulated listeners)
|
||||||
/** @type { function | null } */
|
/** @type { function | null } */
|
||||||
@@ -125,8 +148,8 @@ 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?.translateElement) {
|
if (i18n?.translateElement) {
|
||||||
window.i18n.translateElement(elements.actionsBar);
|
i18n.translateElement(elements.actionsBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'files') {
|
if (mode === 'files') {
|
||||||
@@ -159,7 +182,7 @@ function setupActionsBarDelegation() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'new-folder-btn': {
|
case 'new-folder-btn': {
|
||||||
const folderName = await window.Modal.promptNewFolder();
|
const folderName = await Modal.promptNewFolder();
|
||||||
if (folderName) {
|
if (folderName) {
|
||||||
fileOps.createFolder(folderName);
|
fileOps.createFolder(folderName);
|
||||||
}
|
}
|
||||||
@@ -173,14 +196,14 @@ function setupActionsBarDelegation() {
|
|||||||
break;
|
break;
|
||||||
case 'empty-trash-btn':
|
case 'empty-trash-btn':
|
||||||
if (await fileOps.emptyTrash()) {
|
if (await fileOps.emptyTrash()) {
|
||||||
window.loadTrashItems();
|
loadTrashItems();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'clear-recent-btn':
|
case 'clear-recent-btn':
|
||||||
if (window.recent) {
|
if (recent) {
|
||||||
window.recent.clearRecentFiles();
|
recent.clearRecentFiles();
|
||||||
window.recent.displayRecentFiles();
|
recent.displayRecentFiles();
|
||||||
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
|
ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -245,8 +268,6 @@ function deserializeHash() {
|
|||||||
* @param {boolean} insertHistory true to change url and browser's history, false to change url only
|
* @param {boolean} insertHistory true to change url and browser's history, false to change url only
|
||||||
*/
|
*/
|
||||||
function updateHistory(insertHistory) {
|
function updateHistory(insertHistory) {
|
||||||
const app = window.app;
|
|
||||||
|
|
||||||
const historyData = {
|
const historyData = {
|
||||||
section: app.currentSection,
|
section: app.currentSection,
|
||||||
id: app.currentFolder,
|
id: app.currentFolder,
|
||||||
@@ -259,8 +280,8 @@ function updateHistory(insertHistory) {
|
|||||||
historyData.id = app.currentFolder;
|
historyData.id = app.currentFolder;
|
||||||
historyUrl = historyUrl.concat('/folder/', app.currentFolderInfo.id);
|
historyUrl = historyUrl.concat('/folder/', app.currentFolderInfo.id);
|
||||||
|
|
||||||
if (window.app.viewFile) {
|
if (app.viewFile) {
|
||||||
historyUrl = historyUrl.concat('/file/', window.app.viewFile);
|
historyUrl = historyUrl.concat('/file/', app.viewFile);
|
||||||
}
|
}
|
||||||
// update title
|
// update title
|
||||||
document.title = `OxiCloud: ${app.currentFolderInfo.path}`;
|
document.title = `OxiCloud: ${app.currentFolderInfo.path}`;
|
||||||
@@ -281,7 +302,7 @@ function updateHistory(insertHistory) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function switchSectionTo(section) {
|
function switchSectionTo(section) {
|
||||||
if (window.app.currentSection === section)
|
if (app.currentSection === section)
|
||||||
// no change ...
|
// no change ...
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -324,8 +345,8 @@ function initApp() {
|
|||||||
cacheElements();
|
cacheElements();
|
||||||
|
|
||||||
// Initialize file sharing module first
|
// Initialize file sharing module first
|
||||||
if (window.fileSharing?.init) {
|
if (fileSharing?.init) {
|
||||||
window.fileSharing.init();
|
fileSharing.init();
|
||||||
} else {
|
} else {
|
||||||
console.warn('fileSharing module not fully initialized');
|
console.warn('fileSharing module not fully initialized');
|
||||||
}
|
}
|
||||||
@@ -338,35 +359,26 @@ function initApp() {
|
|||||||
// Setup event listeners
|
// Setup event listeners
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
|
||||||
// Ensure inline viewer is initialized
|
|
||||||
if (!window.inlineViewer && typeof InlineViewer !== 'undefined') {
|
|
||||||
try {
|
|
||||||
window.inlineViewer = new InlineViewer();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error initializing inline viewer:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize favorites module if available
|
// Initialize favorites module if available
|
||||||
if (window.favorites?.init) {
|
if (favorites?.init) {
|
||||||
console.log('Initializing favorites module');
|
console.log('Initializing favorites module');
|
||||||
window.favorites.init();
|
favorites.init();
|
||||||
} else {
|
} else {
|
||||||
console.warn('Favorites module not available or not initializable');
|
console.warn('Favorites module not available or not initializable');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize recent files module if available
|
// Initialize recent files module if available
|
||||||
if (window.recent?.init) {
|
if (recent?.init) {
|
||||||
console.log('Initializing recent files module');
|
console.log('Initializing recent files module');
|
||||||
window.recent.init();
|
recent.init();
|
||||||
} else {
|
} else {
|
||||||
console.warn('Recent files module not available or not initializable');
|
console.warn('Recent files module not available or not initializable');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize multi-select / batch actions
|
// Initialize multi-select / batch actions
|
||||||
if (window.multiSelect?.init) {
|
if (multiSelect?.init) {
|
||||||
console.log('Initializing multi-select module');
|
console.log('Initializing multi-select module');
|
||||||
window.multiSelect.init();
|
multiSelect.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('authenticationDone', () => {
|
window.addEventListener('authenticationDone', () => {
|
||||||
@@ -376,33 +388,33 @@ function initApp() {
|
|||||||
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;
|
app.currentPath = hashContext.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashContext.file !== null) {
|
if (hashContext.file !== null) {
|
||||||
window.app.viewFile = hashContext.file;
|
app.viewFile = hashContext.file;
|
||||||
}
|
}
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for translations to load before checking authentication
|
// Wait for translations to load before checking authentication
|
||||||
if (window.i18n?.isLoaded?.()) {
|
if (i18n?.isLoaded?.()) {
|
||||||
// Translations already loaded, proceed with authentication
|
// Translations already loaded, proceed with authentication
|
||||||
window.checkAuthentication();
|
checkAuthentication();
|
||||||
} else {
|
} else {
|
||||||
// Wait for translations to be loaded before proceeding
|
// Wait for translations to be loaded before proceeding
|
||||||
console.log('Waiting for translations to load...');
|
console.log('Waiting for translations to load...');
|
||||||
window.addEventListener('translationsLoaded', () => {
|
window.addEventListener('translationsLoaded', () => {
|
||||||
console.log('Translations loaded, proceeding with authentication');
|
console.log('Translations loaded, proceeding with authentication');
|
||||||
window.checkAuthentication();
|
checkAuthentication();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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?.isLoaded?.()) {
|
if (!i18n?.isLoaded?.()) {
|
||||||
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
||||||
window.checkAuthentication();
|
checkAuthentication();
|
||||||
}
|
}
|
||||||
}, 3000); // 3 second timeout
|
}, 3000); // 3 second timeout
|
||||||
}
|
}
|
||||||
@@ -493,14 +505,14 @@ function setupEventListeners() {
|
|||||||
const hashContext = deserializeHash();
|
const hashContext = deserializeHash();
|
||||||
switchSectionTo(hashContext.section);
|
switchSectionTo(hashContext.section);
|
||||||
if (hashContext.path) {
|
if (hashContext.path) {
|
||||||
window.app.currentPath = hashContext.path;
|
app.currentPath = hashContext.path;
|
||||||
window.loadFiles({ insertHistory: false });
|
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;
|
app.currentPath = e.state.id;
|
||||||
window.loadFiles({ insertHistory: false });
|
loadFiles({ insertHistory: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -512,19 +524,19 @@ function setupEventListeners() {
|
|||||||
const query = elements.searchInput.value.trim();
|
const query = elements.searchInput.value.trim();
|
||||||
|
|
||||||
// In shared view, filter locally
|
// In shared view, filter locally
|
||||||
if (app.isSharedView && window.sharedView) {
|
if (app.isSharedView && sharedView) {
|
||||||
window.sharedView.filterAndSortItems();
|
sharedView.filterAndSortItems();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
window.performSearch(query);
|
performSearch(query);
|
||||||
} else if (app.isSearchMode) {
|
} else if (app.isSearchMode) {
|
||||||
// If search is empty and we're in search mode, return to normal view
|
// If search is empty and we're in search mode, return to normal view
|
||||||
app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -536,7 +548,7 @@ function setupEventListeners() {
|
|||||||
|
|
||||||
if (query.length >= SEARCH_MIN_CHARS) {
|
if (query.length >= SEARCH_MIN_CHARS) {
|
||||||
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = setTimeout(() => {
|
||||||
window.performSearch(query);
|
performSearch(query);
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
} else if (query.length === 0 && app.isSearchMode) {
|
} else if (query.length === 0 && app.isSearchMode) {
|
||||||
// User cleared the search input — return to normal view
|
// User cleared the search input — return to normal view
|
||||||
@@ -544,7 +556,7 @@ function setupEventListeners() {
|
|||||||
app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -554,7 +566,7 @@ function setupEventListeners() {
|
|||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||||
const query = elements.searchInput.value.trim();
|
const query = elements.searchInput.value.trim();
|
||||||
if (query) {
|
if (query) {
|
||||||
window.performSearch(query);
|
performSearch(query);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -627,12 +639,12 @@ function setupEventListeners() {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
// Use the proper switchToFilesView function which handles all UI restoration
|
// Use the proper switchToFilesView function which handles all UI restoration
|
||||||
window.switchToFilesSection();
|
switchToFilesSection();
|
||||||
// FIXME: because fileview handles it: need to converge code
|
// FIXME: because fileview handles it: need to converge code
|
||||||
_updateHistory = false;
|
_updateHistory = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = `OxiCloud: ${window.i18n.t(itemI18nKey)}`;
|
document.title = `OxiCloud: ${i18n.t(itemI18nKey)}`;
|
||||||
|
|
||||||
if (_updateHistory) {
|
if (_updateHistory) {
|
||||||
updateHistory(true);
|
updateHistory(true);
|
||||||
@@ -649,7 +661,7 @@ function setupEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User menu
|
// User menu
|
||||||
window.setupUserMenu();
|
setupUserMenu();
|
||||||
|
|
||||||
// Global events to close context menus and deselect cards
|
// Global events to close context menus and deselect cards
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
@@ -666,18 +678,19 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose needed functions to global scope
|
// View-switching actions moved to app/navigation.js
|
||||||
window.setActionsBarMode = setActionsBarMode;
|
|
||||||
|
|
||||||
// Set up global selectFolder function for navigation
|
/**
|
||||||
window.selectFolder = (id, name) => {
|
* Navigate into a folder and refresh the file list.
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
export function selectFolder(id, name) {
|
||||||
app.breadcrumbPath.push({ id, name });
|
app.breadcrumbPath.push({ id, name });
|
||||||
app.currentPath = id;
|
app.currentPath = id;
|
||||||
ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
};
|
}
|
||||||
|
|
||||||
// View-switching actions moved to app/navigation.js
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the storage usage display with the user's actual storage usage
|
* Update the storage usage display with the user's actual storage usage
|
||||||
@@ -719,8 +732,8 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
storageInfo.removeAttribute('data-i18n');
|
storageInfo.removeAttribute('data-i18n');
|
||||||
|
|
||||||
// Use i18n if available
|
// Use i18n if available
|
||||||
if (window.i18n?.t) {
|
if (i18n?.t) {
|
||||||
storageInfo.textContent = window.i18n.t('storage.used', {
|
storageInfo.textContent = i18n.t('storage.used', {
|
||||||
percentage: usagePercentage,
|
percentage: usagePercentage,
|
||||||
used: usedFormatted,
|
used: usedFormatted,
|
||||||
total: quotaFormatted
|
total: quotaFormatted
|
||||||
@@ -733,9 +746,4 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.updateStorageUsageDisplay = updateStorageUsageDisplay;
|
export { deserializeHash, initApp, setActionsBarMode, updateHistory, updateStorageUsageDisplay };
|
||||||
|
|
||||||
// Initialize app when DOM is ready
|
|
||||||
window.initApp = initApp;
|
|
||||||
window.updateHistory = updateHistory;
|
|
||||||
window.deserializeHash = deserializeHash;
|
|
||||||
|
|||||||
+74
-66
@@ -3,6 +3,19 @@
|
|||||||
* Extracted from main.js to keep navigation concerns isolated.
|
* Extracted from main.js to keep navigation concerns isolated.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { multiSelect } from '../features/files/multiSelect.js';
|
||||||
|
import { favorites } from '../features/library/favorites.js';
|
||||||
|
import { musicView } from '../features/library/music.js';
|
||||||
|
import { photosView } from '../features/library/photos.js';
|
||||||
|
import { recent } from '../features/library/recent.js';
|
||||||
|
import { sharedView } from '../views/shared/sharedView.js';
|
||||||
|
import { loadFiles } from './filesView.js';
|
||||||
|
import { setActionsBarMode } from './main.js';
|
||||||
|
import { app, appElements } from './state.js';
|
||||||
|
import { loadTrashItems } from './trashView.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the hidden class and inline display for the grid/list containers
|
* Sync the hidden class and inline display for the grid/list containers
|
||||||
* based on the current view preference.
|
* based on the current view preference.
|
||||||
@@ -12,7 +25,7 @@ function syncViewContainers() {
|
|||||||
const gridViewBtn = document.getElementById('grid-view-btn');
|
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
const listViewBtn = document.getElementById('list-view-btn');
|
const listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
|
||||||
const isGrid = window.app.currentView === 'grid';
|
const isGrid = 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');
|
||||||
@@ -89,13 +102,6 @@ function initSidebarToggle() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose functions globally
|
|
||||||
window.sidebarToggle = {
|
|
||||||
open: openSidebar,
|
|
||||||
close: closeSidebar,
|
|
||||||
toggle: toggleSidebar
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize sidebar toggle when DOM is ready
|
// Initialize sidebar toggle when DOM is ready
|
||||||
@@ -128,17 +134,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 (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;
|
app[flag] = key === section;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.app.currentSection = section;
|
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) => {
|
appElements.navItems.forEach((item) => {
|
||||||
const itemSection = getSectionFromNavItem(item);
|
const itemSection = getSectionFromNavItem(item);
|
||||||
item.classList.toggle('active', itemSection === section);
|
item.classList.toggle('active', itemSection === section);
|
||||||
});
|
});
|
||||||
@@ -146,22 +152,22 @@ function setCurrentSection(section) {
|
|||||||
// Update page title
|
// Update page title
|
||||||
const titleKey = `nav.${section}`;
|
const titleKey = `nav.${section}`;
|
||||||
const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
||||||
window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t(titleKey) : defaultTitle;
|
appElements.pageTitle.textContent = i18n ? i18n.t(titleKey) : defaultTitle;
|
||||||
window.appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
||||||
|
|
||||||
// Hide sharedView when switching to any other section
|
// Hide sharedView when switching to any other section
|
||||||
if (section !== 'shared' && window.sharedView) {
|
if (section !== 'shared' && sharedView) {
|
||||||
window.sharedView.hide();
|
sharedView.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide photosView when switching to any other section
|
// Hide photosView when switching to any other section
|
||||||
if (section !== 'photos' && window.photosView) {
|
if (section !== 'photos' && photosView) {
|
||||||
window.photosView.hide();
|
photosView.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide musicView when switching to any other section
|
// Hide musicView when switching to any other section
|
||||||
if (section !== 'music' && window.musicView) {
|
if (section !== 'music' && musicView) {
|
||||||
window.musicView.hide();
|
musicView.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -175,27 +181,27 @@ function switchToSharedSection() {
|
|||||||
breadcrumb?.classList.add('hidden');
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// Hide actions-bar for shared view
|
// Hide actions-bar for shared view
|
||||||
window.setActionsBarMode('hidden');
|
setActionsBarMode('hidden');
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
// Hide file containers
|
// Hide file containers
|
||||||
toggleFileContainer(false);
|
toggleFileContainer(false);
|
||||||
|
|
||||||
// Show shared view
|
// Show shared view
|
||||||
if (window.sharedView) {
|
if (sharedView) {
|
||||||
window.sharedView.init();
|
sharedView.init();
|
||||||
window.sharedView.show();
|
sharedView.show();
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToFilesSection() {
|
function switchToFilesSection() {
|
||||||
if (!setCurrentSection('files')) return;
|
if (!setCurrentSection('files')) return;
|
||||||
|
|
||||||
// Set actions bar mode
|
// Set actions bar mode
|
||||||
window.setActionsBarMode('files', true);
|
setActionsBarMode('files', true);
|
||||||
|
|
||||||
// Show breadcrumb (only in Files view)
|
// Show breadcrumb (only in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
@@ -208,22 +214,22 @@ function switchToFilesSection() {
|
|||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
// Reset to home folder and update breadcrumb
|
// Reset to home folder and update breadcrumb
|
||||||
window.app.currentPath = window.app.userHomeFolderId || '';
|
app.currentPath = app.userHomeFolderId || '';
|
||||||
window.app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
|
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToFavoritesSection() {
|
function switchToFavoritesSection() {
|
||||||
if (!setCurrentSection('favorites')) return;
|
if (!setCurrentSection('favorites')) return;
|
||||||
|
|
||||||
// Set actions bar mode
|
// Set actions bar mode
|
||||||
window.setActionsBarMode('favorites');
|
setActionsBarMode('favorites');
|
||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
@@ -236,26 +242,26 @@ function switchToFavoritesSection() {
|
|||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
if (window.favorites) {
|
if (favorites) {
|
||||||
window.favorites.displayFavorites();
|
favorites.displayFavorites();
|
||||||
} else {
|
} else {
|
||||||
console.error('Favorites module not loaded or initialized');
|
console.error('Favorites module not loaded or initialized');
|
||||||
window.ui.showError(`
|
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 (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToRecentFilesSection() {
|
function switchToRecentFilesSection() {
|
||||||
if (!setCurrentSection('recent')) return;
|
if (!setCurrentSection('recent')) return;
|
||||||
|
|
||||||
// Set actions bar mode
|
// Set actions bar mode
|
||||||
window.setActionsBarMode('recent');
|
setActionsBarMode('recent');
|
||||||
|
|
||||||
// Hide breadcrumb (only shown in Files view)
|
// Hide breadcrumb (only shown in Files view)
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
@@ -268,18 +274,18 @@ function switchToRecentFilesSection() {
|
|||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
if (window.recent) {
|
if (recent) {
|
||||||
window.recent.displayRecentFiles();
|
recent.displayRecentFiles();
|
||||||
} else {
|
} else {
|
||||||
console.error('Recent files module not loaded or initialized');
|
console.error('Recent files module not loaded or initialized');
|
||||||
window.ui.showError(`
|
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 (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToPhotosSection() {
|
function switchToPhotosSection() {
|
||||||
@@ -290,19 +296,19 @@ function switchToPhotosSection() {
|
|||||||
breadcrumb?.classList.add('hidden');
|
breadcrumb?.classList.add('hidden');
|
||||||
|
|
||||||
// Hide actions-bar (photos has its own upload via selection bar)
|
// Hide actions-bar (photos has its own upload via selection bar)
|
||||||
window.setActionsBarMode('hidden');
|
setActionsBarMode('hidden');
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
// Hide file containers
|
// Hide file containers
|
||||||
toggleFileContainer(false);
|
toggleFileContainer(false);
|
||||||
|
|
||||||
// Show photos view
|
// Show photos view
|
||||||
if (window.photosView) {
|
if (photosView) {
|
||||||
window.photosView.show();
|
photosView.show();
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToTrashSection() {
|
function switchToTrashSection() {
|
||||||
@@ -319,15 +325,15 @@ function switchToTrashSection() {
|
|||||||
setActionsBarMode('trash');
|
setActionsBarMode('trash');
|
||||||
|
|
||||||
//reset files view + remove any error
|
//reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
//ensure buttons match the current view
|
//ensure buttons match the current view
|
||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
|
|
||||||
// Load trash items
|
// Load trash items
|
||||||
window.loadTrashItems();
|
loadTrashItems();
|
||||||
|
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToMusicSection() {
|
function switchToMusicSection() {
|
||||||
@@ -341,27 +347,29 @@ function switchToMusicSection() {
|
|||||||
toggleFileContainer(false);
|
toggleFileContainer(false);
|
||||||
|
|
||||||
// Hide actions-bar
|
// Hide actions-bar
|
||||||
window.setActionsBarMode('hidden');
|
setActionsBarMode('hidden');
|
||||||
|
|
||||||
// Reset files view + remove any error
|
// Reset files view + remove any error
|
||||||
window.ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
|
|
||||||
// Hide list header (created by resetFilesList)
|
// Hide list header (created by resetFilesList)
|
||||||
const listHeader = document.querySelector('.list-header');
|
const listHeader = document.querySelector('.list-header');
|
||||||
listHeader?.classList.add('hidden');
|
listHeader?.classList.add('hidden');
|
||||||
|
|
||||||
// Show music view
|
// Show music view
|
||||||
if (window.musicView) {
|
if (musicView) {
|
||||||
window.musicView.show();
|
musicView.show();
|
||||||
}
|
}
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.switchToFilesSection = switchToFilesSection;
|
export {
|
||||||
window.switchToSharedSection = switchToSharedSection;
|
switchToFavoritesSection,
|
||||||
window.switchToFavoritesSection = switchToFavoritesSection;
|
switchToFilesSection,
|
||||||
window.switchToRecentFilesSection = switchToRecentFilesSection;
|
switchToMusicSection,
|
||||||
window.switchToPhotosSection = switchToPhotosSection;
|
switchToPhotosSection,
|
||||||
window.switchToTrashSection = switchToTrashSection;
|
switchToRecentFilesSection,
|
||||||
window.switchToMusicSection = switchToMusicSection;
|
switchToSharedSection,
|
||||||
window.syncViewContainers = syncViewContainers;
|
switchToTrashSection,
|
||||||
|
syncViewContainers
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,16 +2,23 @@
|
|||||||
* Search view orchestration logic
|
* Search view orchestration logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function performSearch(query, sortBy) {
|
import { search } from '../features/files/search.js';
|
||||||
const app = window.app;
|
import { resolveHomeFolder } from './authSession.js';
|
||||||
|
import { app } from './state.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} query
|
||||||
|
* @param {string} [sortBy]
|
||||||
|
*/
|
||||||
|
async function performSearch(query, sortBy) {
|
||||||
console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`);
|
console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
app.isSearchMode = true;
|
app.isSearchMode = true;
|
||||||
window.ui.updateBreadcrumb(`Search: "${query}"`);
|
ui.updateBreadcrumb(`Search: "${query}"`);
|
||||||
|
|
||||||
window.ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -22,7 +29,7 @@ async function performSearch(query, sortBy) {
|
|||||||
if (!app.isTrashView) {
|
if (!app.isTrashView) {
|
||||||
// Ensure we have a valid folder_id before searching
|
// Ensure we have a valid folder_id before searching
|
||||||
if (!app.currentPath || app.currentPath === '') {
|
if (!app.currentPath || app.currentPath === '') {
|
||||||
await window.resolveHomeFolder();
|
await resolveHomeFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set folder_id if we have a valid value
|
// Only set folder_id if we have a valid value
|
||||||
@@ -32,11 +39,11 @@ async function performSearch(query, sortBy) {
|
|||||||
// If still no valid folder_id, search will be global (without folder_id)
|
// If still no valid folder_id, search will be global (without folder_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResults = await window.search.searchFiles(query, options);
|
const searchResults = await search.searchFiles(query, options);
|
||||||
window.search.displaySearchResults(searchResults);
|
search.displaySearchResults(searchResults);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', error);
|
console.error('Search error:', error);
|
||||||
window.ui.showNotification('Error', 'Error performing search');
|
ui.showNotification('Error', 'Error performing search');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,4 +54,4 @@ document.addEventListener('search-resort', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.performSearch = performSearch;
|
export { performSearch };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Centralized mutable state for app and cached DOM references.
|
* Centralized mutable state for app and cached DOM references.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
window.app = {
|
export const app = {
|
||||||
currentView: 'grid',
|
currentView: 'grid',
|
||||||
currentPath: '',
|
currentPath: '',
|
||||||
currentFolder: null,
|
currentFolder: null,
|
||||||
@@ -29,4 +29,4 @@ window.app = {
|
|||||||
viewFile: null // current file in inline view
|
viewFile: null // current file in inline view
|
||||||
};
|
};
|
||||||
|
|
||||||
window.appElements = {};
|
export const appElements = {};
|
||||||
|
|||||||
+29
-28
@@ -2,13 +2,20 @@
|
|||||||
* Trash view loading and rendering logic
|
* Trash view loading and rendering logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { escapeHtml, formatDateTime } from '../core/formatters.js';
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
|
import { multiSelect } from '../features/files/multiSelect.js';
|
||||||
|
import { appElements } from './state.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
async function loadTrashItems() {
|
async function loadTrashItems() {
|
||||||
const elements = window.appElements;
|
const elements = appElements;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
const _tt = window.i18n?.t ? window.i18n.t : (k) => k.split('.').pop();
|
const _tt = i18n?.t ? 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>
|
||||||
@@ -19,14 +26,14 @@ async function loadTrashItems() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
|
|
||||||
const trashItems = await window.fileOps.getTrashItems();
|
const trashItems = await fileOps.getTrashItems();
|
||||||
|
|
||||||
if (trashItems.length === 0) {
|
if (trashItems.length === 0) {
|
||||||
window.ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-trash empty-state-icon"></i>
|
<i class="fas fa-trash empty-state-icon"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
<p>${i18n ? i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
||||||
`);
|
`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -36,33 +43,27 @@ async function loadTrashItems() {
|
|||||||
});
|
});
|
||||||
} 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');
|
ui.showNotification('Error', 'Error loading trash items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTrashItemToView(item) {
|
function addTrashItemToView(item) {
|
||||||
const elements = window.appElements;
|
const elements = appElements;
|
||||||
const isFile = item.item_type === 'file';
|
const isFile = item.item_type === 'file';
|
||||||
|
|
||||||
const formattedDate = window.formatDateTime(item.trashed_at);
|
const formattedDate = formatDateTime(item.trashed_at);
|
||||||
|
|
||||||
let iconClass;
|
let iconClass;
|
||||||
let typeLabel;
|
let typeLabel;
|
||||||
let iconSpecialClass = '';
|
let iconSpecialClass = '';
|
||||||
if (!isFile) {
|
if (!isFile) {
|
||||||
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 = i18n ? i18n.t('files.file_types.folder') : 'Folder';
|
||||||
} else {
|
} else {
|
||||||
iconClass = item.icon_class || (window.ui?.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
|
iconClass = item.icon_class || (ui?.getIconClass ? ui.getIconClass(item.name) : 'fas fa-file');
|
||||||
iconSpecialClass = window.ui?.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : '';
|
iconSpecialClass = ui?.getIconSpecialClass ? ui.getIconSpecialClass(item.name) : '';
|
||||||
const cat = item.category || '';
|
const cat = item.category || '';
|
||||||
typeLabel = cat
|
typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
|
||||||
? window.i18n
|
|
||||||
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
|
||||||
: cat
|
|
||||||
: window.i18n
|
|
||||||
? window.i18n.t('files.file_types.document')
|
|
||||||
: 'Document';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFolder = !isFile;
|
const isFolder = !isFile;
|
||||||
@@ -85,10 +86,10 @@ function addTrashItemToView(item) {
|
|||||||
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
||||||
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
||||||
<div class="actions-cell">
|
<div class="actions-cell">
|
||||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
<button class="btn-restore" title="${i18n ? i18n.t('trash.restore') : 'Restore'}">
|
||||||
<i class="fas fa-undo"></i>
|
<i class="fas fa-undo"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
<button class="btn-delete" title="${i18n ? i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,19 +97,19 @@ function addTrashItemToView(item) {
|
|||||||
|
|
||||||
listElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
|
listElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (await window.fileOps.restoreFromTrash(item.id)) {
|
if (await fileOps.restoreFromTrash(item.id)) {
|
||||||
window.loadTrashItems();
|
loadTrashItems();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
listElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
|
listElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (await window.fileOps.deletePermanently(item.id)) {
|
if (await fileOps.deletePermanently(item.id)) {
|
||||||
window.loadTrashItems();
|
loadTrashItems();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.filesList.appendChild(listElement);
|
elements.filesList.appendChild(listElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.loadTrashItems = loadTrashItems;
|
export { loadTrashItems };
|
||||||
|
|||||||
+116
-111
@@ -5,6 +5,26 @@
|
|||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { escapeHtml, formatDateTime, formatFileSize } from '../core/formatters.js';
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { OxiIcons, replaceIconsInElement } from '../core/icons.js';
|
||||||
|
import { contextMenus } from '../features/files/contextMenus.js';
|
||||||
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
|
import { inlineViewer } from '../features/files/inlineViewer.js';
|
||||||
|
import { multiSelect } from '../features/files/multiSelect.js';
|
||||||
|
import { wopiEditor } from '../features/files/wopiEditor.js';
|
||||||
|
import { favorites } from '../features/library/favorites.js';
|
||||||
|
import { recent } from '../features/library/recent.js';
|
||||||
|
import { fileSharing } from '../features/sharing/fileSharing.js';
|
||||||
|
import { loadFiles } from './filesView.js';
|
||||||
|
import { updateHistory } from './main.js';
|
||||||
|
import { syncViewContainers } from './navigation.js';
|
||||||
|
import { app } from './state.js';
|
||||||
|
import { uiFileTypes } from './uiFileTypes.js';
|
||||||
|
import { uiNotifications } from './uiNotifications.js';
|
||||||
|
|
||||||
|
let __rubberBandJustFinished = false;
|
||||||
|
|
||||||
// UI Module
|
// UI Module
|
||||||
const ui = {
|
const ui = {
|
||||||
/** @type {HTMLDListElement | null} */
|
/** @type {HTMLDListElement | null} */
|
||||||
@@ -297,13 +317,13 @@ const ui = {
|
|||||||
document.body.appendChild(playlistDialog);
|
document.body.appendChild(playlistDialog);
|
||||||
|
|
||||||
document.getElementById('playlist-cancel-btn').addEventListener('click', () => {
|
document.getElementById('playlist-cancel-btn').addEventListener('click', () => {
|
||||||
if (window.contextMenus) window.contextMenus.closePlaylistDialog();
|
if (contextMenus) contextMenus.closePlaylistDialog();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign events to menu items
|
// Assign events to menu items
|
||||||
if (window.contextMenus) {
|
if (contextMenus) {
|
||||||
window.contextMenus.assignMenuEvents();
|
contextMenus.assignMenuEvents();
|
||||||
} else {
|
} else {
|
||||||
console.warn('contextMenus module not loaded');
|
console.warn('contextMenus module not loaded');
|
||||||
}
|
}
|
||||||
@@ -478,10 +498,10 @@ const ui = {
|
|||||||
switchToGridView() {
|
switchToGridView() {
|
||||||
this._hydrateViewIfNeeded();
|
this._hydrateViewIfNeeded();
|
||||||
|
|
||||||
window.app.currentView = 'grid';
|
app.currentView = 'grid';
|
||||||
localStorage.setItem('oxicloud-view', 'grid');
|
localStorage.setItem('oxicloud-view', 'grid');
|
||||||
|
|
||||||
window.syncViewContainers();
|
syncViewContainers();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -490,10 +510,10 @@ const ui = {
|
|||||||
switchToListView() {
|
switchToListView() {
|
||||||
this._hydrateViewIfNeeded();
|
this._hydrateViewIfNeeded();
|
||||||
|
|
||||||
window.app.currentView = 'list';
|
app.currentView = 'list';
|
||||||
localStorage.setItem('oxicloud-view', 'list');
|
localStorage.setItem('oxicloud-view', 'list');
|
||||||
|
|
||||||
window.syncViewContainers();
|
syncViewContainers();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -504,12 +524,12 @@ const ui = {
|
|||||||
updateBreadcrumb() {
|
updateBreadcrumb() {
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
const path = 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?.t) return defaultValue;
|
if (!i18n?.t) return defaultValue;
|
||||||
return window.i18n.t(key);
|
return i18n.t(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- Home icon (always present, clickable to go to root) --
|
// -- Home icon (always present, clickable to go to root) --
|
||||||
@@ -519,24 +539,24 @@ const ui = {
|
|||||||
homeIcon.title = getTranslatedText('breadcrumb.home', 'Home');
|
homeIcon.title = getTranslatedText('breadcrumb.home', 'Home');
|
||||||
|
|
||||||
// Home is always clickable if we have a home folder
|
// Home is always clickable if we have a home folder
|
||||||
if (window.app.userHomeFolderId) {
|
if (app.userHomeFolderId) {
|
||||||
homeIcon.classList.add('breadcrumb-link');
|
homeIcon.classList.add('breadcrumb-link');
|
||||||
homeIcon.addEventListener('click', () => {
|
homeIcon.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
window.app.currentPath = window.app.userHomeFolderId;
|
app.currentPath = app.userHomeFolderId;
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
breadcrumb.appendChild(homeIcon);
|
breadcrumb.appendChild(homeIcon);
|
||||||
|
|
||||||
// -- Root/Home folder name (if available) is always the first element of the breadcrumb --
|
// -- Root/Home folder name (if available) is always the first element of the breadcrumb --
|
||||||
// TODO clarify the difference between homeIcon & this first element
|
// TODO clarify the difference between homeIcon & this first element
|
||||||
if (window.app.userHomeFolderName) {
|
if (app.userHomeFolderName) {
|
||||||
if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) {
|
if (path.length === 0 || path[0].id !== app.userHomeFolderId) {
|
||||||
path.unshift({
|
path.unshift({
|
||||||
name: window.app.userHomeFolderName,
|
name: app.userHomeFolderName,
|
||||||
id: window.app.userHomeFolderId
|
id: app.userHomeFolderId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,10 +581,10 @@ const ui = {
|
|||||||
// Intermediate segment: clickable – truncate path to this level
|
// Intermediate segment: clickable – truncate path to this level
|
||||||
item.classList.add('breadcrumb-link');
|
item.classList.add('breadcrumb-link');
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = path.slice(0, index + 1);
|
app.breadcrumbPath = path.slice(0, index + 1);
|
||||||
window.app.currentPath = segment.id;
|
app.currentPath = segment.id;
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
// can drag files on this folder
|
// can drag files on this folder
|
||||||
@@ -611,7 +631,7 @@ const ui = {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isViewableFile(file) {
|
isViewableFile(file) {
|
||||||
return window.uiFileTypes.isViewableFile(file);
|
return uiFileTypes.isViewableFile(file);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -620,7 +640,7 @@ const ui = {
|
|||||||
* (e.g. trash items).
|
* (e.g. trash items).
|
||||||
*/
|
*/
|
||||||
getIconClass(fileName) {
|
getIconClass(fileName) {
|
||||||
return window.uiFileTypes.getIconClass(fileName);
|
return uiFileTypes.getIconClass(fileName);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -628,7 +648,7 @@ const ui = {
|
|||||||
* Used as fallback when the backend DTO doesn't include icon_special_class.
|
* Used as fallback when the backend DTO doesn't include icon_special_class.
|
||||||
*/
|
*/
|
||||||
getIconSpecialClass(fileName) {
|
getIconSpecialClass(fileName) {
|
||||||
return window.uiFileTypes.getIconSpecialClass(fileName);
|
return uiFileTypes.getIconSpecialClass(fileName);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -637,7 +657,7 @@ const ui = {
|
|||||||
* @param {string} message - Notification message
|
* @param {string} message - Notification message
|
||||||
*/
|
*/
|
||||||
showNotification(title, message) {
|
showNotification(title, message) {
|
||||||
window.uiNotifications.show(title, message);
|
uiNotifications.show(title, message);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -647,7 +667,7 @@ const ui = {
|
|||||||
const menu = document.getElementById('folder-context-menu');
|
const menu = document.getElementById('folder-context-menu');
|
||||||
if (menu) {
|
if (menu) {
|
||||||
menu.style.display = 'none';
|
menu.style.display = 'none';
|
||||||
window.app.contextMenuTargetFolder = null;
|
app.contextMenuTargetFolder = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -658,7 +678,7 @@ const ui = {
|
|||||||
const menu = document.getElementById('file-context-menu');
|
const menu = document.getElementById('file-context-menu');
|
||||||
if (menu) {
|
if (menu) {
|
||||||
menu.style.display = 'none';
|
menu.style.display = 'none';
|
||||||
window.app.contextMenuTargetFile = null;
|
app.contextMenuTargetFile = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -679,8 +699,8 @@ const ui = {
|
|||||||
_delegationReady: false,
|
_delegationReady: false,
|
||||||
|
|
||||||
_getActiveView() {
|
_getActiveView() {
|
||||||
if (window.app && window.app.currentView === 'list') return 'list';
|
if (app && app.currentView === 'list') return 'list';
|
||||||
if (window.app && window.app.currentView === 'grid') return 'grid';
|
if (app && app.currentView === 'grid') return 'grid';
|
||||||
|
|
||||||
const stored = localStorage.getItem('oxicloud-view');
|
const stored = localStorage.getItem('oxicloud-view');
|
||||||
return stored === 'list' ? 'list' : 'grid';
|
return stored === 'list' ? 'list' : 'grid';
|
||||||
@@ -737,9 +757,9 @@ 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) {
|
||||||
const selection = window.multiSelect.getSelection(targetFolderId);
|
const selection = multiSelect.getSelection(targetFolderId);
|
||||||
|
|
||||||
window.multiSelect.clear();
|
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)
|
||||||
@@ -770,20 +790,20 @@ 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 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 fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
|
||||||
// redraw directory
|
// redraw directory
|
||||||
if (result.success > 0) window.loadFiles();
|
if (result.success > 0) loadFiles();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`drag and drop: action ${action} unknown`);
|
console.error(`drag and drop: action ${action} unknown`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.multiSelect.showBatchResult(action, result);
|
multiSelect.showBatchResult(action, result);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -837,7 +857,7 @@ const ui = {
|
|||||||
|
|
||||||
const openFile = async (file) => {
|
const openFile = async (file) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (window.recent) {
|
if (recent) {
|
||||||
document.dispatchEvent(new CustomEvent('file-accessed', { detail: { file } }));
|
document.dispatchEvent(new CustomEvent('file-accessed', { detail: { file } }));
|
||||||
}
|
}
|
||||||
// WOPI editor intercept: open Office documents in the WOPI editor
|
// WOPI editor intercept: open Office documents in the WOPI editor
|
||||||
@@ -846,8 +866,8 @@ const ui = {
|
|||||||
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?.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 && wopiEditor && (await wopiEditor.canEdit(file.name))) {
|
||||||
await window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
await wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -855,38 +875,38 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isViewableFile(file) || isImage) {
|
if (this.isViewableFile(file) || isImage) {
|
||||||
if (window.inlineViewer) {
|
if (inlineViewer) {
|
||||||
window.inlineViewer.openFile(file);
|
inlineViewer.openFile(file);
|
||||||
// update history
|
// update history
|
||||||
window.app.viewFile = file.id;
|
app.viewFile = file.id;
|
||||||
window.updateHistory(false);
|
updateHistory(false);
|
||||||
} else {
|
} else {
|
||||||
window.fileOps.downloadFile(file.id, file.name);
|
fileOps.downloadFile(file.id, file.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.fileOps.downloadFile(file.id, file.name);
|
fileOps.downloadFile(file.id, file.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateFolder = (card) => {
|
const navigateFolder = (card) => {
|
||||||
const folderId = card.dataset.folderId;
|
const folderId = card.dataset.folderId;
|
||||||
const folderName = card.dataset.folderName;
|
const folderName = card.dataset.folderName;
|
||||||
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
app.breadcrumbPath.push({ id: folderId, name: folderName });
|
||||||
window.app.currentPath = folderId;
|
app.currentPath = folderId;
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setContextTarget = (card, info) => {
|
const setContextTarget = (card, info) => {
|
||||||
if (info.type === 'folder') {
|
if (info.type === 'folder') {
|
||||||
window.app.contextMenuTargetFolder = {
|
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 {
|
||||||
const fileData = info.data || self._items.get(info.id);
|
const fileData = info.data || self._items.get(info.id);
|
||||||
window.app.contextMenuTargetFile = {
|
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 || '',
|
||||||
@@ -932,8 +952,8 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shiftkey is used to complete selection
|
// shiftkey is used to complete selection
|
||||||
if (e.shiftKey && window.multiSelect) {
|
if (e.shiftKey && multiSelect) {
|
||||||
window.multiSelect.handleToggleItem(card, e);
|
multiSelect.handleToggleItem(card, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,14 +982,14 @@ const ui = {
|
|||||||
setContextTarget(card, info);
|
setContextTarget(card, info);
|
||||||
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||||
const menu = document.getElementById(menuId);
|
const menu = document.getElementById(menuId);
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') {
|
||||||
window.contextMenus.syncFavoriteOptionLabels();
|
contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (contextMenus && typeof contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') {
|
if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') {
|
||||||
window.contextMenus.syncAddToPlaylistOption();
|
contextMenus.syncAddToPlaylistOption();
|
||||||
}
|
}
|
||||||
menu.style.left = `${e.pageX}px`;
|
menu.style.left = `${e.pageX}px`;
|
||||||
menu.style.top = `${e.pageY}px`;
|
menu.style.top = `${e.pageY}px`;
|
||||||
@@ -1105,7 +1125,7 @@ const ui = {
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!window.favorites) return;
|
if (!favorites) return;
|
||||||
|
|
||||||
const itemId = star.dataset.itemId;
|
const itemId = star.dataset.itemId;
|
||||||
const itemType = star.dataset.itemType;
|
const itemType = star.dataset.itemType;
|
||||||
@@ -1115,15 +1135,15 @@ const ui = {
|
|||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
this.setFavoriteVisualState(itemId, itemType, false);
|
this.setFavoriteVisualState(itemId, itemType, false);
|
||||||
window.favorites.removeFromFavorites(itemId, itemType);
|
favorites.removeFromFavorites(itemId, itemType);
|
||||||
} else {
|
} else {
|
||||||
this.setFavoriteVisualState(itemId, itemType, true);
|
this.setFavoriteVisualState(itemId, itemType, true);
|
||||||
window.favorites.addToFavorites(itemId, itemName, itemType);
|
favorites.addToFavorites(itemId, itemName, itemType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep context-menu label in sync if available
|
// Keep context-menu label in sync if available
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') {
|
||||||
window.contextMenus.syncFavoriteOptionLabels();
|
contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1142,8 +1162,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?.star;
|
const filledPath = OxiIcons?.star;
|
||||||
const outlinePath = window.OxiIcons?.['star-outline'];
|
const outlinePath = 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');
|
||||||
@@ -1168,9 +1188,7 @@ const ui = {
|
|||||||
inlineStar = document.createElement('i');
|
inlineStar = document.createElement('i');
|
||||||
inlineStar.className = 'fas fa-star favorite-star-inline';
|
inlineStar.className = 'fas fa-star favorite-star-inline';
|
||||||
nameCell.appendChild(inlineStar);
|
nameCell.appendChild(inlineStar);
|
||||||
if (window.OxiIcons && typeof window.OxiIcons.replaceIconsInElement === 'function') {
|
replaceIconsInElement(nameCell);
|
||||||
window.OxiIcons.replaceIconsInElement(nameCell);
|
|
||||||
}
|
|
||||||
} else if (!isFavorite && inlineStar) {
|
} else if (!isFavorite && inlineStar) {
|
||||||
inlineStar.remove();
|
inlineStar.remove();
|
||||||
}
|
}
|
||||||
@@ -1190,8 +1208,8 @@ const ui = {
|
|||||||
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?.isFavorite(folder.id, 'folder');
|
const isFav = favorites?.isFavorite(folder.id, 'folder');
|
||||||
const formattedDate = window.formatDateTime(folder.modified_at);
|
const formattedDate = formatDateTime(folder.modified_at);
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
||||||
@@ -1202,7 +1220,7 @@ const ui = {
|
|||||||
<span>${escapeHtml(folder.name)}</span>
|
<span>${escapeHtml(folder.name)}</span>
|
||||||
${isFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
|
${isFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
|
<div class="type-cell">${i18n ? i18n.t('files.file_types.folder') : 'Folder'}</div>
|
||||||
<div class="size-cell">--</div>
|
<div class="size-cell">--</div>
|
||||||
<div class="date-cell">${formattedDate}</div>
|
<div class="date-cell">${formattedDate}</div>
|
||||||
<div class="action-cell">
|
<div class="action-cell">
|
||||||
@@ -1213,7 +1231,7 @@ const ui = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.app.currentPath !== '') {
|
if (app.currentPath !== '') {
|
||||||
el.setAttribute('draggable', 'true');
|
el.setAttribute('draggable', 'true');
|
||||||
}
|
}
|
||||||
this._bindStarClick(el);
|
this._bindStarClick(el);
|
||||||
@@ -1225,16 +1243,10 @@ const ui = {
|
|||||||
const iconClass = file.icon_class || this.getIconClass(file.name);
|
const iconClass = file.icon_class || this.getIconClass(file.name);
|
||||||
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 ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
|
||||||
? window.i18n
|
const fileSize = file.size_formatted || formatFileSize(file.size);
|
||||||
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
const formattedDate = formatDateTime(file.modified_at);
|
||||||
: cat
|
const isFav = favorites?.isFavorite(file.id, 'file');
|
||||||
: window.i18n
|
|
||||||
? window.i18n.t('files.file_types.document')
|
|
||||||
: 'Document';
|
|
||||||
const fileSize = file.size_formatted || window.formatFileSize(file.size);
|
|
||||||
const formattedDate = window.formatDateTime(file.modified_at);
|
|
||||||
const isFav = window.favorites?.isFavorite(file.id, 'file');
|
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'file-item';
|
el.className = 'file-item';
|
||||||
@@ -1293,7 +1305,7 @@ const ui = {
|
|||||||
<div></div><!-- actions -->
|
<div></div><!-- actions -->
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (window.i18n?.translateElement) window.i18n.translateElement(filesList);
|
if (i18n?.translateElement) i18n.translateElement(filesList);
|
||||||
|
|
||||||
filesList.classList.remove('hidden');
|
filesList.classList.remove('hidden');
|
||||||
filesContainerError?.classList.add('hidden');
|
filesContainerError?.classList.add('hidden');
|
||||||
@@ -1317,7 +1329,7 @@ const ui = {
|
|||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesContainerError) filesContainerError.innerHTML = content;
|
if (filesContainerError) filesContainerError.innerHTML = content;
|
||||||
|
|
||||||
if (window.i18n?.translateElement) window.i18n.translateElement(filesContainerError);
|
if (i18n?.translateElement) i18n.translateElement(filesContainerError);
|
||||||
|
|
||||||
filesContainerError?.classList.remove('hidden');
|
filesContainerError?.classList.remove('hidden');
|
||||||
filesList?.classList.add('hidden');
|
filesList?.classList.add('hidden');
|
||||||
@@ -1403,8 +1415,8 @@ const ui = {
|
|||||||
* Routes through the multiSelect module so batch actions know about selected items.
|
* Routes through the multiSelect module so batch actions know about selected items.
|
||||||
*/
|
*/
|
||||||
function toggleCardSelection(card, event) {
|
function toggleCardSelection(card, event) {
|
||||||
if (window.multiSelect) {
|
if (multiSelect) {
|
||||||
window.multiSelect.handleToggleItem(card, event);
|
multiSelect.handleToggleItem(card, event);
|
||||||
} else {
|
} else {
|
||||||
card.classList.toggle('selected');
|
card.classList.toggle('selected');
|
||||||
}
|
}
|
||||||
@@ -1435,14 +1447,14 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
|||||||
top = rect.top - 4 + window.scrollY; // flip above if no room
|
top = rect.top - 4 + window.scrollY; // flip above if no room
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') {
|
||||||
window.contextMenus.syncFavoriteOptionLabels();
|
contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (contextMenus && typeof contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') {
|
if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') {
|
||||||
window.contextMenus.syncAddToPlaylistOption();
|
contextMenus.syncAddToPlaylistOption();
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.style.left = `${left}px`;
|
menu.style.left = `${left}px`;
|
||||||
@@ -1532,16 +1544,16 @@ function initRubberBandSelection() {
|
|||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
|
|
||||||
// Sync with multiSelect module
|
// Sync with multiSelect module
|
||||||
if (window.multiSelect) {
|
if (multiSelect) {
|
||||||
const info = window.multiSelect._extractInfo(card);
|
const info = multiSelect._extractInfo(card);
|
||||||
if (info) window.multiSelect.select(info.id, info.name, info.type, info.parentId);
|
if (info) multiSelect.select(info.id, info.name, info.type, info.parentId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
// Deselect from multiSelect module
|
// Deselect from multiSelect module
|
||||||
if (window.multiSelect) {
|
if (multiSelect) {
|
||||||
const info = window.multiSelect._extractInfo(card);
|
const info = multiSelect._extractInfo(card);
|
||||||
if (info) window.multiSelect.deselect(info.id);
|
if (info) multiSelect.deselect(info.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1553,13 +1565,13 @@ function initRubberBandSelection() {
|
|||||||
const hadSelection = selRect.style.display === 'block';
|
const hadSelection = selRect.style.display === 'block';
|
||||||
selRect.style.display = 'none';
|
selRect.style.display = 'none';
|
||||||
// Update the batch bar after rubber band selection completes
|
// Update the batch bar after rubber band selection completes
|
||||||
if (window.multiSelect) window.multiSelect._syncUI();
|
if (multiSelect) multiSelect._syncUI();
|
||||||
// Suppress the click event that follows mouseup so the global
|
// Suppress the click event that follows mouseup so the global
|
||||||
// deselect handler doesn't immediately clear the selection.
|
// deselect handler doesn't immediately clear the selection.
|
||||||
if (hadSelection) {
|
if (hadSelection) {
|
||||||
window.__rubberBandJustFinished = true;
|
__rubberBandJustFinished = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
window.__rubberBandJustFinished = false;
|
__rubberBandJustFinished = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1572,11 +1584,6 @@ if (document.readyState === 'loading') {
|
|||||||
initRubberBandSelection();
|
initRubberBandSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose helpers globally
|
|
||||||
window.toggleCardSelection = toggleCardSelection;
|
|
||||||
window.showContextMenuAtElement = showContextMenuAtElement;
|
|
||||||
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
|
||||||
@@ -1588,9 +1595,9 @@ window.initRubberBandSelection = initRubberBandSelection;
|
|||||||
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
||||||
*/
|
*/
|
||||||
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
||||||
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete');
|
const ct = confirmText || (i18n ? i18n.t('actions.delete') : 'Delete');
|
||||||
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel');
|
const cc = cancelText || (i18n ? i18n.t('actions.cancel') : 'Cancel');
|
||||||
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action');
|
const t = title || (i18n ? i18n.t('dialogs.confirm_title') : 'Confirm action');
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Remove any previous confirm dialog
|
// Remove any previous confirm dialog
|
||||||
@@ -1633,7 +1640,5 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.showConfirmDialog = showConfirmDialog;
|
|
||||||
|
|
||||||
// Expose UI module globally
|
export { initRubberBandSelection, showConfirmDialog, showContextMenuAtElement, toggleCardSelection, ui };
|
||||||
window.ui = ui;
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* Isolated icon and preview classification helpers used by ui.js.
|
* Isolated icon and preview classification helpers used by ui.js.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isTextViewable } from '../core/formatters.js';
|
||||||
|
|
||||||
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) {
|
||||||
@@ -11,7 +13,7 @@ const uiFileTypes = {
|
|||||||
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;
|
||||||
if (file.mime_type.startsWith('video/')) return true;
|
if (file.mime_type.startsWith('video/')) return true;
|
||||||
return window.isTextViewable ? window.isTextViewable(file.mime_type) : false;
|
return isTextViewable(file.mime_type);
|
||||||
},
|
},
|
||||||
|
|
||||||
getIconClass(fileName) {
|
getIconClass(fileName) {
|
||||||
@@ -170,4 +172,4 @@ const uiFileTypes = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.uiFileTypes = uiFileTypes;
|
export { uiFileTypes };
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
* Isolates notification rendering policy from ui.js.
|
* Isolates notification rendering policy from ui.js.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { notifications } from '../core/notifications.js';
|
||||||
|
|
||||||
const uiNotifications = {
|
const uiNotifications = {
|
||||||
show(title, message) {
|
show(title, message) {
|
||||||
if (window.notifications && typeof window.notifications.addNotification === 'function') {
|
if (notifications && typeof notifications.addNotification === 'function') {
|
||||||
const normalizedTitle = String(title || '').toLowerCase();
|
const normalizedTitle = String(title || '').toLowerCase();
|
||||||
let icon = 'fa-info-circle';
|
let icon = 'fa-info-circle';
|
||||||
let iconClass = 'upload';
|
let iconClass = 'upload';
|
||||||
@@ -27,7 +29,7 @@ const uiNotifications = {
|
|||||||
iconClass = 'success';
|
iconClass = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon,
|
icon,
|
||||||
iconClass,
|
iconClass,
|
||||||
title: title || '',
|
title: title || '',
|
||||||
@@ -58,4 +60,4 @@ const uiNotifications = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.uiNotifications = uiNotifications;
|
export { uiNotifications };
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
* User menu, profile modal and logout logic
|
* User menu, profile modal and logout logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getCsrfHeaders } from '../core/csrf.js';
|
||||||
|
import { formatFileSize, formatQuotaSize } from '../core/formatters.js';
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
function setupUserMenu() {
|
function setupUserMenu() {
|
||||||
const wrapper = document.getElementById('user-menu-wrapper');
|
const wrapper = document.getElementById('user-menu-wrapper');
|
||||||
const avatarBtn = document.getElementById('user-avatar-btn');
|
const avatarBtn = document.getElementById('user-avatar-btn');
|
||||||
@@ -102,7 +107,7 @@ function setupUserMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled');
|
ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +178,8 @@ function updateUserMenuData() {
|
|||||||
|
|
||||||
if (storageFill) storageFill.style.width = `${percentage}%`;
|
if (storageFill) storageFill.style.width = `${percentage}%`;
|
||||||
if (storageText) {
|
if (storageText) {
|
||||||
const used = window.formatFileSize(usedBytes);
|
const used = formatFileSize(usedBytes);
|
||||||
const total = window.formatQuotaSize(quotaBytes);
|
const total = formatQuotaSize(quotaBytes);
|
||||||
storageText.textContent = `${quotaBytes > 0 ? `${percentage}% · ` : ''}${used} / ${total}`;
|
storageText.textContent = `${quotaBytes > 0 ? `${percentage}% · ` : ''}${used} / ${total}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,7 @@ function showUserProfileModal() {
|
|||||||
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?.t ? window.i18n.t(key) || fallback : fallback);
|
const t = (key, fallback) => (i18n?.t ? 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();
|
||||||
@@ -229,7 +234,7 @@ function showUserProfileModal() {
|
|||||||
<div class="about-modal-bar-bg">
|
<div class="about-modal-bar-bg">
|
||||||
<div class="about-modal-bar-fill" id="about-bar-fill"></div>
|
<div class="about-modal-bar-fill" id="about-bar-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-bar-text">${percentage}% · ${window.formatFileSize(usedBytes)} / ${window.formatQuotaSize(quotaBytes)}</div>
|
<div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-footer">
|
<div class="about-modal-footer">
|
||||||
<button id="profile-modal-close" class="about-modal-close-btn">${t('actions.close', 'Close')}</button>
|
<button id="profile-modal-close" class="about-modal-close-btn">${t('actions.close', 'Close')}</button>
|
||||||
@@ -283,6 +288,4 @@ async function logout() {
|
|||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setupUserMenu = setupUserMenu;
|
export { logout, setupUserMenu, showUserProfileModal };
|
||||||
window.showUserProfileModal = showUserProfileModal;
|
|
||||||
window.logout = logout;
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@
|
|||||||
* HttpOnly cookies.
|
* HttpOnly cookies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function getCsrfToken() {
|
function getCsrfToken() {
|
||||||
const match = document.cookie.split('; ').find((row) => row.startsWith('oxicloud_csrf='));
|
const match = document.cookie.split('; ').find((row) => row.startsWith('oxicloud_csrf='));
|
||||||
return match ? match.split('=')[1] : '';
|
return match ? match.split('=')[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 } : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { getCsrfHeaders, getCsrfToken };
|
||||||
|
|||||||
@@ -65,9 +65,4 @@ function isTextViewable(mimeType) {
|
|||||||
return textTypes.includes(mimeType);
|
return textTypes.includes(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.escapeHtml = escapeHtml;
|
export { escapeHtml, formatDateShort, formatDateTime, formatFileSize, formatQuotaSize, isTextViewable };
|
||||||
window.formatFileSize = formatFileSize;
|
|
||||||
window.formatQuotaSize = formatQuotaSize;
|
|
||||||
window.formatDateTime = formatDateTime;
|
|
||||||
window.formatDateShort = formatDateShort;
|
|
||||||
window.isTextViewable = isTextViewable;
|
|
||||||
|
|||||||
@@ -314,8 +314,7 @@ function safeT(key, params = {}) {
|
|||||||
return interpolate(value, params);
|
return interpolate(value, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export functions for use in other modules
|
export const i18n = {
|
||||||
window.i18n = {
|
|
||||||
t: safeT,
|
t: safeT,
|
||||||
setLocale,
|
setLocale,
|
||||||
getCurrentLocale,
|
getCurrentLocale,
|
||||||
|
|||||||
@@ -483,9 +483,8 @@ function replaceIconsInElement(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Expose globally ────────────────────────────────────────────
|
// ── Expose globally ────────────────────────────────────────────
|
||||||
window.oxiIcon = oxiIcon;
|
export { oxiIcon, replaceIconsInElement };
|
||||||
window.replaceIconsInElement = replaceIconsInElement;
|
export const OxiIcons = _ICONS;
|
||||||
window.OxiIcons = _ICONS;
|
|
||||||
|
|
||||||
// ── Auto-replace: MutationObserver bridge ──────────────────────
|
// ── Auto-replace: MutationObserver bridge ──────────────────────
|
||||||
// Watches the DOM for new <i class="fa-..."> elements and converts them
|
// Watches the DOM for new <i class="fa-..."> elements and converts them
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { ALL_LANGUAGES } from '../features/auth/auth.js';
|
||||||
|
import { i18n } from './i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language Selector Component for OxiCloud
|
* Language Selector Component for OxiCloud
|
||||||
* Custom styled dropdown with flags
|
* Custom styled dropdown with flags
|
||||||
@@ -63,7 +66,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 = i18n ? 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
|
||||||
@@ -183,8 +186,8 @@ function closeDropdown(container) {
|
|||||||
*/
|
*/
|
||||||
async function selectLanguage(langCode, container) {
|
async function selectLanguage(langCode, container) {
|
||||||
// Update i18n if available
|
// Update i18n if available
|
||||||
if (window.i18n) {
|
if (i18n) {
|
||||||
await window.i18n.setLocale(langCode);
|
await i18n.setLocale(langCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update HTML lang attribute and dir for RTL languages
|
// Update HTML lang attribute and dir for RTL languages
|
||||||
|
|||||||
+12
-9
@@ -1,3 +1,6 @@
|
|||||||
|
import { i18n } from './i18n.js';
|
||||||
|
import { replaceIconsInElement } from './icons.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal System for OxiCloud
|
* Modal System for OxiCloud
|
||||||
* Provides modern, styled modals to replace browser prompts/alerts
|
* Provides modern, styled modals to replace browser prompts/alerts
|
||||||
@@ -86,8 +89,8 @@ const Modal = {
|
|||||||
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
||||||
this.icon = document.getElementById('modal-icon');
|
this.icon = document.getElementById('modal-icon');
|
||||||
// Let icons.js convert it to SVG
|
// Let icons.js convert it to SVG
|
||||||
if (window.replaceIconsInElement) {
|
if (replaceIconsInElement) {
|
||||||
window.replaceIconsInElement(iconContainer);
|
replaceIconsInElement(iconContainer);
|
||||||
this.icon = document.getElementById('modal-icon');
|
this.icon = document.getElementById('modal-icon');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,14 +102,14 @@ const Modal = {
|
|||||||
// Set button text (use i18n if available)
|
// Set button text (use i18n if available)
|
||||||
if (confirmText) {
|
if (confirmText) {
|
||||||
this.confirmBtn.textContent = confirmText;
|
this.confirmBtn.textContent = confirmText;
|
||||||
} else if (window.i18n) {
|
} else if (i18n) {
|
||||||
this.confirmBtn.textContent = window.i18n.t('actions.confirm');
|
this.confirmBtn.textContent = i18n.t('actions.confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelText) {
|
if (cancelText) {
|
||||||
this.cancelBtn.textContent = cancelText;
|
this.cancelBtn.textContent = cancelText;
|
||||||
} else if (window.i18n) {
|
} else if (i18n) {
|
||||||
this.cancelBtn.textContent = window.i18n.t('actions.cancel');
|
this.cancelBtn.textContent = i18n.t('actions.cancel');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set callbacks
|
// Set callbacks
|
||||||
@@ -126,7 +129,7 @@ const Modal = {
|
|||||||
* @returns {Promise<string|null>}
|
* @returns {Promise<string|null>}
|
||||||
*/
|
*/
|
||||||
promptNewFolder() {
|
promptNewFolder() {
|
||||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
|
||||||
|
|
||||||
return this.prompt({
|
return this.prompt({
|
||||||
title: t('dialogs.new_folder_title') || 'New folder',
|
title: t('dialogs.new_folder_title') || 'New folder',
|
||||||
@@ -144,7 +147,7 @@ const Modal = {
|
|||||||
* @returns {Promise<string|null>}
|
* @returns {Promise<string|null>}
|
||||||
*/
|
*/
|
||||||
promptRename(currentName, isFolder = false) {
|
promptRename(currentName, isFolder = false) {
|
||||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
|
||||||
|
|
||||||
// For files, we want to select only the name part (without extension)
|
// For files, we want to select only the name part (without extension)
|
||||||
this._selectNameOnly = !isFolder;
|
this._selectNameOnly = !isFolder;
|
||||||
@@ -232,4 +235,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Export for use in other modules
|
// Export for use in other modules
|
||||||
window.Modal = Modal;
|
export { Modal };
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { i18n } from './i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OxiCloud – Notification Bell Module
|
* OxiCloud – Notification Bell Module
|
||||||
*
|
*
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
* in the top-bar. Upload progress, quota errors, and general messages all
|
* in the top-bar. Upload progress, quota errors, and general messages all
|
||||||
* go through this module.
|
* go through this module.
|
||||||
*
|
*
|
||||||
* Public API (on window.notifications):
|
* Public API (exported as `notifications`):
|
||||||
* addUploadBatch(totalFiles) → batchId
|
* addUploadBatch(totalFiles) → batchId
|
||||||
* updateFile(batchId, fileName, pct, status)
|
* updateFile(batchId, fileName, pct, status)
|
||||||
* finishBatch(batchId, successCount, totalFiles)
|
* finishBatch(batchId, successCount, totalFiles)
|
||||||
@@ -147,7 +149,7 @@ const notifications = (() => {
|
|||||||
item.className = 'notif-item';
|
item.className = 'notif-item';
|
||||||
item.id = batchId;
|
item.id = batchId;
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = i18n?.t || ((k) => k);
|
||||||
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
|
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
|
||||||
const filesLabel = t('upload.files');
|
const filesLabel = t('upload.files');
|
||||||
|
|
||||||
@@ -252,7 +254,7 @@ const notifications = (() => {
|
|||||||
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 = 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}%`;
|
||||||
@@ -280,7 +282,7 @@ const notifications = (() => {
|
|||||||
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 = i18n?.t || ((k) => k);
|
||||||
const completeText = t('upload.complete', {
|
const completeText = t('upload.complete', {
|
||||||
count: successCount,
|
count: successCount,
|
||||||
total: totalFiles
|
total: totalFiles
|
||||||
@@ -351,4 +353,4 @@ const notifications = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
window.notifications = notifications;
|
export { notifications };
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
* Handles login, registration, and admin setup
|
* Handles login, registration, and admin setup
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
const API_URL = '/api/auth';
|
const API_URL = '/api/auth';
|
||||||
const LOGIN_ENDPOINT = `${API_URL}/login`;
|
const LOGIN_ENDPOINT = `${API_URL}/login`;
|
||||||
@@ -112,7 +115,7 @@ const LANGUAGE_TEXTS = {
|
|||||||
|
|
||||||
// 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 = [
|
export const ALL_LANGUAGES = [
|
||||||
{
|
{
|
||||||
code: 'en',
|
code: 'en',
|
||||||
name: 'English',
|
name: 'English',
|
||||||
@@ -509,8 +512,8 @@ 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?.setLocale) {
|
if (i18n?.setLocale) {
|
||||||
await window.i18n.setLocale(selectedLanguage);
|
await i18n.setLocale(selectedLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide language panel
|
// Hide language panel
|
||||||
@@ -632,7 +635,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?.t ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
const template = i18n?.t ? 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,7 +672,6 @@ function initLoginElements() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
languagePanel = document.getElementById('language-panel');
|
|
||||||
loginPanel = document.getElementById('login-panel');
|
loginPanel = document.getElementById('login-panel');
|
||||||
registerPanel = document.getElementById('register-panel');
|
registerPanel = document.getElementById('register-panel');
|
||||||
adminSetupPanel = document.getElementById('admin-setup-panel');
|
adminSetupPanel = document.getElementById('admin-setup-panel');
|
||||||
@@ -940,7 +942,7 @@ if (isLoginPage && registerForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||||
registerError.textContent = errorMsg;
|
registerError.textContent = errorMsg;
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
@@ -950,7 +952,7 @@ if (isLoginPage && registerForm) {
|
|||||||
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 = i18n ? i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||||
registerSuccess.textContent = successMsg;
|
registerSuccess.textContent = successMsg;
|
||||||
registerSuccess.style.display = 'block';
|
registerSuccess.style.display = 'block';
|
||||||
|
|
||||||
@@ -963,7 +965,7 @@ if (isLoginPage && registerForm) {
|
|||||||
hidePanel(registerPanel);
|
hidePanel(registerPanel);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account';
|
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error registering account';
|
||||||
registerError.textContent = error.message || errorMsg;
|
registerError.textContent = error.message || errorMsg;
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
}
|
}
|
||||||
@@ -986,7 +988,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||||
adminSetupError.textContent = errorMsg;
|
adminSetupError.textContent = errorMsg;
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
@@ -1011,7 +1013,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
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 = i18n ? i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||||
|
|
||||||
if (adminSetupSuccess) {
|
if (adminSetupSuccess) {
|
||||||
adminSetupSuccess.textContent = successMsg;
|
adminSetupSuccess.textContent = successMsg;
|
||||||
@@ -1025,7 +1027,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
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 = i18n ? i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
||||||
adminSetupError.textContent = error.message || errorMsg;
|
adminSetupError.textContent = error.message || errorMsg;
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,14 @@
|
|||||||
* This file handles file and folder operations (create, move, delete, rename, upload)
|
* This file handles file and folder operations (create, move, delete, rename, upload)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { refreshUserData } from '../../app/authSession.js';
|
||||||
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
|
import { app } from '../../app/state.js';
|
||||||
|
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||||
|
import { getCsrfHeaders, getCsrfToken } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { notifications } from '../../core/notifications.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get authorization headers for API requests.
|
* Get authorization headers for API requests.
|
||||||
* Tokens are now in HttpOnly cookies — no explicit Authorization header needed.
|
* Tokens are now in HttpOnly cookies — no explicit Authorization header needed.
|
||||||
@@ -22,13 +30,13 @@ 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 ? window.notifications.addUploadBatch(totalFiles, folderName) : null;
|
this._currentBatchId = notifications ? notifications.addUploadBatch(totalFiles, folderName) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Finalise the batch in the notification bell */
|
/** Finalise the batch in the notification bell */
|
||||||
_finishUploadToast(successCount, totalFiles) {
|
_finishUploadToast(successCount, totalFiles) {
|
||||||
if (window.notifications && this._currentBatchId) {
|
if (notifications && this._currentBatchId) {
|
||||||
window.notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -58,7 +66,7 @@ const fileOps = {
|
|||||||
_uploadFileXHR(formData, batchId, fileName, timeoutMs = 120000) {
|
_uploadFileXHR(formData, batchId, fileName, timeoutMs = 120000) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const notif = window.notifications;
|
const notif = notifications;
|
||||||
// Do NOT set xhr.timeout — it is a TOTAL deadline from send() to
|
// Do NOT set xhr.timeout — it is a TOTAL deadline from send() to
|
||||||
// response and would kill large uploads even while data is flowing.
|
// response and would kill large uploads even while data is flowing.
|
||||||
// Instead we rely on the stall timer (no progress for N seconds)
|
// Instead we rely on the stall timer (no progress for N seconds)
|
||||||
@@ -299,13 +307,13 @@ const fileOps = {
|
|||||||
|
|
||||||
const totalFiles = readableFiles.length;
|
const totalFiles = readableFiles.length;
|
||||||
|
|
||||||
if (skippedEntries.length > 0 && window.notifications) {
|
if (skippedEntries.length > 0 && notifications) {
|
||||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
const locale = i18n?.getCurrentLocale?.() || 'en';
|
||||||
const title = locale.startsWith('es') ? 'Entradas omitidas' : 'Entries skipped';
|
const title = locale.startsWith('es') ? 'Entradas omitidas' : 'Entries skipped';
|
||||||
const text = locale.startsWith('es')
|
const text = locale.startsWith('es')
|
||||||
? `Se omitieron ${skippedEntries.length} carpeta(s)/entrada(s) no legibles. Usa "Subir carpeta".`
|
? `Se omitieron ${skippedEntries.length} carpeta(s)/entrada(s) no legibles. Usa "Subir carpeta".`
|
||||||
: `${skippedEntries.length} unreadable folder/entry items were skipped. Use "Upload folder".`;
|
: `${skippedEntries.length} unreadable folder/entry items were skipped. Use "Upload folder".`;
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-folder-open',
|
icon: 'fa-folder-open',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title,
|
title,
|
||||||
@@ -331,7 +339,7 @@ const fileOps = {
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
const targetFolderId = app.currentPath || app.userHomeFolderId;
|
||||||
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
@@ -353,9 +361,9 @@ const fileOps = {
|
|||||||
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 (notifications && batchId) {
|
||||||
try {
|
try {
|
||||||
window.notifications.fileCompleted(batchId, result.ok);
|
notifications.fileCompleted(batchId, result.ok);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Batch progress update failed:', e);
|
console.warn('Batch progress update failed:', e);
|
||||||
}
|
}
|
||||||
@@ -366,8 +374,8 @@ const fileOps = {
|
|||||||
console.log(`Successfully uploaded ${file.name}`, result.data);
|
console.log(`Successfully uploaded ${file.name}`, result.data);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Upload error for ${file.name}`);
|
console.error(`Upload error for ${file.name}`);
|
||||||
if (result.isTimeout && window.notifications) {
|
if (result.isTimeout && notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-clock',
|
icon: 'fa-clock',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: file.name,
|
title: file.name,
|
||||||
@@ -375,9 +383,9 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (result.isQuotaError) {
|
if (result.isQuotaError) {
|
||||||
const msg = result.errorMsg || window.i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
const msg = result.errorMsg || i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-triangle',
|
icon: 'fa-exclamation-triangle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: file.name,
|
title: file.name,
|
||||||
@@ -393,14 +401,12 @@ const fileOps = {
|
|||||||
this._finishUploadToast(successCount, totalFiles);
|
this._finishUploadToast(successCount, totalFiles);
|
||||||
|
|
||||||
// Refresh storage usage display
|
// Refresh storage usage display
|
||||||
if (typeof window.refreshUserData === 'function') {
|
|
||||||
try {
|
try {
|
||||||
await window.refreshUserData();
|
await refreshUserData();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.loadFiles({ forceRefresh: true });
|
await loadFiles({ forceRefresh: true });
|
||||||
} catch (reloadError) {
|
} catch (reloadError) {
|
||||||
console.error('Error reloading files:', reloadError);
|
console.error('Error reloading files:', reloadError);
|
||||||
}
|
}
|
||||||
@@ -466,7 +472,7 @@ const fileOps = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
const currentFolderId = app.currentPath || app.userHomeFolderId;
|
||||||
|
|
||||||
// Build folder structure from relative paths
|
// Build folder structure from relative paths
|
||||||
const folderMap = new Map();
|
const folderMap = new Map();
|
||||||
@@ -528,7 +534,7 @@ const fileOps = {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
const locale = i18n?.getCurrentLocale?.() || 'en';
|
||||||
const rootFolderLabel =
|
const rootFolderLabel =
|
||||||
rootFolderNames.length <= 1
|
rootFolderNames.length <= 1
|
||||||
? rootFolderNames[0] || ''
|
? rootFolderNames[0] || ''
|
||||||
@@ -585,9 +591,9 @@ const fileOps = {
|
|||||||
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
successCount++;
|
successCount++;
|
||||||
if (window.notifications && batchId) {
|
if (notifications && batchId) {
|
||||||
try {
|
try {
|
||||||
window.notifications.fileCompleted(batchId, true);
|
notifications.fileCompleted(batchId, true);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -617,9 +623,9 @@ const fileOps = {
|
|||||||
|
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
|
|
||||||
if (window.notifications && batchId) {
|
if (notifications && batchId) {
|
||||||
try {
|
try {
|
||||||
window.notifications.fileCompleted(batchId, result.ok);
|
notifications.fileCompleted(batchId, result.ok);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (progressBar && uploadedCount % 10 === 0) {
|
if (progressBar && uploadedCount % 10 === 0) {
|
||||||
@@ -633,8 +639,8 @@ const fileOps = {
|
|||||||
successCount++;
|
successCount++;
|
||||||
} else if (result.isQuotaError) {
|
} else if (result.isQuotaError) {
|
||||||
quotaStop = true;
|
quotaStop = true;
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-triangle',
|
icon: 'fa-exclamation-triangle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: file.name,
|
title: file.name,
|
||||||
@@ -661,14 +667,12 @@ const fileOps = {
|
|||||||
|
|
||||||
this._finishUploadToast(successCount, totalFiles);
|
this._finishUploadToast(successCount, totalFiles);
|
||||||
|
|
||||||
if (typeof window.refreshUserData === 'function') {
|
|
||||||
try {
|
try {
|
||||||
await window.refreshUserData();
|
await refreshUserData();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.loadFiles({ forceRefresh: true });
|
await loadFiles({ forceRefresh: true });
|
||||||
} catch (reloadError) {
|
} catch (reloadError) {
|
||||||
console.error('Error reloading files:', reloadError);
|
console.error('Error reloading files:', reloadError);
|
||||||
}
|
}
|
||||||
@@ -699,7 +703,7 @@ const fileOps = {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: name,
|
name: name,
|
||||||
parent_id: window.app.currentPath || window.app.userHomeFolderId || null
|
parent_id: app.currentPath || app.userHomeFolderId || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -710,17 +714,17 @@ const fileOps = {
|
|||||||
|
|
||||||
// Optimistic UI: add folder card directly from server response
|
// Optimistic UI: add folder card directly from server response
|
||||||
// — no reload needed since the backend already confirmed creation.
|
// — no reload needed since the backend already confirmed creation.
|
||||||
window.ui.addFolderToView(folder);
|
ui.addFolderToView(folder);
|
||||||
|
|
||||||
window.ui.showNotification('Folder created', `"${name}" created successfully`);
|
ui.showNotification('Folder created', `"${name}" created successfully`);
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.text();
|
const errorData = await response.text();
|
||||||
console.error('Create folder error:', errorData);
|
console.error('Create folder error:', errorData);
|
||||||
window.ui.showNotification('Error', 'Error creating the folder');
|
ui.showNotification('Error', 'Error creating the folder');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating folder:', error);
|
console.error('Error creating folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error creating the folder');
|
ui.showNotification('Error', 'Error creating the folder');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -745,8 +749,8 @@ const fileOps = {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Reload files after moving
|
// Reload files after moving
|
||||||
await window.loadFiles();
|
await loadFiles();
|
||||||
window.ui.showNotification('File moved', 'File moved successfully');
|
ui.showNotification('File moved', 'File moved successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Unknown error';
|
let errorMessage = 'Unknown error';
|
||||||
@@ -756,12 +760,12 @@ const fileOps = {
|
|||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving file:', error);
|
console.error('Error moving file:', error);
|
||||||
window.ui.showNotification('Error', 'Error moving the file');
|
ui.showNotification('Error', 'Error moving the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -787,8 +791,8 @@ const fileOps = {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Reload files after moving
|
// Reload files after moving
|
||||||
await window.loadFiles();
|
await loadFiles();
|
||||||
window.ui.showNotification('Folder moved', 'Folder moved successfully');
|
ui.showNotification('Folder moved', 'Folder moved successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Unknown error';
|
let errorMessage = 'Unknown error';
|
||||||
@@ -798,12 +802,12 @@ const fileOps = {
|
|||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving folder:', error);
|
console.error('Error moving folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error moving the folder');
|
ui.showNotification('Error', 'Error moving the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -890,8 +894,8 @@ const fileOps = {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await response.json();
|
await response.json();
|
||||||
// Reload files after copying
|
// Reload files after copying
|
||||||
await window.loadFiles();
|
await loadFiles();
|
||||||
window.ui.showNotification('File copied', 'File copied successfully');
|
ui.showNotification('File copied', 'File copied successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Unknown error';
|
let errorMessage = 'Unknown error';
|
||||||
@@ -901,12 +905,12 @@ const fileOps = {
|
|||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying file:', error);
|
console.error('Error copying file:', error);
|
||||||
window.ui.showNotification('Error', 'Error copying the file');
|
ui.showNotification('Error', 'Error copying the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -920,7 +924,7 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async copyFolder(_folderId, _targetFolderId) {
|
async copyFolder(_folderId, _targetFolderId) {
|
||||||
// Folder copy is not yet implemented in the backend
|
// Folder copy is not yet implemented in the backend
|
||||||
window.ui.showNotification('Not implemented', 'Folder copy is not yet supported');
|
ui.showNotification('Not implemented', 'Folder copy is not yet supported');
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -954,7 +958,7 @@ const fileOps = {
|
|||||||
|
|
||||||
// Note: Folder copy is not yet implemented in batch API
|
// Note: Folder copy is not yet implemented in batch API
|
||||||
if (folderIds.length > 0) {
|
if (folderIds.length > 0) {
|
||||||
window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
||||||
errors += folderIds.lenngth;
|
errors += folderIds.lenngth;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -990,9 +994,9 @@ const fileOps = {
|
|||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification(
|
ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed',
|
i18n ? i18n.t('notifications.file_renamed') : 'File renamed',
|
||||||
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
i18n ? i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -1005,12 +1009,12 @@ const fileOps = {
|
|||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming file:', error);
|
console.error('Error renaming file:', error);
|
||||||
window.ui.showNotification('Error', 'Error renaming the file');
|
ui.showNotification('Error', 'Error renaming the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1037,7 +1041,7 @@ const fileOps = {
|
|||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
@@ -1053,12 +1057,12 @@ const fileOps = {
|
|||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming folder:', error);
|
console.error('Error renaming folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error renaming the folder');
|
ui.showNotification('Error', 'Error renaming the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1071,11 +1075,9 @@ 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: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||||
message: window.i18n
|
message: i18n ? i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||||
? window.i18n.t('dialogs.confirm_delete_file', { name: fileName })
|
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
||||||
: `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;
|
||||||
|
|
||||||
@@ -1087,8 +1089,8 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
window.ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct deletion if trash fails
|
// Fallback to direct deletion if trash fails
|
||||||
@@ -1098,17 +1100,17 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (fallbackResponse.ok) {
|
if (fallbackResponse.ok) {
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error deleting the file');
|
ui.showNotification('Error', 'Error deleting the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting file:', error);
|
console.error('Error deleting file:', error);
|
||||||
window.ui.showNotification('Error', 'Error deleting the file');
|
ui.showNotification('Error', 'Error deleting the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1121,11 +1123,11 @@ 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: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||||
message: window.i18n
|
message: i18n
|
||||||
? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
? i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
||||||
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
: `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'
|
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1138,12 +1140,12 @@ const fileOps = {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// If we're inside the folder we just deleted, go back up
|
// If we're inside the folder we just deleted, go back up
|
||||||
if (window.app.currentPath === folderId) {
|
if (app.currentPath === folderId) {
|
||||||
window.app.currentPath = '';
|
app.currentPath = '';
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
}
|
}
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
window.ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct deletion if trash fails
|
// Fallback to direct deletion if trash fails
|
||||||
@@ -1154,21 +1156,21 @@ const fileOps = {
|
|||||||
|
|
||||||
if (fallbackResponse.ok) {
|
if (fallbackResponse.ok) {
|
||||||
// If we're inside the folder we just deleted, go back up
|
// If we're inside the folder we just deleted, go back up
|
||||||
if (window.app.currentPath === folderId) {
|
if (app.currentPath === folderId) {
|
||||||
window.app.currentPath = '';
|
app.currentPath = '';
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
}
|
}
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error deleting the folder');
|
ui.showNotification('Error', 'Error deleting the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting folder:', error);
|
console.error('Error deleting folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error deleting the folder');
|
ui.showNotification('Error', 'Error deleting the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1212,15 +1214,15 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Item restored', 'Item restored successfully');
|
ui.showNotification('Item restored', 'Item restored successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error restoring the item');
|
ui.showNotification('Error', 'Error restoring the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring item from trash:', error);
|
console.error('Error restoring item from trash:', error);
|
||||||
window.ui.showNotification('Error', 'Error restoring the item');
|
ui.showNotification('Error', 'Error restoring the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1232,11 +1234,11 @@ 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: i18n ? i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
||||||
message: window.i18n
|
message: i18n
|
||||||
? window.i18n.t('dialogs.confirm_permanent_delete_msg')
|
? i18n.t('dialogs.confirm_permanent_delete_msg')
|
||||||
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
: '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'
|
confirmText: i18n ? i18n.t('actions.delete_permanently') : 'Delete permanently'
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1247,15 +1249,15 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Item deleted', 'Item permanently deleted');
|
ui.showNotification('Item deleted', 'Item permanently deleted');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error deleting the item');
|
ui.showNotification('Error', 'Error deleting the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting item permanently:', error);
|
console.error('Error deleting item permanently:', error);
|
||||||
window.ui.showNotification('Error', 'Error deleting the item');
|
ui.showNotification('Error', 'Error deleting the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1266,11 +1268,9 @@ 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: i18n ? i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
||||||
message: window.i18n
|
message: i18n ? i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||||
? window.i18n.t('trash.empty_confirm')
|
confirmText: i18n ? i18n.t('actions.empty_trash') : 'Empty trash'
|
||||||
: '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;
|
||||||
|
|
||||||
@@ -1281,15 +1281,15 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error emptying the trash');
|
ui.showNotification('Error', 'Error emptying the trash');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error emptying trash:', error);
|
console.error('Error emptying trash:', error);
|
||||||
window.ui.showNotification('Error', 'Error emptying the trash');
|
ui.showNotification('Error', 'Error emptying the trash');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1315,11 +1315,11 @@ const fileOps = {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error downloading the file');
|
ui.showNotification('Error', 'Error downloading the file');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading file:', error);
|
console.error('Error downloading file:', error);
|
||||||
window.ui.showNotification('Error', 'Error downloading the file');
|
ui.showNotification('Error', 'Error downloading the file');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1331,7 +1331,7 @@ const fileOps = {
|
|||||||
async downloadFolder(folderId, folderName) {
|
async downloadFolder(folderId, folderName) {
|
||||||
try {
|
try {
|
||||||
// Show notification to user
|
// Show notification to user
|
||||||
window.ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||||
|
|
||||||
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
||||||
headers: getAuthHeaders()
|
headers: getAuthHeaders()
|
||||||
@@ -1347,14 +1347,13 @@ const fileOps = {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error downloading the folder');
|
ui.showNotification('Error', 'Error downloading the folder');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading folder:', error);
|
console.error('Error downloading folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error downloading the folder');
|
ui.showNotification('Error', 'Error downloading the folder');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose file operations module globally
|
export { fileOps, getAuthHeaders };
|
||||||
window.fileOps = fileOps;
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
* A simpler approach to viewing files that doesn't rely on complex DOM manipulation
|
* A simpler approach to viewing files that doesn't rely on complex DOM manipulation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { updateHistory } from '../../app/main.js';
|
||||||
|
import { app } from '../../app/state.js';
|
||||||
|
import { isTextViewable } from '../../core/formatters.js';
|
||||||
|
import { wopiEditor } from './wopiEditor.js';
|
||||||
|
|
||||||
class InlineViewer {
|
class InlineViewer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setupViewer();
|
this.setupViewer();
|
||||||
@@ -97,9 +102,9 @@ class InlineViewer {
|
|||||||
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?.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 && wopiEditor && (await wopiEditor.canEdit(file.name))) {
|
||||||
try {
|
try {
|
||||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('WOPI editor failed, falling back to inline viewer:', e);
|
console.warn('WOPI editor failed, falling back to inline viewer:', e);
|
||||||
@@ -204,9 +209,9 @@ class InlineViewer {
|
|||||||
modal.classList.add('active');
|
modal.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a MIME type is text-viewable — delegates to window.isTextViewable
|
// Check if a MIME type is text-viewable
|
||||||
isTextViewable(mimeType) {
|
isTextViewable(mimeType) {
|
||||||
return window.isTextViewable ? window.isTextViewable(mimeType) : false;
|
return isTextViewable(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a text viewer using authenticated fetch
|
// Creates a text viewer using authenticated fetch
|
||||||
@@ -513,8 +518,8 @@ class InlineViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clear
|
// clear
|
||||||
window.app.viewFile = null;
|
app.viewFile = null;
|
||||||
window.updateHistory(false);
|
updateHistory(false);
|
||||||
|
|
||||||
// Clear references
|
// Clear references
|
||||||
this.currentFile = null;
|
this.currentFile = null;
|
||||||
@@ -573,21 +578,4 @@ class InlineViewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize viewer when document is ready
|
export const inlineViewer = new InlineViewer();
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Check if it's already initialized
|
|
||||||
if (!window.inlineViewer) {
|
|
||||||
console.log('Initializing inline viewer on DOMContentLoaded');
|
|
||||||
window.inlineViewer = new InlineViewer();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback initialization for cases where DOMContentLoaded already fired
|
|
||||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
||||||
if (!window.inlineViewer) {
|
|
||||||
console.log('Fallback initialization for inline viewer');
|
|
||||||
setTimeout(() => {
|
|
||||||
window.inlineViewer = new InlineViewer();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
|
import { app } from '../../app/state.js';
|
||||||
|
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { favorites } from '../library/favorites.js';
|
||||||
|
import { contextMenus } from './contextMenus.js';
|
||||||
|
import { getAuthHeaders } from './fileOperations.js';
|
||||||
|
|
||||||
const multiSelect = {
|
const multiSelect = {
|
||||||
/** Currently selected items: Map<id, { id, name, type, parentId }> */
|
/** Currently selected items: Map<id, { id, name, type, parentId }> */
|
||||||
_selected: new Map(),
|
_selected: new Map(),
|
||||||
@@ -42,8 +50,8 @@ const multiSelect = {
|
|||||||
// ── Helpers for i18n ────────────────────────────────────
|
// ── Helpers for i18n ────────────────────────────────────
|
||||||
|
|
||||||
_t(key, vars) {
|
_t(key, vars) {
|
||||||
if (window.i18n && typeof window.i18n.t === 'function') {
|
if (i18n && typeof i18n.t === 'function') {
|
||||||
const val = window.i18n.t(key, vars);
|
const val = i18n.t(key, vars);
|
||||||
// If i18n returned the key itself, it's missing → fall back
|
// If i18n returned the key itself, it's missing → fall back
|
||||||
if (val && val !== key) return val;
|
if (val && val !== key) return val;
|
||||||
}
|
}
|
||||||
@@ -133,15 +141,15 @@ const multiSelect = {
|
|||||||
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`);
|
ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Items copied', `${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
ui.showNotification('Items copied', `${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`);
|
ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Items moved', `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
ui.showNotification('Items moved', `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -366,18 +374,18 @@ const multiSelect = {
|
|||||||
const errors = data.stats?.failed || 0;
|
const errors = data.stats?.failed || 0;
|
||||||
|
|
||||||
this.clear();
|
this.clear();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
|
|
||||||
if (errors > 0) {
|
if (errors > 0) {
|
||||||
window.ui.showNotification('Batch delete', `${success} moved to trash, ${errors} failed`);
|
ui.showNotification('Batch delete', `${success} moved to trash, ${errors} failed`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Moved to trash', `${success} item${success !== 1 ? 's' : ''} moved to trash`);
|
ui.showNotification('Moved to trash', `${success} item${success !== 1 ? 's' : ''} moved to trash`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Batch trash error:', e);
|
console.error('Batch trash error:', e);
|
||||||
window.ui.showNotification('Error', 'Could not move items to trash');
|
ui.showNotification('Error', 'Could not move items to trash');
|
||||||
this.clear();
|
this.clear();
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -386,9 +394,9 @@ const multiSelect = {
|
|||||||
const items = this.items;
|
const items = this.items;
|
||||||
if (items.length === 0) return;
|
if (items.length === 0) return;
|
||||||
|
|
||||||
window.app.moveDialogMode = 'batch';
|
app.moveDialogMode = 'batch';
|
||||||
window.app.batchMoveItems = items;
|
app.batchMoveItems = items;
|
||||||
window.app.selectedTargetFolderId = '';
|
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');
|
||||||
@@ -406,7 +414,7 @@ const multiSelect = {
|
|||||||
const items = this.items;
|
const items = this.items;
|
||||||
if (items.length === 0) return;
|
if (items.length === 0) return;
|
||||||
|
|
||||||
window.ui.showNotification('Preparing download', 'Creating ZIP archive...');
|
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);
|
||||||
@@ -431,20 +439,20 @@ const multiSelect = {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Batch download error:', e);
|
console.error('Batch download error:', e);
|
||||||
window.ui.showNotification('Error', 'Could not download selected items');
|
ui.showNotification('Error', 'Could not download selected items');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Batch add to favorites — single API call */
|
/** Batch add to favorites — single API call */
|
||||||
async batchFavorites() {
|
async batchFavorites() {
|
||||||
const items = this.items;
|
const items = this.items;
|
||||||
if (items.length === 0 || !window.favorites) return;
|
if (items.length === 0 || !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) => !favorites.isFavorite(i.id, i.type));
|
||||||
if (toAdd.length === 0) {
|
if (toAdd.length === 0) {
|
||||||
this.clear();
|
this.clear();
|
||||||
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,23 +471,23 @@ const multiSelect = {
|
|||||||
const inserted = data.stats?.inserted || 0;
|
const inserted = data.stats?.inserted || 0;
|
||||||
|
|
||||||
// Replace cache directly from response (no extra GET)
|
// Replace cache directly from response (no extra GET)
|
||||||
if (data.favorites && window.favorites._replaceCacheFromResponse) {
|
if (data.favorites && favorites._replaceCacheFromResponse) {
|
||||||
window.favorites._replaceCacheFromResponse(data.favorites);
|
favorites._replaceCacheFromResponse(data.favorites);
|
||||||
} else {
|
} else {
|
||||||
await window.favorites._fetchFromServer();
|
await favorites._fetchFromServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clear();
|
this.clear();
|
||||||
if (typeof window.loadFiles === 'function') window.loadFiles();
|
loadFiles();
|
||||||
|
|
||||||
if (inserted > 0) {
|
if (inserted > 0) {
|
||||||
window.ui.showNotification(this._t('favorites.add') || 'Added to favorites', `${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`);
|
ui.showNotification(this._t('favorites.add') || 'Added to favorites', `${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
ui.showNotification(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);
|
||||||
window.ui.showNotification('Error', 'Could not add items to favorites');
|
ui.showNotification('Error', 'Could not add items to favorites');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -510,8 +518,8 @@ 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?.translateElement) {
|
if (i18n?.translateElement) {
|
||||||
window.i18n.translateElement(batchSelectionBar);
|
i18n.translateElement(batchSelectionBar);
|
||||||
}
|
}
|
||||||
this._wireBarButtons();
|
this._wireBarButtons();
|
||||||
},
|
},
|
||||||
@@ -524,5 +532,4 @@ const multiSelect = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose globally
|
export { multiSelect };
|
||||||
window.multiSelect = multiSelect;
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
* displays the enriched results returned by the server.
|
* displays the enriched results returned by the server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
|
import { app } from '../../app/state.js';
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
import { getAuthHeaders } from './fileOperations.js';
|
||||||
|
|
||||||
const search = {
|
const search = {
|
||||||
/**
|
/**
|
||||||
* Perform a search using query parameters.
|
* Perform a search using query parameters.
|
||||||
@@ -55,7 +60,7 @@ 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');
|
ui.showNotification('Error', 'Error performing search');
|
||||||
return {
|
return {
|
||||||
files: [],
|
files: [],
|
||||||
folders: [],
|
folders: [],
|
||||||
@@ -109,7 +114,7 @@ const search = {
|
|||||||
* @param {Object} results - Enriched search results from backend
|
* @param {Object} results - Enriched search results from backend
|
||||||
*/
|
*/
|
||||||
displaySearchResults(results) {
|
displaySearchResults(results) {
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
//FIXME: move into action rather bage sticky header + hide bread crumb ? + note also that search result does not consider section
|
//FIXME: move into action rather bage sticky header + hide bread crumb ? + note also that search result does not consider section
|
||||||
const pageStickyHeader = document.getElementById('page-sticky-header');
|
const pageStickyHeader = document.getElementById('page-sticky-header');
|
||||||
|
|
||||||
@@ -163,17 +168,17 @@ const search = {
|
|||||||
if (clearSearchBtn) {
|
if (clearSearchBtn) {
|
||||||
clearSearchBtn.addEventListener('click', () => {
|
clearSearchBtn.addEventListener('click', () => {
|
||||||
document.querySelector('.search-container input').value = '';
|
document.querySelector('.search-container input').value = '';
|
||||||
window.app.currentPath = '';
|
app.currentPath = '';
|
||||||
window.app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
document.querySelector('.search-results-header')?.remove();
|
document.querySelector('.search-results-header')?.remove();
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
window.loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty state
|
// Empty state
|
||||||
if (results.files.length === 0 && results.folders.length === 0) {
|
if (results.files.length === 0 && results.folders.length === 0) {
|
||||||
window.ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-search empty-state-icon"></i>
|
<i class="fas fa-search empty-state-icon"></i>
|
||||||
<p class="search-empty-text">No results found for this search</p>
|
<p class="search-empty-text">No results found for this search</p>
|
||||||
`);
|
`);
|
||||||
@@ -182,12 +187,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);
|
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);
|
ui.addFileToView(file);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -203,19 +208,18 @@ const search = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Cache cleared', 'Search cache cleared successfully');
|
ui.showNotification('Cache cleared', 'Search cache cleared successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error clearing search cache');
|
ui.showNotification('Error', 'Error clearing search cache');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing search cache:', error);
|
console.error('Error clearing search cache:', error);
|
||||||
window.ui.showNotification('Error', 'Error clearing search cache');
|
ui.showNotification('Error', 'Error clearing search cache');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose the search module globally
|
export { search };
|
||||||
window.search = search;
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* Opens document files in Collabora Online / OnlyOffice via WOPI protocol.
|
* Opens document files in Collabora Online / OnlyOffice via WOPI protocol.
|
||||||
* Supports two modes: in-app modal (default) and new browser tab.
|
* Supports two modes: in-app modal (default) and new browser tab.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
|
|
||||||
class WopiEditor {
|
class WopiEditor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.editorModal = null;
|
this.editorModal = null;
|
||||||
@@ -203,11 +206,10 @@ class WopiEditor {
|
|||||||
this._messageHandler = null;
|
this._messageHandler = null;
|
||||||
}
|
}
|
||||||
this.editorModal = null;
|
this.editorModal = null;
|
||||||
|
|
||||||
// Refresh file list to pick up any saves
|
// Refresh file list to pick up any saves
|
||||||
if (typeof loadFiles === 'function') {
|
|
||||||
loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch supported extensions from the server (cached).
|
* Fetch supported extensions from the server (cached).
|
||||||
@@ -243,7 +245,7 @@ class WopiEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global instance
|
// Global instance
|
||||||
window.wopiEditor = new WopiEditor();
|
export const wopiEditor = new WopiEditor();
|
||||||
|
|
||||||
// Prefetch supported extensions so canEdit() is fast on first use
|
// Prefetch supported extensions so canEdit() is fast on first use
|
||||||
window.wopiEditor._fetchSupportedExtensions();
|
wopiEditor._fetchSupportedExtensions();
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
* rendering path so star icons can be painted without a round-trip.
|
* rendering path so star icons can be painted without a round-trip.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
const favorites = {
|
const favorites = {
|
||||||
/** @type {Map<string, object>} key = "file:<id>" | "folder:<id>" */
|
/** @type {Map<string, object>} key = "file:<id>" | "folder:<id>" */
|
||||||
_cache: new Map(),
|
_cache: new Map(),
|
||||||
@@ -104,10 +108,10 @@ const favorites = {
|
|||||||
await this._fetchFromServer();
|
await this._fetchFromServer();
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
if (window.ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
i18n ? i18n.t('favorites.added_title') : 'Added to favorites',
|
||||||
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
`"${name}" ${i18n ? i18n.t('favorites.added_msg') : 'added to favorites'}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +143,10 @@ 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?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
i18n ? i18n.t('favorites.removed_title') : 'Removed from favorites',
|
||||||
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
`"${itemName}" ${i18n ? i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,14 +171,14 @@ const favorites = {
|
|||||||
await this._fetchFromServer();
|
await this._fetchFromServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
|
|
||||||
if (this._cache.size === 0) {
|
if (this._cache.size === 0) {
|
||||||
window.ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-star empty-state-icon"></i>
|
<i class="fas fa-star empty-state-icon"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
<p>${i18n ? i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
||||||
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
<p>${i18n ? i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
||||||
`);
|
`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -204,16 +208,15 @@ const favorites = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (folders.length) window.ui.renderFolders(folders);
|
if (folders.length) ui.renderFolders(folders);
|
||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying favorites:', error);
|
console.error('Error displaying favorites:', error);
|
||||||
if (window.ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading favorite items');
|
ui.showNotification('Error', 'Error loading favorite items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose globally
|
export { favorites };
|
||||||
window.favorites = favorites;
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import { app } from '../../app/state.js';
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { formatFileSize } from '../../core/formatters.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { oxiIcon } from '../../core/icons.js';
|
||||||
|
import { Modal } from '../../core/modal.js';
|
||||||
|
import { notifications } from '../../core/notifications.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OxiCloud - Music Library View
|
* OxiCloud - Music Library View
|
||||||
* Playlist management with track listings and audio player
|
* Playlist management with track listings and audio player
|
||||||
@@ -13,7 +21,7 @@ const musicView = {
|
|||||||
selected: new Set(),
|
selected: new Set(),
|
||||||
|
|
||||||
_headers(json = false) {
|
_headers(json = false) {
|
||||||
const h = typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
const h = { ...getCsrfHeaders() };
|
||||||
if (json) h['Content-Type'] = 'application/json';
|
if (json) h['Content-Type'] = 'application/json';
|
||||||
return h;
|
return h;
|
||||||
},
|
},
|
||||||
@@ -78,8 +86,9 @@ const musicView = {
|
|||||||
_renderPlaylists() {
|
_renderPlaylists() {
|
||||||
if (!this._container) return;
|
if (!this._container) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
// FIXME should call directly
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
const t = (key, _fallback = '') => {
|
||||||
|
return i18n.t(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Empty state: no playlists at all — show full-width centered onboarding
|
// Empty state: no playlists at all — show full-width centered onboarding
|
||||||
@@ -178,7 +187,7 @@ const musicView = {
|
|||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.playlists.length === 0) {
|
if (this.playlists.length === 0) {
|
||||||
@@ -283,7 +292,7 @@ const musicView = {
|
|||||||
if (nameEl) nameEl.textContent = playlist.name;
|
if (nameEl) nameEl.textContent = playlist.name;
|
||||||
if (metaEl) {
|
if (metaEl) {
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
|
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
|
||||||
}
|
}
|
||||||
@@ -305,7 +314,7 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
|
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
|
||||||
if (togglePublicBtn) {
|
if (togglePublicBtn) {
|
||||||
const t2 = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t2 = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
|
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
|
||||||
togglePublicBtn.classList.toggle('active', playlist.is_public);
|
togglePublicBtn.classList.toggle('active', playlist.is_public);
|
||||||
}
|
}
|
||||||
@@ -344,7 +353,7 @@ const musicView = {
|
|||||||
if (!trackListEl) return;
|
if (!trackListEl) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.currentTracks.length === 0) {
|
if (this.currentTracks.length === 0) {
|
||||||
@@ -459,7 +468,7 @@ const musicView = {
|
|||||||
if (!this.currentTracks[idx]) return;
|
if (!this.currentTracks[idx]) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
|
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
|
||||||
musicPlayer.playTrack(idx);
|
musicPlayer.playTrack(idx);
|
||||||
@@ -479,7 +488,7 @@ const musicView = {
|
|||||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
}
|
}
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
|
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
|
||||||
musicPlayer.playTrack(0);
|
musicPlayer.playTrack(0);
|
||||||
@@ -488,11 +497,10 @@ const musicView = {
|
|||||||
|
|
||||||
async _showCreatePlaylistDialog() {
|
async _showCreatePlaylistDialog() {
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.Modal) return;
|
const name = await Modal.prompt({
|
||||||
const name = await window.Modal.prompt({
|
|
||||||
title: t('music.create_playlist', 'Create Playlist'),
|
title: t('music.create_playlist', 'Create Playlist'),
|
||||||
label: t('music.playlist_name', 'Playlist name'),
|
label: t('music.playlist_name', 'Playlist name'),
|
||||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
placeholder: t('music.playlist_name', 'Playlist name'),
|
||||||
@@ -506,7 +514,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _createPlaylist(name) {
|
async _createPlaylist(name) {
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
const createBtn = document.getElementById('music-create-playlist-btn');
|
const createBtn = document.getElementById('music-create-playlist-btn');
|
||||||
if (createBtn) createBtn.disabled = true;
|
if (createBtn) createBtn.disabled = true;
|
||||||
@@ -524,8 +532,8 @@ const musicView = {
|
|||||||
this.playlists.unshift(playlist);
|
this.playlists.unshift(playlist);
|
||||||
this._renderPlaylists();
|
this._renderPlaylists();
|
||||||
this._selectPlaylist(playlist.id);
|
this._selectPlaylist(playlist.id);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.create_playlist', 'Create Playlist'),
|
title: t('music.create_playlist', 'Create Playlist'),
|
||||||
@@ -534,8 +542,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Create playlist error:', err);
|
console.error('Create playlist error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -549,17 +557,13 @@ const musicView = {
|
|||||||
|
|
||||||
async _deletePlaylist() {
|
async _deletePlaylist() {
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
|
|
||||||
const confirmed = await new Promise((resolve) => {
|
const confirmed = await new Promise((resolve) => {
|
||||||
if (!window.Modal) {
|
Modal.prompt({
|
||||||
resolve(confirm(t('music.confirm_delete', 'Delete this playlist?')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.Modal.prompt({
|
|
||||||
title: t('music.delete', 'Delete'),
|
title: t('music.delete', 'Delete'),
|
||||||
label: t('music.confirm_delete', 'Delete this playlist?'),
|
label: t('music.confirm_delete', 'Delete this playlist?'),
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
@@ -586,13 +590,13 @@ const musicView = {
|
|||||||
this.currentPlaylist = null;
|
this.currentPlaylist = null;
|
||||||
this.currentTracks = [];
|
this.currentTracks = [];
|
||||||
this._renderPlaylists();
|
this._renderPlaylists();
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
|
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Delete playlist error:', err);
|
console.error('Delete playlist error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -641,11 +645,10 @@ const musicView = {
|
|||||||
async _showEditPlaylistDialog() {
|
async _showEditPlaylistDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.Modal) return;
|
const newName = await Modal.prompt({
|
||||||
const newName = await window.Modal.prompt({
|
|
||||||
title: t('music.edit', 'Edit'),
|
title: t('music.edit', 'Edit'),
|
||||||
label: t('music.playlist_name', 'Playlist name'),
|
label: t('music.playlist_name', 'Playlist name'),
|
||||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
placeholder: t('music.playlist_name', 'Playlist name'),
|
||||||
@@ -674,8 +677,8 @@ const musicView = {
|
|||||||
this._renderPlaylistList();
|
this._renderPlaylistList();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Edit playlist error:', err);
|
console.error('Edit playlist error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -688,11 +691,10 @@ const musicView = {
|
|||||||
async _showSharePlaylistDialog() {
|
async _showSharePlaylistDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.Modal) return;
|
const userId = await Modal.prompt({
|
||||||
const userId = await window.Modal.prompt({
|
|
||||||
title: t('music.share', 'Share'),
|
title: t('music.share', 'Share'),
|
||||||
label: t('music.share_with_user', 'User ID or email'),
|
label: t('music.share_with_user', 'User ID or email'),
|
||||||
placeholder: t('music.share_with_user', 'User ID or email'),
|
placeholder: t('music.share_with_user', 'User ID or email'),
|
||||||
@@ -711,8 +713,8 @@ const musicView = {
|
|||||||
|
|
||||||
if (!resp.ok) throw new Error('Failed to share playlist');
|
if (!resp.ok) throw new Error('Failed to share playlist');
|
||||||
|
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.share', 'Share'),
|
title: t('music.share', 'Share'),
|
||||||
@@ -721,8 +723,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Share playlist error:', err);
|
console.error('Share playlist error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -735,7 +737,7 @@ const musicView = {
|
|||||||
async _showAddTracksDialog() {
|
async _showAddTracksDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Build modal overlay ──
|
// ── Build modal overlay ──
|
||||||
@@ -813,7 +815,7 @@ const musicView = {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const row = document.createElement('label');
|
const row = document.createElement('label');
|
||||||
row.className = `music-picker-item${selectedIds.has(file.id) ? ' selected' : ''}`;
|
row.className = `music-picker-item${selectedIds.has(file.id) ? ' selected' : ''}`;
|
||||||
const sizeStr = file.size != null && window.formatFileSize ? window.formatFileSize(file.size) : '';
|
const sizeStr = file.size != null && formatFileSize ? formatFileSize(file.size) : '';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<input type="checkbox" value="${file.id}" ${selectedIds.has(file.id) ? 'checked' : ''}>
|
<input type="checkbox" value="${file.id}" ${selectedIds.has(file.id) ? 'checked' : ''}>
|
||||||
<i class="fas fa-file-audio"></i>
|
<i class="fas fa-file-audio"></i>
|
||||||
@@ -858,8 +860,8 @@ const musicView = {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Failed to add tracks');
|
if (!resp.ok) throw new Error('Failed to add tracks');
|
||||||
|
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.add_tracks', 'Add Tracks'),
|
title: t('music.add_tracks', 'Add Tracks'),
|
||||||
@@ -878,8 +880,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Add tracks error:', err);
|
console.error('Add tracks error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -898,7 +900,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _removeTrackFromPlaylist(_trackId, fileId) {
|
async _removeTrackFromPlaylist(_trackId, fileId) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
|
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
|
||||||
@@ -908,8 +910,8 @@ const musicView = {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Failed to remove track');
|
if (!resp.ok) throw new Error('Failed to remove track');
|
||||||
|
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.remove', 'Remove'),
|
title: t('music.remove', 'Remove'),
|
||||||
@@ -927,8 +929,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Remove track error:', err);
|
console.error('Remove track error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -940,7 +942,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _reorderTrack(fromIdx, toIdx) {
|
async _reorderTrack(fromIdx, toIdx) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
|
|
||||||
const tracks = [...this.currentTracks];
|
const tracks = [...this.currentTracks];
|
||||||
const [moved] = tracks.splice(fromIdx, 1);
|
const [moved] = tracks.splice(fromIdx, 1);
|
||||||
@@ -959,8 +961,8 @@ const musicView = {
|
|||||||
if (!resp.ok) throw new Error('Failed to reorder tracks');
|
if (!resp.ok) throw new Error('Failed to reorder tracks');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Reorder error:', err);
|
console.error('Reorder error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -973,7 +975,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _showManageSharesDialog() {
|
async _showManageSharesDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
|
|
||||||
const existing = document.getElementById('music-shares-dialog');
|
const existing = document.getElementById('music-shares-dialog');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
@@ -1025,8 +1027,8 @@ const musicView = {
|
|||||||
userInput.value = '';
|
userInput.value = '';
|
||||||
writeInput.checked = false;
|
writeInput.checked = false;
|
||||||
this._loadSharesList(dialog);
|
this._loadSharesList(dialog);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.share', 'Share'),
|
title: t('music.share', 'Share'),
|
||||||
@@ -1034,8 +1036,8 @@ const musicView = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -1050,7 +1052,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _loadSharesList(dialog) {
|
async _loadSharesList(dialog) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
const body = dialog.querySelector('.music-shares-body');
|
const body = dialog.querySelector('.music-shares-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
@@ -1095,7 +1097,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _removeShare(userId, dialog) {
|
async _removeShare(userId, dialog) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
|
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
|
||||||
@@ -1106,8 +1108,8 @@ const musicView = {
|
|||||||
if (!resp.ok) throw new Error('Failed to remove share');
|
if (!resp.ok) throw new Error('Failed to remove share');
|
||||||
this._loadSharesList(dialog);
|
this._loadSharesList(dialog);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -1119,7 +1121,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _togglePublic() {
|
async _togglePublic() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
const newValue = !this.currentPlaylist.is_public;
|
const newValue = !this.currentPlaylist.is_public;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1144,9 +1146,9 @@ const musicView = {
|
|||||||
btn.classList.toggle('active', newValue);
|
btn.classList.toggle('active', newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
|
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.toggle_public', 'Visibility'),
|
title: t('music.toggle_public', 'Visibility'),
|
||||||
@@ -1155,8 +1157,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Toggle public error:', err);
|
console.error('Toggle public error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -1168,7 +1170,7 @@ const musicView = {
|
|||||||
|
|
||||||
async _showCoverPicker() {
|
async _showCoverPicker() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
@@ -1184,13 +1186,13 @@ const musicView = {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const folderId = window.app?.currentPath || window.app?.userHomeFolderId || '';
|
const folderId = app?.currentPath || app?.userHomeFolderId || '';
|
||||||
formData.append('folder_id', folderId);
|
formData.append('folder_id', folderId);
|
||||||
|
|
||||||
const uploadResp = await fetch('/api/files/upload', {
|
const uploadResp = await fetch('/api/files/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: typeof getCsrfHeaders === 'function' ? getCsrfHeaders() : {},
|
headers: getCsrfHeaders(),
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
if (!uploadResp.ok) throw new Error('Upload failed');
|
if (!uploadResp.ok) throw new Error('Upload failed');
|
||||||
@@ -1214,8 +1216,8 @@ const musicView = {
|
|||||||
coverEl.innerHTML = `<img src="/api/files/${encodeURIComponent(uploaded.id)}" alt="" class="music-cover-img"><div class="music-cover-overlay"><i class="fas fa-camera"></i></div>`;
|
coverEl.innerHTML = `<img src="/api/files/${encodeURIComponent(uploaded.id)}" alt="" class="music-cover-img"><div class="music-cover-overlay"><i class="fas fa-camera"></i></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.set_cover', 'Set cover'),
|
title: t('music.set_cover', 'Set cover'),
|
||||||
@@ -1224,8 +1226,8 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Cover upload error:', err);
|
console.error('Cover upload error:', err);
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -1647,11 +1649,11 @@ const musicPlayer = {
|
|||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
this._updateUI();
|
this._updateUI();
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
if (window.notifications) {
|
if (notifications) {
|
||||||
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
|
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
|
||||||
window.notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: t('music.error', 'Error'),
|
||||||
@@ -1670,8 +1672,8 @@ const musicPlayer = {
|
|||||||
if (icon) {
|
if (icon) {
|
||||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||||
const extraClass = 'player-btn-main';
|
const extraClass = 'player-btn-main';
|
||||||
if (window.oxiIcon) {
|
if (oxiIcon) {
|
||||||
icon.outerHTML = window.oxiIcon(iconName, extraClass);
|
icon.outerHTML = oxiIcon(iconName, extraClass);
|
||||||
} else {
|
} else {
|
||||||
icon.className = `fas fa-${iconName} ${extraClass}`;
|
icon.className = `fas fa-${iconName} ${extraClass}`;
|
||||||
}
|
}
|
||||||
@@ -1680,7 +1682,7 @@ const musicPlayer = {
|
|||||||
|
|
||||||
if (trackName) {
|
if (trackName) {
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
trackName.textContent = this.currentTrack
|
trackName.textContent = this.currentTrack
|
||||||
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
|
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
|
||||||
@@ -1704,8 +1706,8 @@ const musicPlayer = {
|
|||||||
if (playIcon) playIcon.classList.remove('hidden');
|
if (playIcon) playIcon.classList.remove('hidden');
|
||||||
if (playIcon) {
|
if (playIcon) {
|
||||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||||
if (window.oxiIcon) {
|
if (oxiIcon) {
|
||||||
playIcon.outerHTML = window.oxiIcon(iconName, 'track-play-icon');
|
playIcon.outerHTML = oxiIcon(iconName, 'track-play-icon');
|
||||||
} else {
|
} else {
|
||||||
playIcon.className = `fas fa-${iconName} track-play-icon`;
|
playIcon.className = `fas fa-${iconName} track-play-icon`;
|
||||||
}
|
}
|
||||||
@@ -1735,7 +1737,7 @@ const musicPlayer = {
|
|||||||
if (!queueList) return;
|
if (!queueList) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
const t = (key, fallback = '') => {
|
||||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.queue.length === 0) {
|
if (this.queue.length === 0) {
|
||||||
@@ -1838,4 +1840,4 @@ const musicPlayer = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.musicView = musicView;
|
export { musicView };
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* Photo grid grouped by day/month/year, with infinite scroll and multi-select.
|
* Photo grid grouped by day/month/year, with infinite scroll and multi-select.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { photosLightbox } from './photosLightbox.js';
|
||||||
|
|
||||||
const photosView = {
|
const photosView = {
|
||||||
/** @type {Array} All loaded photo items */
|
/** @type {Array} All loaded photo items */
|
||||||
items: [],
|
items: [],
|
||||||
@@ -37,7 +41,7 @@ const photosView = {
|
|||||||
|
|
||||||
/** Auth headers (HttpOnly cookies) */
|
/** Auth headers (HttpOnly cookies) */
|
||||||
_headers(json = false) {
|
_headers(json = false) {
|
||||||
const h = typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
const h = getCsrfHeaders();
|
||||||
if (json) h['Content-Type'] = 'application/json';
|
if (json) h['Content-Type'] = 'application/json';
|
||||||
return h;
|
return h;
|
||||||
},
|
},
|
||||||
@@ -406,7 +410,7 @@ const photosView = {
|
|||||||
|
|
||||||
/** 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) => (i18n ? 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')],
|
||||||
@@ -423,7 +427,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) => (i18n ? 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>
|
||||||
@@ -483,8 +487,8 @@ 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.open(this.items, idx);
|
photosLightbox.open(this.items, idx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -516,7 +520,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) => (i18n ? 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>
|
||||||
@@ -605,4 +609,6 @@ const photosView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.photosView = photosView;
|
photosLightbox.setPhotosView(photosView);
|
||||||
|
|
||||||
|
export { photosView };
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
* Full-screen image/video viewer with prev/next navigation.
|
* Full-screen image/video viewer with prev/next navigation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const photosLightbox = {
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { favorites } from '../library/favorites.js';
|
||||||
|
|
||||||
|
export const photosLightbox = {
|
||||||
/** @type {Array} Items array reference */
|
/** @type {Array} Items array reference */
|
||||||
items: [],
|
items: [],
|
||||||
/** @type {number} Current index */
|
/** @type {number} Current index */
|
||||||
@@ -14,10 +17,20 @@ const photosLightbox = {
|
|||||||
_blobUrl: null,
|
_blobUrl: null,
|
||||||
/** @type {Function|null} */
|
/** @type {Function|null} */
|
||||||
_keyHandler: null,
|
_keyHandler: null,
|
||||||
|
/** @type {Object|null} Reference to photosView, set after both modules load */
|
||||||
|
_photosView: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the photosView reference (called from photos.js to avoid circular imports).
|
||||||
|
* @param {Object} pv
|
||||||
|
*/
|
||||||
|
setPhotosView(pv) {
|
||||||
|
this._photosView = pv;
|
||||||
|
},
|
||||||
|
|
||||||
/** Auth headers */
|
/** Auth headers */
|
||||||
_headers() {
|
_headers() {
|
||||||
return typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
return getCsrfHeaders();
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Open lightbox at given index */
|
/** Open lightbox at given index */
|
||||||
@@ -202,7 +215,7 @@ const photosLightbox = {
|
|||||||
/** Toggle favorite on current item */
|
/** Toggle favorite on current item */
|
||||||
async _toggleFavorite() {
|
async _toggleFavorite() {
|
||||||
const item = this.items[this.index];
|
const item = this.items[this.index];
|
||||||
if (!item || !window.favorites) return;
|
if (!item || !favorites) return;
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/favorites/file/${item.id}`, {
|
await fetch(`/api/favorites/file/${item.id}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -235,17 +248,17 @@ const photosLightbox = {
|
|||||||
headers: this._headers()
|
headers: this._headers()
|
||||||
});
|
});
|
||||||
// Remove from photosView items too
|
// Remove from photosView items too
|
||||||
if (window.photosView) {
|
if (this._photosView) {
|
||||||
window.photosView.items = window.photosView.items.filter((f) => f.id !== item.id);
|
this._photosView.items = this._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) {
|
||||||
this.close();
|
this.close();
|
||||||
if (window.photosView) window.photosView._render();
|
if (this._photosView) this._photosView._render();
|
||||||
} else {
|
} else {
|
||||||
if (this.index >= this.items.length) this.index = this.items.length - 1;
|
if (this.index >= this.items.length) this.index = this.items.length - 1;
|
||||||
this._show();
|
this._show();
|
||||||
if (window.photosView) window.photosView._render();
|
if (this._photosView) this._photosView._render();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Delete failed:', err);
|
console.error('Delete failed:', err);
|
||||||
@@ -282,5 +295,3 @@ const photosLightbox = {
|
|||||||
.replace(/</g, '<');
|
.replace(/</g, '<');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.photosLightbox = photosLightbox;
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
* No localStorage usage — the server persists and prunes recent items.
|
* No localStorage usage — the server persists and prunes recent items.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { multiSelect } from '../files/multiSelect.js';
|
||||||
|
|
||||||
const recent = {
|
const recent = {
|
||||||
/** Maximum items to request from the server */
|
/** Maximum items to request from the server */
|
||||||
MAX_RECENT_FILES: 20,
|
MAX_RECENT_FILES: 20,
|
||||||
@@ -86,7 +91,7 @@ const recent = {
|
|||||||
|
|
||||||
const recentItems = await response.json();
|
const recentItems = await response.json();
|
||||||
|
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
|
|
||||||
filesList.innerHTML = `
|
filesList.innerHTML = `
|
||||||
@@ -100,17 +105,17 @@ const recent = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.multiSelect) {
|
if (multiSelect) {
|
||||||
window.multiSelect.clear();
|
multiSelect.clear();
|
||||||
window.multiSelect.init(); // this will wire buttons & select-all-checkbox
|
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
window.ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
|
|
||||||
if (recentItems.length === 0) {
|
if (recentItems.length === 0) {
|
||||||
window.ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-clock empty-state-icon"></i>
|
<i class="fas fa-clock empty-state-icon"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
|
<p>${i18n ? i18n.t('recent.empty_state') : 'No recent files'}</p>
|
||||||
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
<p>${i18n ? i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,16 +145,15 @@ const recent = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (folders.length) window.ui.renderFolders(folders);
|
if (folders.length) ui.renderFolders(folders);
|
||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying recent files:', error);
|
console.error('Error displaying recent files:', error);
|
||||||
if (window.ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading recent files');
|
ui.showNotification('Error', 'Error loading recent files');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose globally
|
export { recent };
|
||||||
window.recent = recent;
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
* No localStorage is used for share data.
|
* No localStorage is used for share data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { switchToSharedSection } from '../../app/navigation.js';
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { formatDateTime } from '../../core/formatters.js';
|
||||||
|
|
||||||
const fileSharing = {
|
const fileSharing = {
|
||||||
/** Auth header helper — tokens are in HttpOnly cookies now */
|
/** Auth header helper — tokens are in HttpOnly cookies now */
|
||||||
_headers(json = true) {
|
_headers(json = true) {
|
||||||
@@ -149,11 +154,11 @@ const fileSharing = {
|
|||||||
async copyLinkToClipboard(url) {
|
async copyLinkToClipboard(url) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
window.ui.showNotification('Link copied', 'Link copied to clipboard');
|
ui.showNotification('Link copied', 'Link copied to clipboard');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying to clipboard:', error);
|
console.error('Error copying to clipboard:', error);
|
||||||
window.ui.showNotification('Error', 'Could not copy link');
|
ui.showNotification('Error', 'Could not copy link');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,7 +170,7 @@ const fileSharing = {
|
|||||||
*/
|
*/
|
||||||
formatExpirationDate(value) {
|
formatExpirationDate(value) {
|
||||||
if (!value) return 'No expiration';
|
if (!value) return 'No expiration';
|
||||||
return window.formatDateTime(value);
|
return formatDateTime(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,9 +183,7 @@ const fileSharing = {
|
|||||||
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
||||||
// TODO: implement backend endpoint for email notifications
|
// TODO: implement backend endpoint for email notifications
|
||||||
console.log(`Share notification for ${shareUrl} sent to ${recipientEmail}`);
|
console.log(`Share notification for ${shareUrl} sent to ${recipientEmail}`);
|
||||||
if (window.ui) {
|
ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
|
||||||
window.ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -193,20 +196,11 @@ const fileSharing = {
|
|||||||
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', () => {
|
||||||
if (window.switchToSharedSection) {
|
switchToSharedSection();
|
||||||
window.switchToSharedSection();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose module globally
|
export { fileSharing };
|
||||||
window.fileSharing = fileSharing;
|
|
||||||
|
|
||||||
// Global convenience functions that delegate to the module
|
|
||||||
window.getSharedLinks = () => fileSharing.getSharedLinks();
|
|
||||||
window.updateSharedLink = (id, data) => fileSharing.updateSharedLink(id, data);
|
|
||||||
window.removeSharedLink = (id) => fileSharing.removeSharedLink(id);
|
|
||||||
window.sendShareNotification = (url, email, msg) => fileSharing.sendShareNotification(url, email, msg);
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { escapeHtml } from '../../core/formatters.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
const API = '/api';
|
const API = '/api';
|
||||||
let currentAdminId = '';
|
let currentAdminId = '';
|
||||||
let usersPage = 0;
|
let usersPage = 0;
|
||||||
@@ -6,7 +10,7 @@ let totalUsers = 0;
|
|||||||
|
|
||||||
/* ── i18n helper — falls back to key if i18n not ready ── */
|
/* ── i18n helper — falls back to key if i18n not ready ── */
|
||||||
function t(key, params) {
|
function t(key, params) {
|
||||||
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
|
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
|
||||||
// fallback: strip prefix and humanise
|
// fallback: strip prefix and humanise
|
||||||
return key.split('.').pop().replace(/_/g, ' ');
|
return key.split('.').pop().replace(/_/g, ' ');
|
||||||
}
|
}
|
||||||
@@ -748,13 +752,13 @@ function showAccessDenied() {
|
|||||||
|
|
||||||
/* ── Apply i18n when translations load / change ── */
|
/* ── Apply i18n when translations load / change ── */
|
||||||
document.addEventListener('translationsLoaded', () => {
|
document.addEventListener('translationsLoaded', () => {
|
||||||
if (window.i18n?.translatePage) window.i18n.translatePage();
|
if (i18n?.translatePage) 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', () => {
|
document.addEventListener('localeChanged', () => {
|
||||||
if (window.i18n?.translatePage) window.i18n.translatePage();
|
if (i18n?.translatePage) i18n.translatePage();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// device-verify.js — Extracted from inline <script> in device-verify.html
|
// device-verify.js — Extracted from inline <script> in device-verify.html
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
var API_BASE = window.location.origin;
|
var API_BASE = window.location.origin;
|
||||||
var codeInput = document.getElementById('user-code');
|
var codeInput = document.getElementById('user-code');
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
const API = '/api';
|
const API = '/api';
|
||||||
|
|
||||||
/* ── i18n helper — falls back to key if i18n not ready ── */
|
/* ── i18n helper — falls back to key if i18n not ready ── */
|
||||||
function t(key, params) {
|
function t(key, params) {
|
||||||
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
|
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
|
||||||
return key.split('.').pop().replace(/_/g, ' ');
|
return key.split('.').pop().replace(/_/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
* In-app shared files view. All operations go through the backend API.
|
* In-app shared files view. All operations go through the backend API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { switchToFilesSection } from '../../app/navigation.js';
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { formatDateShort } from '../../core/formatters.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { fileSharing } from '../../features/sharing/fileSharing.js';
|
||||||
|
|
||||||
const sharedView = {
|
const sharedView = {
|
||||||
// State
|
// State
|
||||||
items: [],
|
items: [],
|
||||||
@@ -181,8 +188,8 @@ const sharedView = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.i18n?.translateElement) {
|
if (i18n?.translateElement) {
|
||||||
window.i18n.translateElement(container);
|
i18n.translateElement(container);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -246,7 +253,7 @@ const sharedView = {
|
|||||||
const goToFilesBtn = document.getElementById('go-to-files-btn');
|
const goToFilesBtn = document.getElementById('go-to-files-btn');
|
||||||
if (goToFilesBtn) {
|
if (goToFilesBtn) {
|
||||||
goToFilesBtn.addEventListener('click', () => {
|
goToFilesBtn.addEventListener('click', () => {
|
||||||
if (window.switchToFilesSection) window.switchToFilesSection();
|
if (switchToFilesSection) switchToFilesSection();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -590,8 +597,8 @@ const sharedView = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.fileSharing?.sendShareNotification) {
|
if (fileSharing?.sendShareNotification) {
|
||||||
window.fileSharing
|
fileSharing
|
||||||
.sendShareNotification(this.currentItem.url, email, message)
|
.sendShareNotification(this.currentItem.url, email, message)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.closeNotificationDialog();
|
this.closeNotificationDialog();
|
||||||
@@ -602,8 +609,8 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showNotification(message, type = 'success') {
|
showNotification(message, type = 'success') {
|
||||||
if (window.ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
window.ui.showNotification(message, type);
|
ui.showNotification(message, type);
|
||||||
} else {
|
} else {
|
||||||
alert(message);
|
alert(message);
|
||||||
}
|
}
|
||||||
@@ -614,13 +621,13 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
formatDate(value) {
|
formatDate(value) {
|
||||||
return window.formatDateShort ? window.formatDateShort(value) : String(value);
|
return formatDateShort(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(key, defaultText) {
|
translate(key, defaultText) {
|
||||||
if (window.i18n?.t) return window.i18n.t(key, defaultText);
|
if (i18n?.t) return i18n.t(key, defaultText);
|
||||||
return defaultText;
|
return defaultText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.sharedView = sharedView;
|
export { sharedView };
|
||||||
|
|||||||
+1
-4
@@ -12,10 +12,7 @@
|
|||||||
<link rel="stylesheet" href="/css/views/auth.css">
|
<link rel="stylesheet" href="/css/views/auth.css">
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/js/core/i18n.js"></script>
|
<script type="module" src="/js/features/auth/auth.js"></script>
|
||||||
<script src="/js/core/csrf.js"></script>
|
|
||||||
<script src="/js/core/icons.js" defer></script>
|
|
||||||
<script src="/js/features/auth/auth.js" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
|
|||||||
+1
-4
@@ -5,9 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>OxiCloud — My Profile</title>
|
<title>OxiCloud — My Profile</title>
|
||||||
<script src="/js/core/theme-init.js"></script>
|
<script src="/js/core/theme-init.js"></script>
|
||||||
<script src="/js/core/i18n.js" defer></script>
|
|
||||||
<script src="/js/core/icons.js" defer></script>
|
|
||||||
<script src="/js/core/csrf.js" defer></script>
|
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
<link rel="stylesheet" href="/css/views/profile.css">
|
<link rel="stylesheet" href="/css/views/profile.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -164,6 +161,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/views/profile/profile.js" defer></script>
|
<script type="module" src="/js/views/profile/profile.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+20
-7
@@ -1,12 +1,14 @@
|
|||||||
// OxiCloud Service Worker
|
// OxiCloud Service Worker
|
||||||
const CACHE_NAME = 'oxicloud-cache-v16';
|
const CACHE_NAME = 'oxicloud-cache-v17';
|
||||||
|
|
||||||
|
// Only cache static assets — NOT HTML files.
|
||||||
|
// HTML files are served network-first so browsers always get the latest
|
||||||
|
// script/link references. Caching HTML causes stale entry points.
|
||||||
const ASSETS_TO_CACHE = [
|
const ASSETS_TO_CACHE = [
|
||||||
'/',
|
'/js/core/icons.js',
|
||||||
'/index.html',
|
'/js/core/i18n.js',
|
||||||
'/js/icons.js',
|
'/js/core/languageSelector.js',
|
||||||
'/js/i18n.js',
|
'/js/core/notifications.js',
|
||||||
'/js/languageSelector.js',
|
|
||||||
'/js/notifications.js',
|
|
||||||
'/locales/en.json',
|
'/locales/en.json',
|
||||||
'/locales/es.json',
|
'/locales/es.json',
|
||||||
'/locales/fa.json',
|
'/locales/fa.json',
|
||||||
@@ -15,6 +17,9 @@ const ASSETS_TO_CACHE = [
|
|||||||
'/favicon.ico'
|
'/favicon.ico'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// HTML paths that should always be fetched from the network
|
||||||
|
const HTML_PATHS = ['/', '/index.html', '/login', '/login.html', '/admin', '/admin.html', '/profile', '/profile.html'];
|
||||||
|
|
||||||
// Install event - cache assets
|
// Install event - cache assets
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
@@ -55,6 +60,14 @@ self.addEventListener('fetch', (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML pages: always network-first so entry points are never stale
|
||||||
|
const pathname = new URL(event.request.url).pathname;
|
||||||
|
const isHtml = HTML_PATHS.includes(pathname) || pathname.endsWith('.html') || event.request.headers.get('accept')?.includes('text/html');
|
||||||
|
if (isHtml) {
|
||||||
|
event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request).then((response) => {
|
caches.match(event.request).then((response) => {
|
||||||
// Cache hit - return the response from the cached version
|
// Cache hit - return the response from the cached version
|
||||||
|
|||||||
Reference in New Issue
Block a user