@burdenoff/microfe-files 2026.830.1 → 2026.930.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/files/FilesRoot.d.ts.map +1 -1
- package/dist/files/FilesRoot.js +42 -37
- package/dist/files/FilesRoot.js.map +1 -1
- package/dist/files/components/FileInfoPanel.js.map +1 -1
- package/dist/files/components/SearchBar.js +3 -3
- package/dist/files/components/SearchBar.js.map +1 -1
- package/dist/files/components/ShareDialog.d.ts.map +1 -1
- package/dist/files/components/ShareDialog.js +4 -1
- package/dist/files/components/ShareDialog.js.map +1 -1
- package/dist/files/components/StorageBanner.d.ts.map +1 -1
- package/dist/files/components/StorageBanner.js +50 -41
- package/dist/files/components/StorageBanner.js.map +1 -1
- package/dist/files/components/viewers/AudioPlayer.d.ts.map +1 -1
- package/dist/files/components/viewers/AudioPlayer.js +59 -58
- package/dist/files/components/viewers/AudioPlayer.js.map +1 -1
- package/dist/files/components/viewers/CodeViewer.d.ts.map +1 -1
- package/dist/files/components/viewers/CodeViewer.js +49 -44
- package/dist/files/components/viewers/CodeViewer.js.map +1 -1
- package/dist/files/components/viewers/ImageViewer.d.ts.map +1 -1
- package/dist/files/components/viewers/ImageViewer.js +75 -74
- package/dist/files/components/viewers/ImageViewer.js.map +1 -1
- package/dist/files/components/viewers/PDFViewer.d.ts.map +1 -1
- package/dist/files/components/viewers/PDFViewer.js +44 -41
- package/dist/files/components/viewers/PDFViewer.js.map +1 -1
- package/dist/files/components/viewers/TextViewer.d.ts.map +1 -1
- package/dist/files/components/viewers/TextViewer.js +77 -72
- package/dist/files/components/viewers/TextViewer.js.map +1 -1
- package/dist/files/components/viewers/VideoPlayer.d.ts.map +1 -1
- package/dist/files/components/viewers/VideoPlayer.js +77 -75
- package/dist/files/components/viewers/VideoPlayer.js.map +1 -1
- package/dist/shared/hooks/useTr.d.ts +17 -0
- package/dist/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/dist/translations/en.d.ts +85 -0
- package/dist/translations/en.d.ts.map +1 -1
- package/dist/translations/en.js +84 -1
- package/dist/translations/en.js.map +1 -1
- package/package.json +6 -1
- package/src/styles/dark.css +27 -0
|
@@ -1,131 +1,136 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useTr as e } from "../../../shared/hooks/useTr.js";
|
|
2
|
+
import { useEffect as t, useMemo as n, useState as r } from "react";
|
|
3
|
+
import { Check as i, Copy as a, Search as o, WrapText as s } from "lucide-react";
|
|
4
|
+
import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
4
5
|
//#region src/files/components/viewers/TextViewer.tsx
|
|
5
|
-
function
|
|
6
|
+
function d(e) {
|
|
6
7
|
return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
-
let [
|
|
10
|
-
|
|
11
|
-
fetch(
|
|
12
|
-
|
|
9
|
+
function f({ file: f, url: p }) {
|
|
10
|
+
let m = e(), [h, g] = r(""), [_, v] = r(!0), [y, b] = r(!0), [x, S] = r(""), [C, w] = r(!1), [T, E] = r(!1);
|
|
11
|
+
t(() => {
|
|
12
|
+
fetch(p).then((e) => e.text()).then((e) => {
|
|
13
|
+
g(e), v(!1);
|
|
13
14
|
}).catch(() => {
|
|
14
|
-
|
|
15
|
+
v(!1);
|
|
15
16
|
});
|
|
16
|
-
}, [
|
|
17
|
-
let
|
|
18
|
-
navigator.clipboard.writeText(
|
|
17
|
+
}, [p]);
|
|
18
|
+
let D = () => {
|
|
19
|
+
navigator.clipboard.writeText(h), E(!0);
|
|
19
20
|
};
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
let e = setTimeout(() =>
|
|
21
|
+
t(() => {
|
|
22
|
+
if (T) {
|
|
23
|
+
let e = setTimeout(() => E(!1), 2e3);
|
|
23
24
|
return () => clearTimeout(e);
|
|
24
25
|
}
|
|
25
|
-
}, [
|
|
26
|
-
let
|
|
27
|
-
let e =
|
|
28
|
-
if (!
|
|
26
|
+
}, [T]);
|
|
27
|
+
let O = n(() => {
|
|
28
|
+
let e = h.split("\n");
|
|
29
|
+
if (!x.trim()) return e.map((e) => [{
|
|
29
30
|
text: e,
|
|
30
31
|
isMatch: !1
|
|
31
32
|
}]);
|
|
32
|
-
let t = RegExp(`(${
|
|
33
|
+
let t = RegExp(`(${d(x)})`, "gi");
|
|
33
34
|
return e.map((e) => e.split(t).map((e) => ({
|
|
34
35
|
text: e,
|
|
35
|
-
isMatch: e.toLowerCase() ===
|
|
36
|
+
isMatch: e.toLowerCase() === x.toLowerCase()
|
|
36
37
|
})));
|
|
37
|
-
}, [
|
|
38
|
-
if (!
|
|
39
|
-
let e = new RegExp(
|
|
40
|
-
return (
|
|
41
|
-
}, [
|
|
42
|
-
return
|
|
38
|
+
}, [h, x]), k = n(() => {
|
|
39
|
+
if (!x.trim()) return 0;
|
|
40
|
+
let e = new RegExp(x, "gi");
|
|
41
|
+
return (h.match(e) || []).length;
|
|
42
|
+
}, [h, x]);
|
|
43
|
+
return _ ? /* @__PURE__ */ l("div", {
|
|
43
44
|
className: "flex items-center justify-center h-full bg-bg-canvas",
|
|
44
|
-
children: /* @__PURE__ */
|
|
45
|
+
children: /* @__PURE__ */ l("span", {
|
|
45
46
|
className: "text-text-secondary",
|
|
46
|
-
children: "Loading text..."
|
|
47
|
+
children: m("files.preview.text.loading", "Loading text...")
|
|
47
48
|
})
|
|
48
|
-
}) : /* @__PURE__ */
|
|
49
|
+
}) : /* @__PURE__ */ u("div", {
|
|
49
50
|
className: "flex flex-col h-full bg-bg-canvas",
|
|
50
51
|
children: [
|
|
51
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ u("div", {
|
|
52
53
|
className: "flex items-center justify-between p-3 bg-bg-surface border-b border-border-default",
|
|
53
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ l("div", {
|
|
54
55
|
className: "flex items-center gap-2",
|
|
55
|
-
children: /* @__PURE__ */
|
|
56
|
+
children: /* @__PURE__ */ l("span", {
|
|
56
57
|
className: "text-sm text-text-secondary",
|
|
57
|
-
children:
|
|
58
|
+
children: f.name
|
|
58
59
|
})
|
|
59
|
-
}), /* @__PURE__ */
|
|
60
|
+
}), /* @__PURE__ */ u("div", {
|
|
60
61
|
className: "flex items-center gap-2",
|
|
61
62
|
children: [
|
|
62
|
-
|
|
63
|
+
C && /* @__PURE__ */ u("div", {
|
|
63
64
|
className: "flex items-center gap-2 px-2 py-1 border border-border-default rounded",
|
|
64
65
|
children: [
|
|
65
|
-
/* @__PURE__ */
|
|
66
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ l(o, { className: "size-4 text-text-secondary" }),
|
|
67
|
+
/* @__PURE__ */ l("input", {
|
|
67
68
|
type: "text",
|
|
68
|
-
value:
|
|
69
|
-
onChange: (e) =>
|
|
70
|
-
placeholder: "Search...",
|
|
71
|
-
"aria-label": "Search text",
|
|
69
|
+
value: x,
|
|
70
|
+
onChange: (e) => S(e.target.value),
|
|
71
|
+
placeholder: m("files.preview.text.searchPlaceholder", "Search..."),
|
|
72
|
+
"aria-label": m("files.preview.text.searchAria", "Search text"),
|
|
72
73
|
className: "w-40 bg-transparent text-sm text-text-primary placeholder:text-text-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"
|
|
73
74
|
}),
|
|
74
|
-
|
|
75
|
+
k > 0 && /* @__PURE__ */ u("span", {
|
|
75
76
|
className: "text-xs text-text-secondary",
|
|
76
77
|
children: [
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
|
|
78
|
+
k,
|
|
79
|
+
" ",
|
|
80
|
+
k > 1 ? m("files.preview.text.matches", "matches") : m("files.preview.text.match", "match")
|
|
80
81
|
]
|
|
81
82
|
})
|
|
82
83
|
]
|
|
83
84
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
85
|
+
/* @__PURE__ */ l("button", {
|
|
85
86
|
type: "button",
|
|
86
|
-
onClick: () =>
|
|
87
|
-
className: `p-2 rounded transition-colors duration-200 ${
|
|
88
|
-
title: "Search",
|
|
89
|
-
"aria-label": "Search within text file",
|
|
90
|
-
children: /* @__PURE__ */
|
|
87
|
+
onClick: () => w(!C),
|
|
88
|
+
className: `p-2 rounded transition-colors duration-200 ${C ? "text-text-link bg-bg-accent" : "text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover"}`,
|
|
89
|
+
title: m("files.preview.text.search", "Search"),
|
|
90
|
+
"aria-label": m("files.preview.text.searchWithinFile", "Search within text file"),
|
|
91
|
+
children: /* @__PURE__ */ l(o, { className: "size-4" })
|
|
91
92
|
}),
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ l("button", {
|
|
93
94
|
type: "button",
|
|
94
|
-
onClick: () =>
|
|
95
|
-
className: `p-2 rounded transition-colors duration-200 ${
|
|
96
|
-
title:
|
|
97
|
-
"aria-label":
|
|
98
|
-
children: /* @__PURE__ */
|
|
95
|
+
onClick: () => b(!y),
|
|
96
|
+
className: `p-2 rounded transition-colors duration-200 ${y ? "text-text-link bg-bg-accent" : "text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover"}`,
|
|
97
|
+
title: y ? m("files.preview.text.disableWordWrap", "Disable word wrap") : m("files.preview.text.enableWordWrap", "Enable word wrap"),
|
|
98
|
+
"aria-label": y ? m("files.preview.text.disableWordWrap", "Disable word wrap") : m("files.preview.text.enableWordWrap", "Enable word wrap"),
|
|
99
|
+
children: /* @__PURE__ */ l(s, { className: "size-4" })
|
|
99
100
|
}),
|
|
100
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ l("button", {
|
|
101
102
|
type: "button",
|
|
102
|
-
onClick:
|
|
103
|
+
onClick: D,
|
|
103
104
|
className: "flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium text-text-inverse bg-action-primary-bg transition-colors duration-200 hover:bg-action-primary-bg-hover",
|
|
104
|
-
title: "Copy text",
|
|
105
|
-
"aria-label": "Copy text content",
|
|
106
|
-
children:
|
|
105
|
+
title: m("files.preview.text.copyText", "Copy text"),
|
|
106
|
+
"aria-label": m("files.preview.text.copyTextContent", "Copy text content"),
|
|
107
|
+
children: T ? /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(i, { className: "size-4" }), m("files.preview.text.copied", "Copied!")] }) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(a, { className: "size-4" }), m("files.preview.text.copy", "Copy")] })
|
|
107
108
|
})
|
|
108
109
|
]
|
|
109
110
|
})]
|
|
110
111
|
}),
|
|
111
|
-
/* @__PURE__ */
|
|
112
|
+
/* @__PURE__ */ l("div", {
|
|
112
113
|
className: "flex-1 overflow-auto",
|
|
113
|
-
children: /* @__PURE__ */
|
|
114
|
-
className: `p-4 text-sm font-mono leading-relaxed text-text-primary ${
|
|
115
|
-
children:
|
|
114
|
+
children: /* @__PURE__ */ l("pre", {
|
|
115
|
+
className: `p-4 text-sm font-mono leading-relaxed text-text-primary ${y ? "whitespace-pre-wrap" : "whitespace-pre"}`,
|
|
116
|
+
children: O.map((e, t) => /* @__PURE__ */ l("div", { children: e.length === 0 ? " " : e.map((e, n) => e.isMatch ? /* @__PURE__ */ l("mark", {
|
|
116
117
|
className: "bg-status-warning-bg text-text-primary",
|
|
117
118
|
children: e.text
|
|
118
|
-
}, `${t}-${n}`) : /* @__PURE__ */
|
|
119
|
+
}, `${t}-${n}`) : /* @__PURE__ */ l("span", { children: e.text || " " }, `${t}-${n}`)) }, t))
|
|
119
120
|
})
|
|
120
121
|
}),
|
|
121
|
-
/* @__PURE__ */
|
|
122
|
+
/* @__PURE__ */ u("div", {
|
|
122
123
|
className: "flex items-center justify-between p-3 bg-bg-surface border-t border-border-default text-xs text-text-secondary",
|
|
123
|
-
children: [/* @__PURE__ */
|
|
124
|
+
children: [/* @__PURE__ */ u("span", { children: [
|
|
125
|
+
h.split("\n").length,
|
|
126
|
+
" ",
|
|
127
|
+
m("files.preview.text.lines", "lines")
|
|
128
|
+
] }), /* @__PURE__ */ u("span", { children: [(f.size / 1024).toFixed(2), " KB"] })]
|
|
124
129
|
})
|
|
125
130
|
]
|
|
126
131
|
});
|
|
127
132
|
}
|
|
128
133
|
//#endregion
|
|
129
|
-
export {
|
|
134
|
+
export { f as TextViewer };
|
|
130
135
|
|
|
131
136
|
//# sourceMappingURL=TextViewer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextViewer.js","names":[],"sources":["../../../../src/files/components/viewers/TextViewer.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { Copy, Check, WrapText, Search } from \"lucide-react\";\nimport type { FileItem } from \"../../types\";\n\ninterface TextViewerProps {\n file: FileItem;\n url: string;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function TextViewer({ file, url }: TextViewerProps) {\n const [content, setContent] = useState(\"\");\n const [loading, setLoading] = useState(true);\n const [wordWrap, setWordWrap] = useState(true);\n const [searchTerm, setSearchTerm] = useState(\"\");\n const [showSearch, setShowSearch] = useState(false);\n const [copied, setCopied] = useState(false);\n\n useEffect(() => {\n fetch(url)\n .then((res) => res.text())\n .then((text) => {\n setContent(text);\n setLoading(false);\n })\n .catch(() => {\n setLoading(false);\n });\n }, [url]);\n\n const handleCopy = () => {\n navigator.clipboard.writeText(content);\n setCopied(true);\n };\n\n // Cleanup timeout on unmount to prevent React state update warning\n useEffect(() => {\n if (copied) {\n const timer = setTimeout(() => setCopied(false), 2000);\n return () => clearTimeout(timer);\n }\n return undefined;\n }, [copied]);\n\n // Highlight search matches\n const highlightedLines = useMemo(() => {\n const lines = content.split(\"\\n\");\n\n if (!searchTerm.trim()) {\n return lines.map((line) => [{ text: line, isMatch: false }]);\n }\n\n const matcher = new RegExp(`(${escapeRegExp(searchTerm)})`, \"gi\");\n return lines.map((line) =>\n line.split(matcher).map((part) => ({\n text: part,\n isMatch: part.toLowerCase() === searchTerm.toLowerCase(),\n })),\n );\n }, [content, searchTerm]);\n\n const matchCount = useMemo(() => {\n if (!searchTerm.trim()) return 0;\n const regex = new RegExp(searchTerm, \"gi\");\n return (content.match(regex) || []).length;\n }, [content, searchTerm]);\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full bg-bg-canvas\">\n <span className=\"text-text-secondary\">Loading text...</span>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full bg-bg-canvas\">\n {/* Toolbar */}\n <div className=\"flex items-center justify-between p-3 bg-bg-surface border-b border-border-default\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">{file.name}</span>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {showSearch && (\n <div className=\"flex items-center gap-2 px-2 py-1 border border-border-default rounded\">\n <Search className=\"size-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n placeholder=\"Search...\"\n aria-label=\"Search text\"\n className=\"w-40 bg-transparent text-sm text-text-primary placeholder:text-text-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n {matchCount > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {matchCount} match{matchCount > 1 ? \"es\" : \"\"}\n </span>\n )}\n </div>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowSearch(!showSearch)}\n className={`p-2 rounded transition-colors duration-200 ${\n showSearch\n ? \"text-text-link bg-bg-accent\"\n : \"text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover\"\n }`}\n title=\"Search\"\n aria-label=\"Search within text file\"\n >\n <Search className=\"size-4\" />\n </button>\n\n <button\n type=\"button\"\n onClick={() => setWordWrap(!wordWrap)}\n className={`p-2 rounded transition-colors duration-200 ${\n wordWrap\n ? \"text-text-link bg-bg-accent\"\n : \"text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover\"\n }`}\n title={wordWrap ? \"Disable word wrap\" : \"Enable word wrap\"}\n aria-label={wordWrap ? \"Disable word wrap\" : \"Enable word wrap\"}\n >\n <WrapText className=\"size-4\" />\n </button>\n\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium text-text-inverse bg-action-primary-bg transition-colors duration-200 hover:bg-action-primary-bg-hover\"\n title=\"Copy text\"\n aria-label=\"Copy text content\"\n >\n {copied ? (\n <>\n <Check className=\"size-4\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-4\" />\n Copy\n </>\n )}\n </button>\n </div>\n </div>\n\n {/* Text Content */}\n <div className=\"flex-1 overflow-auto\">\n <pre\n className={`p-4 text-sm font-mono leading-relaxed text-text-primary ${\n wordWrap ? \"whitespace-pre-wrap\" : \"whitespace-pre\"\n }`}\n >\n {highlightedLines.map((segments, lineIndex) => (\n <div key={lineIndex}>\n {segments.length === 0\n ? \" \"\n : segments.map((segment, partIndex) =>\n segment.isMatch ? (\n <mark\n key={`${lineIndex}-${partIndex}`}\n className=\"bg-status-warning-bg text-text-primary\"\n >\n {segment.text}\n </mark>\n ) : (\n <span key={`${lineIndex}-${partIndex}`}>\n {segment.text || \" \"}\n </span>\n ),\n )}\n </div>\n ))}\n </pre>\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between p-3 bg-bg-surface border-t border-border-default text-xs text-text-secondary\">\n <span>{content.split(\"\\n\").length} lines</span>\n <span>{(file.size / 1024).toFixed(2)} KB</span>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextViewer.js","names":[],"sources":["../../../../src/files/components/viewers/TextViewer.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { Copy, Check, WrapText, Search } from \"lucide-react\";\nimport type { FileItem } from \"../../types\";\nimport { useTr } from \"../../../shared/hooks/useTr\";\n\ninterface TextViewerProps {\n file: FileItem;\n url: string;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function TextViewer({ file, url }: TextViewerProps) {\n const tr = useTr();\n const [content, setContent] = useState(\"\");\n const [loading, setLoading] = useState(true);\n const [wordWrap, setWordWrap] = useState(true);\n const [searchTerm, setSearchTerm] = useState(\"\");\n const [showSearch, setShowSearch] = useState(false);\n const [copied, setCopied] = useState(false);\n\n useEffect(() => {\n fetch(url)\n .then((res) => res.text())\n .then((text) => {\n setContent(text);\n setLoading(false);\n })\n .catch(() => {\n setLoading(false);\n });\n }, [url]);\n\n const handleCopy = () => {\n navigator.clipboard.writeText(content);\n setCopied(true);\n };\n\n // Cleanup timeout on unmount to prevent React state update warning\n useEffect(() => {\n if (copied) {\n const timer = setTimeout(() => setCopied(false), 2000);\n return () => clearTimeout(timer);\n }\n return undefined;\n }, [copied]);\n\n // Highlight search matches\n const highlightedLines = useMemo(() => {\n const lines = content.split(\"\\n\");\n\n if (!searchTerm.trim()) {\n return lines.map((line) => [{ text: line, isMatch: false }]);\n }\n\n const matcher = new RegExp(`(${escapeRegExp(searchTerm)})`, \"gi\");\n return lines.map((line) =>\n line.split(matcher).map((part) => ({\n text: part,\n isMatch: part.toLowerCase() === searchTerm.toLowerCase(),\n })),\n );\n }, [content, searchTerm]);\n\n const matchCount = useMemo(() => {\n if (!searchTerm.trim()) return 0;\n const regex = new RegExp(searchTerm, \"gi\");\n return (content.match(regex) || []).length;\n }, [content, searchTerm]);\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full bg-bg-canvas\">\n <span className=\"text-text-secondary\">\n {tr(\"files.preview.text.loading\", \"Loading text...\")}\n </span>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full bg-bg-canvas\">\n {/* Toolbar */}\n <div className=\"flex items-center justify-between p-3 bg-bg-surface border-b border-border-default\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">{file.name}</span>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {showSearch && (\n <div className=\"flex items-center gap-2 px-2 py-1 border border-border-default rounded\">\n <Search className=\"size-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n placeholder={tr(\n \"files.preview.text.searchPlaceholder\",\n \"Search...\",\n )}\n aria-label={tr(\"files.preview.text.searchAria\", \"Search text\")}\n className=\"w-40 bg-transparent text-sm text-text-primary placeholder:text-text-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n {matchCount > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {matchCount}{\" \"}\n {matchCount > 1\n ? tr(\"files.preview.text.matches\", \"matches\")\n : tr(\"files.preview.text.match\", \"match\")}\n </span>\n )}\n </div>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowSearch(!showSearch)}\n className={`p-2 rounded transition-colors duration-200 ${\n showSearch\n ? \"text-text-link bg-bg-accent\"\n : \"text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover\"\n }`}\n title={tr(\"files.preview.text.search\", \"Search\")}\n aria-label={tr(\n \"files.preview.text.searchWithinFile\",\n \"Search within text file\",\n )}\n >\n <Search className=\"size-4\" />\n </button>\n\n <button\n type=\"button\"\n onClick={() => setWordWrap(!wordWrap)}\n className={`p-2 rounded transition-colors duration-200 ${\n wordWrap\n ? \"text-text-link bg-bg-accent\"\n : \"text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover\"\n }`}\n title={\n wordWrap\n ? tr(\"files.preview.text.disableWordWrap\", \"Disable word wrap\")\n : tr(\"files.preview.text.enableWordWrap\", \"Enable word wrap\")\n }\n aria-label={\n wordWrap\n ? tr(\"files.preview.text.disableWordWrap\", \"Disable word wrap\")\n : tr(\"files.preview.text.enableWordWrap\", \"Enable word wrap\")\n }\n >\n <WrapText className=\"size-4\" />\n </button>\n\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium text-text-inverse bg-action-primary-bg transition-colors duration-200 hover:bg-action-primary-bg-hover\"\n title={tr(\"files.preview.text.copyText\", \"Copy text\")}\n aria-label={tr(\n \"files.preview.text.copyTextContent\",\n \"Copy text content\",\n )}\n >\n {copied ? (\n <>\n <Check className=\"size-4\" />\n {tr(\"files.preview.text.copied\", \"Copied!\")}\n </>\n ) : (\n <>\n <Copy className=\"size-4\" />\n {tr(\"files.preview.text.copy\", \"Copy\")}\n </>\n )}\n </button>\n </div>\n </div>\n\n {/* Text Content */}\n <div className=\"flex-1 overflow-auto\">\n <pre\n className={`p-4 text-sm font-mono leading-relaxed text-text-primary ${\n wordWrap ? \"whitespace-pre-wrap\" : \"whitespace-pre\"\n }`}\n >\n {highlightedLines.map((segments, lineIndex) => (\n <div key={lineIndex}>\n {segments.length === 0\n ? \" \"\n : segments.map((segment, partIndex) =>\n segment.isMatch ? (\n <mark\n key={`${lineIndex}-${partIndex}`}\n className=\"bg-status-warning-bg text-text-primary\"\n >\n {segment.text}\n </mark>\n ) : (\n <span key={`${lineIndex}-${partIndex}`}>\n {segment.text || \" \"}\n </span>\n ),\n )}\n </div>\n ))}\n </pre>\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between p-3 bg-bg-surface border-t border-border-default text-xs text-text-secondary\">\n <span>\n {content.split(\"\\n\").length} {tr(\"files.preview.text.lines\", \"lines\")}\n </span>\n <span>{(file.size / 1024).toFixed(2)} KB</span>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../src/files/components/viewers/VideoPlayer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../src/files/components/viewers/VideoPlayer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,UAAU,gBAAgB;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,gBAAgB,+BAuN1D"}
|
|
@@ -1,138 +1,140 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useTr as e } from "../../../shared/hooks/useTr.js";
|
|
2
|
+
import { useEffect as t, useRef as n, useState as r } from "react";
|
|
3
|
+
import { Maximize as i, Pause as a, Play as o, Volume2 as s, VolumeX as c } from "lucide-react";
|
|
4
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
4
5
|
//#region src/files/components/viewers/VideoPlayer.tsx
|
|
5
|
-
function
|
|
6
|
-
let
|
|
7
|
-
|
|
8
|
-
let e =
|
|
6
|
+
function d({ file: d, url: f }) {
|
|
7
|
+
let p = e(), m = n(null), [h, g] = r(!1), [_, v] = r(0), [y, b] = r(0), [x, S] = r(1), [C, w] = r(!1), [T, E] = r(1);
|
|
8
|
+
t(() => {
|
|
9
|
+
let e = m.current;
|
|
9
10
|
if (!e) return;
|
|
10
|
-
let t = () =>
|
|
11
|
+
let t = () => v(e.currentTime), n = () => b(e.duration), r = () => g(!1);
|
|
11
12
|
return e.addEventListener("timeupdate", t), e.addEventListener("loadedmetadata", n), e.addEventListener("ended", r), () => {
|
|
12
13
|
e.removeEventListener("timeupdate", t), e.removeEventListener("loadedmetadata", n), e.removeEventListener("ended", r);
|
|
13
14
|
};
|
|
14
15
|
}, []);
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
}, D = (e) => {
|
|
20
|
-
b(e), S(e === 0), f.current && (f.current.volume = e);
|
|
21
|
-
}, O = () => {
|
|
22
|
-
if (f.current) {
|
|
23
|
-
let e = !x;
|
|
24
|
-
S(e), f.current.muted = e;
|
|
25
|
-
}
|
|
16
|
+
let D = () => {
|
|
17
|
+
m.current && (h ? m.current.pause() : m.current.play(), g(!h));
|
|
18
|
+
}, O = (e) => {
|
|
19
|
+
m.current && (m.current.currentTime = e, v(e));
|
|
26
20
|
}, k = (e) => {
|
|
27
|
-
w(e),
|
|
21
|
+
S(e), w(e === 0), m.current && (m.current.volume = e);
|
|
28
22
|
}, A = () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
if (m.current) {
|
|
24
|
+
let e = !C;
|
|
25
|
+
w(e), m.current.muted = e;
|
|
26
|
+
}
|
|
27
|
+
}, j = (e) => {
|
|
28
|
+
E(e), m.current && (m.current.playbackRate = e);
|
|
29
|
+
}, M = () => {
|
|
30
|
+
m.current && m.current.requestFullscreen();
|
|
31
|
+
}, N = (e) => `${Math.floor(e / 60)}:${Math.floor(e % 60).toString().padStart(2, "0")}`;
|
|
32
|
+
return /* @__PURE__ */ u("div", {
|
|
32
33
|
className: "flex h-full flex-col bg-bg-canvas",
|
|
33
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ l("div", {
|
|
34
35
|
className: "flex-1 flex items-center justify-center",
|
|
35
|
-
children: /* @__PURE__ */
|
|
36
|
-
ref:
|
|
37
|
-
src:
|
|
38
|
-
"aria-label":
|
|
36
|
+
children: /* @__PURE__ */ l("video", {
|
|
37
|
+
ref: m,
|
|
38
|
+
src: f,
|
|
39
|
+
"aria-label": d.name,
|
|
39
40
|
className: "max-w-full max-h-full",
|
|
40
|
-
onClick:
|
|
41
|
-
children: /* @__PURE__ */
|
|
41
|
+
onClick: D,
|
|
42
|
+
children: /* @__PURE__ */ l("track", { kind: "captions" })
|
|
42
43
|
})
|
|
43
|
-
}), /* @__PURE__ */
|
|
44
|
+
}), /* @__PURE__ */ u("div", {
|
|
44
45
|
className: "bg-bg-surface border-t border-border-default p-4",
|
|
45
|
-
children: [/* @__PURE__ */
|
|
46
|
+
children: [/* @__PURE__ */ u("div", {
|
|
46
47
|
className: "mb-4",
|
|
47
|
-
children: [/* @__PURE__ */
|
|
48
|
+
children: [/* @__PURE__ */ l("input", {
|
|
48
49
|
type: "range",
|
|
49
50
|
min: 0,
|
|
50
|
-
max:
|
|
51
|
-
value:
|
|
52
|
-
onChange: (e) =>
|
|
53
|
-
"aria-label": "Seek",
|
|
51
|
+
max: y || 0,
|
|
52
|
+
value: _,
|
|
53
|
+
onChange: (e) => O(Number(e.target.value)),
|
|
54
|
+
"aria-label": p("files.preview.video.seek", "Seek"),
|
|
54
55
|
className: "w-full h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg"
|
|
55
|
-
}), /* @__PURE__ */
|
|
56
|
+
}), /* @__PURE__ */ u("div", {
|
|
56
57
|
className: "flex justify-between mt-1 text-xs text-text-secondary",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
58
|
+
children: [/* @__PURE__ */ l("span", { children: N(_) }), /* @__PURE__ */ l("span", { children: N(y) })]
|
|
58
59
|
})]
|
|
59
|
-
}), /* @__PURE__ */
|
|
60
|
+
}), /* @__PURE__ */ u("div", {
|
|
60
61
|
className: "flex items-center justify-between",
|
|
61
|
-
children: [/* @__PURE__ */
|
|
62
|
+
children: [/* @__PURE__ */ u("div", {
|
|
62
63
|
className: "flex items-center gap-2",
|
|
63
64
|
children: [
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
+
/* @__PURE__ */ l("button", {
|
|
65
66
|
type: "button",
|
|
66
|
-
onClick:
|
|
67
|
+
onClick: D,
|
|
67
68
|
className: "p-2 text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200",
|
|
68
|
-
title:
|
|
69
|
-
"aria-label":
|
|
70
|
-
children:
|
|
69
|
+
title: h ? p("files.preview.video.pause", "Pause") : p("files.preview.video.play", "Play"),
|
|
70
|
+
"aria-label": h ? p("files.preview.video.pauseVideo", "Pause video") : p("files.preview.video.playVideo", "Play video"),
|
|
71
|
+
children: l(h ? a : o, { className: "size-5" })
|
|
71
72
|
}),
|
|
72
|
-
/* @__PURE__ */
|
|
73
|
+
/* @__PURE__ */ u("div", {
|
|
73
74
|
className: "flex items-center gap-2",
|
|
74
|
-
children: [/* @__PURE__ */
|
|
75
|
+
children: [/* @__PURE__ */ l("button", {
|
|
75
76
|
type: "button",
|
|
76
|
-
onClick:
|
|
77
|
+
onClick: A,
|
|
77
78
|
className: "p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200",
|
|
78
|
-
title:
|
|
79
|
-
"aria-label":
|
|
80
|
-
children:
|
|
81
|
-
}), /* @__PURE__ */
|
|
79
|
+
title: C ? p("files.preview.video.unmute", "Unmute") : p("files.preview.video.mute", "Mute"),
|
|
80
|
+
"aria-label": C ? p("files.preview.video.unmuteVideo", "Unmute video") : p("files.preview.video.muteVideo", "Mute video"),
|
|
81
|
+
children: l(C ? c : s, { className: "size-5" })
|
|
82
|
+
}), /* @__PURE__ */ l("input", {
|
|
82
83
|
type: "range",
|
|
83
84
|
min: 0,
|
|
84
85
|
max: 1,
|
|
85
86
|
step: .1,
|
|
86
|
-
value:
|
|
87
|
-
onChange: (e) =>
|
|
88
|
-
"aria-label": "Volume",
|
|
87
|
+
value: x,
|
|
88
|
+
onChange: (e) => k(Number(e.target.value)),
|
|
89
|
+
"aria-label": p("files.preview.video.volume", "Volume"),
|
|
89
90
|
className: "w-20 h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg"
|
|
90
91
|
})]
|
|
91
92
|
}),
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/* @__PURE__ */ u("select", {
|
|
94
|
+
"aria-label": p("files.preview.video.playbackRate", "Playback speed"),
|
|
95
|
+
value: T,
|
|
96
|
+
onChange: (e) => j(Number(e.target.value)),
|
|
97
|
+
className: "px-2 py-1 text-sm bg-bg-surface border border-border-default rounded text-text-primary scheme-normal dark:scheme-dark cursor-pointer [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
96
98
|
children: [
|
|
97
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ l("option", {
|
|
98
100
|
value: "0.5",
|
|
99
101
|
children: "0.5x"
|
|
100
102
|
}),
|
|
101
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ l("option", {
|
|
102
104
|
value: "0.75",
|
|
103
105
|
children: "0.75x"
|
|
104
106
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ l("option", {
|
|
106
108
|
value: "1",
|
|
107
109
|
children: "1x"
|
|
108
110
|
}),
|
|
109
|
-
/* @__PURE__ */
|
|
111
|
+
/* @__PURE__ */ l("option", {
|
|
110
112
|
value: "1.25",
|
|
111
113
|
children: "1.25x"
|
|
112
114
|
}),
|
|
113
|
-
/* @__PURE__ */
|
|
115
|
+
/* @__PURE__ */ l("option", {
|
|
114
116
|
value: "1.5",
|
|
115
117
|
children: "1.5x"
|
|
116
118
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ l("option", {
|
|
118
120
|
value: "2",
|
|
119
121
|
children: "2x"
|
|
120
122
|
})
|
|
121
123
|
]
|
|
122
124
|
})
|
|
123
125
|
]
|
|
124
|
-
}), /* @__PURE__ */
|
|
126
|
+
}), /* @__PURE__ */ u("div", {
|
|
125
127
|
className: "flex items-center gap-2",
|
|
126
|
-
children: [/* @__PURE__ */
|
|
128
|
+
children: [/* @__PURE__ */ l("span", {
|
|
127
129
|
className: "text-sm text-text-secondary",
|
|
128
|
-
children:
|
|
129
|
-
}), /* @__PURE__ */
|
|
130
|
+
children: d.name
|
|
131
|
+
}), /* @__PURE__ */ l("button", {
|
|
130
132
|
type: "button",
|
|
131
|
-
onClick:
|
|
133
|
+
onClick: M,
|
|
132
134
|
className: "p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200",
|
|
133
|
-
title: "Fullscreen",
|
|
134
|
-
"aria-label": "Open fullscreen",
|
|
135
|
-
children: /* @__PURE__ */
|
|
135
|
+
title: p("files.preview.video.fullscreen", "Fullscreen"),
|
|
136
|
+
"aria-label": p("files.preview.video.openFullscreen", "Open fullscreen"),
|
|
137
|
+
children: /* @__PURE__ */ l(i, { className: "size-5" })
|
|
136
138
|
})]
|
|
137
139
|
})]
|
|
138
140
|
})]
|
|
@@ -140,6 +142,6 @@ function u({ file: u, url: d }) {
|
|
|
140
142
|
});
|
|
141
143
|
}
|
|
142
144
|
//#endregion
|
|
143
|
-
export {
|
|
145
|
+
export { d as VideoPlayer };
|
|
144
146
|
|
|
145
147
|
//# sourceMappingURL=VideoPlayer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.js","names":[],"sources":["../../../../src/files/components/viewers/VideoPlayer.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { Play, Pause, Volume2, VolumeX, Maximize } from \"lucide-react\";\nimport type { FileItem } from \"../../types\";\n\ninterface VideoPlayerProps {\n file: FileItem;\n url: string;\n}\n\nexport function VideoPlayer({ file, url }: VideoPlayerProps) {\n const videoRef = useRef<HTMLVideoElement>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [volume, setVolume] = useState(1);\n const [isMuted, setIsMuted] = useState(false);\n const [playbackRate, setPlaybackRate] = useState(1);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleTimeUpdate = () => setCurrentTime(video.currentTime);\n const handleLoadedMetadata = () => setDuration(video.duration);\n const handleEnded = () => setIsPlaying(false);\n\n video.addEventListener(\"timeupdate\", handleTimeUpdate);\n video.addEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.addEventListener(\"ended\", handleEnded);\n\n return () => {\n video.removeEventListener(\"timeupdate\", handleTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.removeEventListener(\"ended\", handleEnded);\n };\n }, []);\n\n const togglePlay = () => {\n if (videoRef.current) {\n if (isPlaying) {\n videoRef.current.pause();\n } else {\n videoRef.current.play();\n }\n setIsPlaying(!isPlaying);\n }\n };\n\n const handleSeek = (time: number) => {\n if (videoRef.current) {\n videoRef.current.currentTime = time;\n setCurrentTime(time);\n }\n };\n\n const handleVolumeChange = (newVolume: number) => {\n setVolume(newVolume);\n setIsMuted(newVolume === 0);\n if (videoRef.current) {\n videoRef.current.volume = newVolume;\n }\n };\n\n const toggleMute = () => {\n if (videoRef.current) {\n const newMuted = !isMuted;\n setIsMuted(newMuted);\n videoRef.current.muted = newMuted;\n }\n };\n\n const handlePlaybackRateChange = (rate: number) => {\n setPlaybackRate(rate);\n if (videoRef.current) {\n videoRef.current.playbackRate = rate;\n }\n };\n\n const handleFullscreen = () => {\n if (videoRef.current) {\n videoRef.current.requestFullscreen();\n }\n };\n\n const formatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n };\n\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n {/* Video */}\n <div className=\"flex-1 flex items-center justify-center\">\n <video\n ref={videoRef}\n src={url}\n aria-label={file.name}\n className=\"max-w-full max-h-full\"\n onClick={togglePlay}\n >\n <track kind=\"captions\" />\n </video>\n </div>\n\n {/* Controls */}\n <div className=\"bg-bg-surface border-t border-border-default p-4\">\n {/* Progress Bar */}\n <div className=\"mb-4\">\n <input\n type=\"range\"\n min={0}\n max={duration || 0}\n value={currentTime}\n onChange={(e) => handleSeek(Number(e.target.value))}\n aria-label=\"Seek\"\n className=\"w-full h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg\"\n />\n <div className=\"flex justify-between mt-1 text-xs text-text-secondary\">\n <span>{formatTime(currentTime)}</span>\n <span>{formatTime(duration)}</span>\n </div>\n </div>\n\n {/* Control Buttons */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n {/* Play/Pause */}\n <button\n type=\"button\"\n onClick={togglePlay}\n className=\"p-2 text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title={isPlaying ? \"Pause\" : \"Play\"}\n aria-label={isPlaying ? \"Pause video\" : \"Play video\"}\n >\n {isPlaying ? (\n <Pause className=\"size-5\" />\n ) : (\n <Play className=\"size-5\" />\n )}\n </button>\n\n {/* Volume */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={toggleMute}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title={isMuted ? \"Unmute\" : \"Mute\"}\n aria-label={isMuted ? \"Unmute video\" : \"Mute video\"}\n >\n {isMuted ? (\n <VolumeX className=\"size-5\" />\n ) : (\n <Volume2 className=\"size-5\" />\n )}\n </button>\n <input\n type=\"range\"\n min={0}\n max={1}\n step={0.1}\n value={volume}\n onChange={(e) => handleVolumeChange(Number(e.target.value))}\n aria-label=\"Volume\"\n className=\"w-20 h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg\"\n />\n </div>\n\n {/* Playback Speed */}\n <select\n value={playbackRate}\n onChange={(e) => handlePlaybackRateChange(Number(e.target.value))}\n className=\"px-2 py-1 text-sm bg-bg-surface border border-border-default rounded text-text-primary cursor-pointer\"\n >\n <option value=\"0.5\">0.5x</option>\n <option value=\"0.75\">0.75x</option>\n <option value=\"1\">1x</option>\n <option value=\"1.25\">1.25x</option>\n <option value=\"1.5\">1.5x</option>\n <option value=\"2\">2x</option>\n </select>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">{file.name}</span>\n <button\n type=\"button\"\n onClick={handleFullscreen}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title=\"Fullscreen\"\n aria-label=\"Open fullscreen\"\n >\n <Maximize className=\"size-5\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoPlayer.js","names":[],"sources":["../../../../src/files/components/viewers/VideoPlayer.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { Play, Pause, Volume2, VolumeX, Maximize } from \"lucide-react\";\nimport type { FileItem } from \"../../types\";\nimport { useTr } from \"../../../shared/hooks/useTr\";\n\ninterface VideoPlayerProps {\n file: FileItem;\n url: string;\n}\n\nexport function VideoPlayer({ file, url }: VideoPlayerProps) {\n const tr = useTr();\n const videoRef = useRef<HTMLVideoElement>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [volume, setVolume] = useState(1);\n const [isMuted, setIsMuted] = useState(false);\n const [playbackRate, setPlaybackRate] = useState(1);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleTimeUpdate = () => setCurrentTime(video.currentTime);\n const handleLoadedMetadata = () => setDuration(video.duration);\n const handleEnded = () => setIsPlaying(false);\n\n video.addEventListener(\"timeupdate\", handleTimeUpdate);\n video.addEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.addEventListener(\"ended\", handleEnded);\n\n return () => {\n video.removeEventListener(\"timeupdate\", handleTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.removeEventListener(\"ended\", handleEnded);\n };\n }, []);\n\n const togglePlay = () => {\n if (videoRef.current) {\n if (isPlaying) {\n videoRef.current.pause();\n } else {\n videoRef.current.play();\n }\n setIsPlaying(!isPlaying);\n }\n };\n\n const handleSeek = (time: number) => {\n if (videoRef.current) {\n videoRef.current.currentTime = time;\n setCurrentTime(time);\n }\n };\n\n const handleVolumeChange = (newVolume: number) => {\n setVolume(newVolume);\n setIsMuted(newVolume === 0);\n if (videoRef.current) {\n videoRef.current.volume = newVolume;\n }\n };\n\n const toggleMute = () => {\n if (videoRef.current) {\n const newMuted = !isMuted;\n setIsMuted(newMuted);\n videoRef.current.muted = newMuted;\n }\n };\n\n const handlePlaybackRateChange = (rate: number) => {\n setPlaybackRate(rate);\n if (videoRef.current) {\n videoRef.current.playbackRate = rate;\n }\n };\n\n const handleFullscreen = () => {\n if (videoRef.current) {\n videoRef.current.requestFullscreen();\n }\n };\n\n const formatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n };\n\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n {/* Video */}\n <div className=\"flex-1 flex items-center justify-center\">\n <video\n ref={videoRef}\n src={url}\n aria-label={file.name}\n className=\"max-w-full max-h-full\"\n onClick={togglePlay}\n >\n <track kind=\"captions\" />\n </video>\n </div>\n\n {/* Controls */}\n <div className=\"bg-bg-surface border-t border-border-default p-4\">\n {/* Progress Bar */}\n <div className=\"mb-4\">\n <input\n type=\"range\"\n min={0}\n max={duration || 0}\n value={currentTime}\n onChange={(e) => handleSeek(Number(e.target.value))}\n aria-label={tr(\"files.preview.video.seek\", \"Seek\")}\n className=\"w-full h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg\"\n />\n <div className=\"flex justify-between mt-1 text-xs text-text-secondary\">\n <span>{formatTime(currentTime)}</span>\n <span>{formatTime(duration)}</span>\n </div>\n </div>\n\n {/* Control Buttons */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n {/* Play/Pause */}\n <button\n type=\"button\"\n onClick={togglePlay}\n className=\"p-2 text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title={\n isPlaying\n ? tr(\"files.preview.video.pause\", \"Pause\")\n : tr(\"files.preview.video.play\", \"Play\")\n }\n aria-label={\n isPlaying\n ? tr(\"files.preview.video.pauseVideo\", \"Pause video\")\n : tr(\"files.preview.video.playVideo\", \"Play video\")\n }\n >\n {isPlaying ? (\n <Pause className=\"size-5\" />\n ) : (\n <Play className=\"size-5\" />\n )}\n </button>\n\n {/* Volume */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={toggleMute}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title={\n isMuted\n ? tr(\"files.preview.video.unmute\", \"Unmute\")\n : tr(\"files.preview.video.mute\", \"Mute\")\n }\n aria-label={\n isMuted\n ? tr(\"files.preview.video.unmuteVideo\", \"Unmute video\")\n : tr(\"files.preview.video.muteVideo\", \"Mute video\")\n }\n >\n {isMuted ? (\n <VolumeX className=\"size-5\" />\n ) : (\n <Volume2 className=\"size-5\" />\n )}\n </button>\n <input\n type=\"range\"\n min={0}\n max={1}\n step={0.1}\n value={volume}\n onChange={(e) => handleVolumeChange(Number(e.target.value))}\n aria-label={tr(\"files.preview.video.volume\", \"Volume\")}\n className=\"w-20 h-1 bg-bg-sunken rounded-lg appearance-none cursor-pointer accent-action-primary-bg\"\n />\n </div>\n\n {/* Playback Speed */}\n <select\n aria-label={tr(\n \"files.preview.video.playbackRate\",\n \"Playback speed\",\n )}\n value={playbackRate}\n onChange={(e) => handlePlaybackRateChange(Number(e.target.value))}\n className=\"px-2 py-1 text-sm bg-bg-surface border border-border-default rounded text-text-primary scheme-normal dark:scheme-dark cursor-pointer [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"0.5\">0.5x</option>\n <option value=\"0.75\">0.75x</option>\n <option value=\"1\">1x</option>\n <option value=\"1.25\">1.25x</option>\n <option value=\"1.5\">1.5x</option>\n <option value=\"2\">2x</option>\n </select>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">{file.name}</span>\n <button\n type=\"button\"\n onClick={handleFullscreen}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-action-ghost-bg-hover rounded transition-colors duration-200\"\n title={tr(\"files.preview.video.fullscreen\", \"Fullscreen\")}\n aria-label={tr(\n \"files.preview.video.openFullscreen\",\n \"Open fullscreen\",\n )}\n >\n <Maximize className=\"size-5\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTr — translation hook with fallback semantics.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `useI18n().t()` so that when a key has no translation (i.e. the
|
|
5
|
+
* provider returns the key itself), the caller's English fallback is used
|
|
6
|
+
* instead. This keeps consumer call sites consistent and avoids leaking
|
|
7
|
+
* untranslated keys into the UI.
|
|
8
|
+
*
|
|
9
|
+
* Supports optional interpolation params using `{{key}}` placeholders.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const tr = useTr();
|
|
13
|
+
* return <h1>{tr('files.browser.title', 'My Drive')}</h1>;
|
|
14
|
+
* return <p>{tr('files.browser.selectedCount', '{{count}} selected', { count: 5 })}</p>;
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTr(): (key: string, fallback: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
//# sourceMappingURL=useTr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../src/shared/hooks/useTr.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,IAAI,CACvB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KACrC,MAAM,CAmBV"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
//#region src/shared/hooks/useTr.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let { t: n } = t();
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
|
+
}, [n]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { n as useTr };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useTr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../../src/shared/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('files.browser.title', 'My Drive')}</h1>;\n * return <p>{tr('files.browser.selectedCount', '{{count}} selected', { count: 5 })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n) => string {\n const { t } = useI18n();\n return useCallback(\n (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n ): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) =>\n str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, \"g\"), String(v)),\n raw,\n );\n },\n [t],\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,GAEH,GACA,GACA,MACW;EACX,IAAM,IAAa,EAAE,EAAI,EACnB,IAAM,MAAe,IAAM,IAAW;AAE5C,SADK,IACE,OAAO,QAAQ,EAAO,CAAC,QAC3B,GAAK,CAAC,GAAG,OACR,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,EAC7D,EACD,GALmB;IAOtB,CAAC,EAAE,CACJ"}
|