@xiping/react-components 1.0.52 → 1.0.53

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.
Files changed (27) hide show
  1. package/dist/cjs/components/image-compare/ImageCompare.d.ts +6 -0
  2. package/dist/cjs/components/image-compare/ImageCompare.js +1 -1
  3. package/dist/cjs/components/subtitle-player/CurrentMode.d.ts +2 -0
  4. package/dist/cjs/components/subtitle-player/CurrentMode.js +2 -2
  5. package/dist/cjs/components/subtitle-player/LyricsMode.d.ts +2 -0
  6. package/dist/cjs/components/subtitle-player/LyricsMode.js +2 -2
  7. package/dist/cjs/components/subtitle-player/SubtitlePlayer.d.ts +34 -0
  8. package/dist/cjs/components/subtitle-player/SubtitlePlayer.js +1 -1
  9. package/dist/cjs/components/subtitle-player/utils.d.ts +1 -0
  10. package/dist/cjs/components/subtitle-player/utils.js +1 -1
  11. package/dist/cjs/react-components.css +1 -1
  12. package/dist/es/components/image-compare/ImageCompare.d.ts +6 -0
  13. package/dist/es/components/image-compare/ImageCompare.js +83 -65
  14. package/dist/es/components/subtitle-player/CurrentMode.d.ts +2 -0
  15. package/dist/es/components/subtitle-player/CurrentMode.js +25 -22
  16. package/dist/es/components/subtitle-player/LyricsMode.d.ts +2 -0
  17. package/dist/es/components/subtitle-player/LyricsMode.js +46 -43
  18. package/dist/es/components/subtitle-player/SubtitlePlayer.d.ts +34 -0
  19. package/dist/es/components/subtitle-player/SubtitlePlayer.js +146 -98
  20. package/dist/es/components/subtitle-player/utils.d.ts +1 -0
  21. package/dist/es/components/subtitle-player/utils.js +7 -6
  22. package/dist/es/react-components.css +1 -1
  23. package/package.json +16 -16
  24. package/dist/cjs/components/toast/index.d.ts +0 -4
  25. package/dist/cjs/components/translate/Translate.d.ts +0 -5
  26. package/dist/es/components/toast/index.d.ts +0 -4
  27. package/dist/es/components/translate/Translate.d.ts +0 -5
@@ -1,85 +1,103 @@
1
- import { jsxs as R, jsx as s } from "react/jsx-runtime";
2
- import y from "clsx";
3
- import { useState as d, useRef as I, useCallback as t, useEffect as N } from "react";
4
- const T = (i, o) => {
5
- let n;
6
- return function(...l) {
7
- n || (i.apply(this, l), n = !0, setTimeout(() => n = !1, o));
8
- };
9
- }, P = (i) => {
10
- const { originalImage: o, modifiedImage: n, className: l } = i, [h, v] = d(50), [a, g] = d(!1), [w, b] = d(16 / 9), c = I(null), f = t(
11
- (e) => {
12
- if (!a || !c.current) return;
13
- const p = c.current.getBoundingClientRect(), x = (e - p.left) / p.width * 100;
14
- v(Math.min(100, Math.max(0, x)));
15
- },
16
- [a]
17
- ), r = t(
18
- T((e) => f(e), 16),
19
- [f]
20
- ), M = t((e) => {
21
- r(e.clientX);
22
- }, [r]), k = t((e) => {
23
- r(e.touches[0].clientX);
24
- }, [r]), m = t(() => {
25
- g(!0);
26
- }, []), u = t(() => {
27
- g(!1);
28
- }, []);
29
- return N(() => {
1
+ import { jsxs as G, jsx as n } from "react/jsx-runtime";
2
+ import T from "clsx";
3
+ import { useState as L, useRef as s, useCallback as a, useEffect as E, useMemo as g } from "react";
4
+ const W = (N) => {
5
+ const {
6
+ originalImage: c,
7
+ modifiedImage: b,
8
+ className: S,
9
+ dividerWidth: l = 1,
10
+ originalLabel: I = "原图",
11
+ modifiedLabel: P = "处理过的图"
12
+ } = N, [i, z] = L(50), [x, $] = L(16 / 9), p = s(null), u = s(!1), o = s(null), d = s(null), t = s(null), k = a((e) => {
13
+ if (!p.current) return;
14
+ (!d.current || !u.current) && (d.current = p.current.getBoundingClientRect());
15
+ const r = d.current, h = (e - r.left) / r.width * 100, f = Math.min(100, Math.max(0, h));
16
+ z(f);
17
+ }, []), m = a((e) => {
18
+ u.current && (o.current !== null && cancelAnimationFrame(o.current), o.current = requestAnimationFrame(() => {
19
+ k(e);
20
+ }));
21
+ }, [k]), y = a((e) => {
22
+ m(e.clientX);
23
+ }, [m]), M = a((e) => {
24
+ e.touches.length > 0 && m(e.touches[0].clientX);
25
+ }, [m]), R = a(() => {
26
+ u.current = !0, d.current = null;
27
+ const e = (w) => y(w), r = (w) => M(w), v = () => {
28
+ u.current = !1, o.current !== null && (cancelAnimationFrame(o.current), o.current = null), t.current && (t.current(), t.current = null);
29
+ }, h = () => v(), f = () => v();
30
+ window.addEventListener("mousemove", e), window.addEventListener("touchmove", r, { passive: !1 }), window.addEventListener("mouseup", h), window.addEventListener("touchend", f);
31
+ const D = () => {
32
+ window.removeEventListener("mousemove", e), window.removeEventListener("touchmove", r), window.removeEventListener("mouseup", h), window.removeEventListener("touchend", f);
33
+ };
34
+ t.current = D;
35
+ }, [y, M]);
36
+ E(() => () => {
37
+ t.current && (t.current(), t.current = null);
38
+ }, []), E(() => {
30
39
  const e = new Image();
31
40
  e.onload = () => {
32
- b(e.width / e.height);
33
- }, e.src = o;
34
- }, [o]), /* @__PURE__ */ R(
41
+ $(e.width / e.height);
42
+ }, e.src = c;
43
+ }, [c]);
44
+ const j = g(
45
+ () => ({
46
+ backgroundRepeat: "no-repeat",
47
+ backgroundPosition: "top left",
48
+ backgroundImage: `url(${c})`,
49
+ backgroundSize: "contain",
50
+ aspectRatio: x
51
+ }),
52
+ [c, x]
53
+ ), A = g(
54
+ () => ({
55
+ width: `${i}%`,
56
+ backgroundImage: `url(${b})`,
57
+ backgroundRepeat: "no-repeat",
58
+ backgroundPosition: "top left",
59
+ backgroundSize: "auto 100%"
60
+ }),
61
+ [i, b]
62
+ ), C = g(
63
+ () => ({
64
+ left: `${i}%`,
65
+ transform: "translateX(-50%)",
66
+ width: typeof l == "number" ? `${l}px` : l
67
+ }),
68
+ [i, l]
69
+ );
70
+ return /* @__PURE__ */ G(
35
71
  "div",
36
72
  {
37
- ref: c,
38
- className: y("relative select-none overflow-hidden w-full", l),
39
- style: {
40
- backgroundRepeat: "no-repeat",
41
- backgroundPosition: "top left",
42
- backgroundImage: `url(${o})`,
43
- backgroundSize: "contain",
44
- aspectRatio: w
45
- },
46
- onMouseMove: a ? M : void 0,
47
- onTouchMove: a ? k : void 0,
48
- onMouseUp: u,
49
- onMouseLeave: u,
50
- onTouchEnd: u,
73
+ ref: p,
74
+ className: T("relative select-none overflow-hidden w-full", S),
75
+ style: j,
51
76
  children: [
52
- /* @__PURE__ */ s(
77
+ /* @__PURE__ */ n("div", { className: "absolute top-4 left-4 px-3 py-1.5 bg-black/60 text-white text-sm font-medium rounded backdrop-blur-sm z-10", children: I }),
78
+ /* @__PURE__ */ n(
53
79
  "div",
54
80
  {
55
81
  className: "absolute inset-0 h-full overflow-hidden",
56
- style: {
57
- width: `${h}%`,
58
- backgroundImage: `url(${n})`,
59
- backgroundRepeat: "no-repeat",
60
- backgroundPosition: "top left",
61
- backgroundSize: "auto 100%"
62
- }
82
+ style: A,
83
+ children: /* @__PURE__ */ n("div", { className: "absolute top-4 right-4 px-3 py-1.5 bg-black/60 text-white text-sm font-medium rounded backdrop-blur-sm z-10", children: P })
63
84
  }
64
85
  ),
65
- /* @__PURE__ */ s(
86
+ /* @__PURE__ */ n(
66
87
  "div",
67
88
  {
68
- className: "absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize",
69
- style: {
70
- left: `${h}%`,
71
- transform: "translateX(-50%)"
72
- },
73
- onMouseDown: m,
74
- onTouchStart: m,
75
- children: /* @__PURE__ */ s("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-white shadow-lg flex items-center justify-center", children: /* @__PURE__ */ s(
89
+ className: "absolute top-0 bottom-0 bg-white cursor-ew-resize",
90
+ style: C,
91
+ onMouseDown: R,
92
+ onTouchStart: R,
93
+ children: /* @__PURE__ */ n("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-white shadow-lg flex items-center justify-center", children: /* @__PURE__ */ n(
76
94
  "svg",
77
95
  {
78
96
  className: "w-6 h-6 text-gray-600",
79
97
  fill: "none",
80
98
  stroke: "currentColor",
81
99
  viewBox: "0 0 24 24",
82
- children: /* @__PURE__ */ s(
100
+ children: /* @__PURE__ */ n(
83
101
  "path",
84
102
  {
85
103
  strokeLinecap: "round",
@@ -97,5 +115,5 @@ const T = (i, o) => {
97
115
  );
98
116
  };
99
117
  export {
100
- P as default
118
+ W as default
101
119
  };
@@ -13,6 +13,8 @@ export interface CurrentModeProps {
13
13
  enableWordHover: boolean;
14
14
  /** 悬停覆盖层节点 */
15
15
  overlayNode: React.ReactNode;
16
+ /** 整条字幕点击处理器 */
17
+ onSubtitleClick?: (event: React.MouseEvent<HTMLDivElement>, entry: SubtitleEntry, label?: string) => void;
16
18
  }
17
19
  /**
18
20
  * 当前模式组件:只显示当前时间对应的字幕
@@ -1,40 +1,43 @@
1
- import { jsxs as u, Fragment as m, jsx as e } from "react/jsx-runtime";
2
- import { renderTextWithWords as h } from "./utils.js";
3
- const g = ({
4
- currentEntries: t,
5
- wordHoverFactory: o,
6
- enableWordHover: l,
7
- overlayNode: p
8
- }) => t.some(({ entry: i }) => i !== null) ? /* @__PURE__ */ u(m, { children: [
9
- t.map(({ entry: i, label: s }, c) => i ? /* @__PURE__ */ u(
1
+ import { jsxs as p, Fragment as h, jsx as r } from "react/jsx-runtime";
2
+ import { renderTextWithWords as x } from "./utils.js";
3
+ const N = ({
4
+ currentEntries: a,
5
+ wordHoverFactory: u,
6
+ enableWordHover: d,
7
+ overlayNode: c,
8
+ onSubtitleClick: t
9
+ }) => a.some(({ entry: i }) => i !== null) ? /* @__PURE__ */ p(h, { children: [
10
+ a.map(({ entry: i, label: s }, m) => i ? /* @__PURE__ */ p(
10
11
  "div",
11
12
  {
12
13
  className: "xiping-subtitle-player__item",
13
14
  "data-label": s || void 0,
15
+ onClick: t ? (e) => t(e, i, s) : void 0,
16
+ style: t ? { cursor: "pointer" } : void 0,
14
17
  children: [
15
- s && /* @__PURE__ */ e("span", { className: "xiping-subtitle-player__label", children: s }),
16
- /* @__PURE__ */ e("div", { className: "xiping-subtitle-player__text", children: (() => {
17
- let a = 0;
18
+ s && /* @__PURE__ */ r("span", { className: "xiping-subtitle-player__label", children: s }),
19
+ /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__text", children: (() => {
20
+ let e = 0;
18
21
  return i.text.split(`
19
- `).map((n, r) => {
22
+ `).map((n, l) => {
20
23
  if (!i.words || i.words.length === 0)
21
- return /* @__PURE__ */ e("div", { className: "xiping-subtitle-player__line", children: n }, r);
22
- const d = h(
24
+ return /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__line", children: n }, l);
25
+ const o = x(
23
26
  n,
24
27
  i.words,
25
- a,
26
- r,
27
- l ? o(i, s) : void 0
28
+ e,
29
+ l,
30
+ d ? u(i, s) : void 0
28
31
  );
29
- return a = d.nextWordIndex, /* @__PURE__ */ e("div", { className: "xiping-subtitle-player__line", children: d.nodes }, r);
32
+ return e = o.nextWordIndex, /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__line", children: o.nodes }, l);
30
33
  });
31
34
  })() })
32
35
  ]
33
36
  },
34
- c
37
+ m
35
38
  ) : null),
36
- l && /* @__PURE__ */ e("div", { className: "xiping-subtitle-player__hover-layer", children: p })
39
+ d && /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__hover-layer", children: c })
37
40
  ] }) : null;
38
41
  export {
39
- g as CurrentMode
42
+ N as CurrentMode
40
43
  };
@@ -19,6 +19,8 @@ export interface LyricsModeProps {
19
19
  overlayNode: React.ReactNode;
20
20
  /** 容器引用,用于滚动 */
21
21
  containerRef: React.RefObject<HTMLDivElement | null>;
22
+ /** 整条字幕点击处理器 */
23
+ onSubtitleClick?: (event: React.MouseEvent<HTMLDivElement>, entry: SubtitleEntry, label?: string) => void;
22
24
  }
23
25
  /**
24
26
  * 歌词模式组件:显示全部字幕并高亮当前字幕
@@ -1,67 +1,70 @@
1
- import { jsxs as f, Fragment as S, jsx as i } from "react/jsx-runtime";
2
- import { useRef as H, useEffect as j } from "react";
3
- import F from "clsx";
4
- import { secondsToTimeString as W, renderTextWithWords as $ } from "./utils.js";
5
- const M = ({
6
- groupedEntriesByTime: v,
7
- currentTime: l,
8
- wordHoverFactory: b,
9
- enableWordHover: m,
10
- overlayNode: y,
11
- containerRef: o
1
+ import { jsxs as v, Fragment as j, jsx as s } from "react/jsx-runtime";
2
+ import { useRef as F, useEffect as S } from "react";
3
+ import W from "clsx";
4
+ import { secondsToTimeString as $, renderTextWithWords as A } from "./utils.js";
5
+ const P = ({
6
+ groupedEntriesByTime: y,
7
+ currentTime: o,
8
+ wordHoverFactory: N,
9
+ enableWordHover: u,
10
+ overlayNode: b,
11
+ containerRef: n,
12
+ onSubtitleClick: c
12
13
  }) => {
13
- const n = H(null);
14
- return j(() => {
15
- if (n.current && o.current) {
16
- const s = o.current, r = n.current, c = r.offsetTop, a = s.clientHeight, t = r.clientHeight, e = c - a / 2 + t / 2;
17
- s.scrollTo({
18
- top: e,
14
+ const a = F(null);
15
+ return S(() => {
16
+ if (a.current && n.current) {
17
+ const e = n.current, r = a.current, p = r.offsetTop, m = e.clientHeight, t = r.clientHeight, i = p - m / 2 + t / 2;
18
+ e.scrollTo({
19
+ top: i,
19
20
  behavior: "smooth"
20
21
  });
21
22
  }
22
- }, [l, o]), /* @__PURE__ */ f(S, { children: [
23
- v.map((s, r) => {
24
- const c = s[0]?.startTime ?? 0, a = W(c);
25
- return /* @__PURE__ */ i("div", { className: "xiping-subtitle-player__group", children: s.map(({ entry: t, label: e, startTime: N, endTime: d }, u) => {
26
- const _ = l >= N && l <= d, T = l > d, w = u === 0;
27
- let g = 0;
28
- return /* @__PURE__ */ f(
23
+ }, [o, n]), /* @__PURE__ */ v(j, { children: [
24
+ y.map((e, r) => {
25
+ const p = e[0]?.startTime ?? 0, m = $(p);
26
+ return /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__group", children: e.map(({ entry: t, label: i, startTime: T, endTime: _ }, g) => {
27
+ const h = o >= T && o <= _, w = o > _, H = g === 0;
28
+ let x = 0;
29
+ return /* @__PURE__ */ v(
29
30
  "div",
30
31
  {
31
- ref: _ ? n : null,
32
- className: F(
32
+ ref: h ? a : null,
33
+ className: W(
33
34
  "xiping-subtitle-player__item",
34
35
  {
35
- "xiping-subtitle-player__item--active": _,
36
- "xiping-subtitle-player__item--past": T
36
+ "xiping-subtitle-player__item--active": h,
37
+ "xiping-subtitle-player__item--past": w
37
38
  }
38
39
  ),
39
- "data-label": e || void 0,
40
+ "data-label": i || void 0,
41
+ onClick: c ? (l) => c(l, t, i) : void 0,
42
+ style: c ? { cursor: "pointer" } : void 0,
40
43
  children: [
41
- w && /* @__PURE__ */ i("span", { className: "xiping-subtitle-player__time", children: a }),
42
- e && /* @__PURE__ */ i("span", { className: "xiping-subtitle-player__label", children: e }),
43
- /* @__PURE__ */ i("div", { className: "xiping-subtitle-player__text", children: t.text.split(`
44
- `).map((h, p) => {
44
+ H && /* @__PURE__ */ s("span", { className: "xiping-subtitle-player__time", children: m }),
45
+ i && /* @__PURE__ */ s("span", { className: "xiping-subtitle-player__label", children: i }),
46
+ /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__text", children: t.text.split(`
47
+ `).map((l, d) => {
45
48
  if (!t.words || t.words.length === 0)
46
- return /* @__PURE__ */ i("div", { className: "xiping-subtitle-player__line", children: h }, p);
47
- const x = $(
48
- h,
49
+ return /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__line", children: l }, d);
50
+ const f = A(
51
+ l,
49
52
  t.words,
50
- g,
51
- p,
52
- m ? b(t, e) : void 0
53
+ x,
54
+ d,
55
+ u ? N(t, i) : void 0
53
56
  );
54
- return g = x.nextWordIndex, /* @__PURE__ */ i("div", { className: "xiping-subtitle-player__line", children: x.nodes }, p);
57
+ return x = f.nextWordIndex, /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__line", children: f.nodes }, d);
55
58
  }) })
56
59
  ]
57
60
  },
58
- `${r}-${u}`
61
+ `${r}-${g}`
59
62
  );
60
63
  }) }, r);
61
64
  }),
62
- m && /* @__PURE__ */ i("div", { className: "xiping-subtitle-player__hover-layer", children: y })
65
+ u && /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__hover-layer", children: b })
63
66
  ] });
64
67
  };
65
68
  export {
66
- M as LyricsMode
69
+ P as LyricsMode
67
70
  };
@@ -26,6 +26,7 @@ export interface SubtitlePlayerProps {
26
26
  /**
27
27
  * 分词 hover 状态回调,由外部决定如何处理(比如请求第三方接口、展示弹窗等)
28
28
  * 传入 null 表示 hover 结束
29
+ * target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
29
30
  */
30
31
  onWordHoverChange?: (target: HoverTarget | null) => void;
31
32
  /**
@@ -33,16 +34,49 @@ export interface SubtitlePlayerProps {
33
34
  * 建议将数据请求放到父组件,通过该函数返回需要展示的内容
34
35
  */
35
36
  renderWordOverlay?: (target: HoverTarget) => React.ReactNode;
37
+ /**
38
+ * 单词点击回调
39
+ * target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
40
+ */
41
+ onWordClick?: (target: WordClickTarget) => void;
42
+ /**
43
+ * 整条字幕点击回调
44
+ * target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
45
+ */
46
+ onSubtitleClick?: (target: SubtitleClickTarget) => void;
36
47
  }
37
48
  export interface HoverTarget {
38
49
  word: string;
39
50
  wordIndex: number;
40
51
  lineIndex: number;
52
+ /** 完整的字幕条目数据(JSON 格式的数据项) */
53
+ entry: SubtitleEntry;
54
+ label?: string;
55
+ rect: DOMRect;
56
+ element: HTMLElement;
57
+ }
58
+ /** 点击目标类型:单词或整条字幕 */
59
+ export type ClickTargetType = "word" | "subtitle";
60
+ export interface WordClickTarget {
61
+ type: "word";
62
+ word: string;
63
+ wordIndex: number;
64
+ lineIndex: number;
65
+ /** 完整的字幕条目数据(JSON 格式的数据项) */
66
+ entry: SubtitleEntry;
67
+ label?: string;
68
+ rect: DOMRect;
69
+ element: HTMLElement;
70
+ }
71
+ export interface SubtitleClickTarget {
72
+ type: "subtitle";
73
+ /** 完整的字幕条目数据(JSON 格式的数据项) */
41
74
  entry: SubtitleEntry;
42
75
  label?: string;
43
76
  rect: DOMRect;
44
77
  element: HTMLElement;
45
78
  }
79
+ export type ClickTarget = WordClickTarget | SubtitleClickTarget;
46
80
  /**
47
81
  * 字幕播放组件
48
82
  * 支持 SRT 和 WebVTT 格式,可以同时显示多个字幕进行对照