@sweet-player/core 0.2.1 → 0.3.0

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/README.md CHANGED
@@ -58,7 +58,8 @@ player.destroy();
58
58
  - **i18n**:内置 zh-CN/en,支持自定义语言
59
59
  - **插件机制**:`plugins` 选项或 `player.use(plugin)` 运行时安装
60
60
  - **主题定制**:CSS 变量覆盖,如 `--sp-accent`
61
- - **统计信息**:标题 10 连击弹出 YouTube 风格统计面板
61
+ - **右键菜单**:屏蔽原生菜单,内置"更新记录(当前版本,跳转 npm)"、"视频信息(YouTube 风格统计面板)"、"快捷键(键位说明面板)"
62
+ - **按需隐藏**:`hiddenControls: ['ratio', 'audioTrack', ...]` 收集不显示的功能,默认全显示(只影响 UI,不影响 API 与快捷键)
62
63
 
63
64
  ## 画质 / 音轨
64
65
 
@@ -84,23 +85,40 @@ new SweetPlayer({ ..., plugins: [myPlugin] });
84
85
 
85
86
  ### 接入 sweet-subtitle 字幕
86
87
 
88
+ 插件工厂持有字幕实例并对外暴露切换 API,运行时换字幕无需重建播放器:
89
+
87
90
  ```ts
88
91
  import { SweetSubtitle } from 'sweet-subtitle';
89
92
  import type { SweetPlayerPlugin } from '@sweet-player/core';
90
93
 
91
- function subtitlePlugin(src: string): SweetPlayerPlugin {
92
- return {
94
+ function createSubtitlePlugin(src?: string) {
95
+ let sub: SweetSubtitle | null = null;
96
+ const plugin: SweetPlayerPlugin = {
93
97
  name: 'sweet-subtitle',
94
98
  apply(player) {
95
- const sub = new SweetSubtitle(player.video, { src });
96
- return () => sub.destroy?.();
99
+ sub = new SweetSubtitle(player.video, src ? { src } : {});
100
+ return () => { sub?.destroy(); sub = null; }; // 播放器 destroy 时自动清理
97
101
  },
98
102
  };
103
+ return {
104
+ plugin,
105
+ load: (url: string) => sub?.loadFromUrl(url), // 切换字幕
106
+ show: () => sub?.show(),
107
+ hide: () => sub?.hide(),
108
+ setOffset: (s: number) => sub?.setOffset(s),
109
+ };
99
110
  }
100
111
 
101
- new SweetPlayer({ ..., plugins: [subtitlePlugin('/subs/ep-01.ass')] });
112
+ const subtitle = createSubtitlePlugin('/subs/ep-01.ass');
113
+ const player = new SweetPlayer({ ..., plugins: [subtitle.plugin] });
114
+
115
+ // 随时换字幕 / 关字幕,播放器不动
116
+ await subtitle.load('/subs/ep-02.ass');
117
+ subtitle.hide();
102
118
  ```
103
119
 
120
+ 不想用插件的话也可以完全独立管理:`new SweetSubtitle(player.video, ...)` 拿到实例自己持有,只需在销毁播放器前调用 `sub.destroy()`。
121
+
104
122
  ## 实例 API
105
123
 
106
124
  `play()` `pause()` `toggle()` `seek(t)` `seekBy(±s)` `setRate(r)` `setVolume(0-100)` `setMuted(b)` `setAspectRatio(r)` `setQualities(list)` `setAudioTracks(list)` `toggleFullscreen()` `togglePip()` `load(src)` `setTitle(s)` `use(plugin)` `on/off(event, fn)` `destroy()`
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  SweetPlayer: () => SweetPlayer,
34
+ VERSION: () => VERSION,
34
35
  registerLocale: () => registerLocale
35
36
  });
36
37
  module.exports = __toCommonJS(src_exports);
@@ -62,6 +63,25 @@ var EventEmitter = class {
62
63
 
63
64
  // src/core/media.ts
64
65
  var import_hls = __toESM(require("hls.js"), 1);
66
+
67
+ // src/logger.ts
68
+ var MAX_ENTRIES = 2e3;
69
+ var entries = [];
70
+ function pad2(n) {
71
+ return n < 10 ? `0${n}` : String(n);
72
+ }
73
+ function formatTs(d) {
74
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
75
+ }
76
+ function log(category, message) {
77
+ if (entries.length >= MAX_ENTRIES) entries.shift();
78
+ entries.push({ time: /* @__PURE__ */ new Date(), category, message });
79
+ }
80
+ function getLogText() {
81
+ return entries.map((e) => `[${formatTs(e.time)}] [${e.category}]\uFF1A${e.message}`).join("\n");
82
+ }
83
+
84
+ // src/core/media.ts
65
85
  function isHlsSource(src) {
66
86
  return /\.m3u8($|\?)/i.test(src);
67
87
  }
@@ -78,10 +98,15 @@ var MediaController = class {
78
98
  load(src) {
79
99
  this.detachHls();
80
100
  this.currentSrc = src;
101
+ log("\u64AD\u653E\u5668", `\u52A0\u8F7D\u6E90: ${src}`);
81
102
  if (isHlsSource(src)) {
82
103
  if (import_hls.default.isSupported()) {
83
104
  this.hls = new import_hls.default(this.hlsConfig);
105
+ this.hls.on(import_hls.default.Events.MEDIA_ATTACHED, () => {
106
+ log("hls\u4E8B\u4EF6", "\u5A92\u4F53\u6210\u529F\u9644\u52A0\u5230\u64AD\u653E\u5668");
107
+ });
84
108
  this.hls.on(import_hls.default.Events.ERROR, (_event, data) => {
109
+ log("hls\u4E8B\u4EF6", `${data.fatal ? "\u81F4\u547D" : ""}\u9519\u8BEF: ${data.type} - ${data.details}`);
85
110
  if (data.fatal) {
86
111
  switch (data.type) {
87
112
  case import_hls.default.ErrorTypes.NETWORK_ERROR:
@@ -97,8 +122,17 @@ var MediaController = class {
97
122
  }
98
123
  });
99
124
  this.hls.on(import_hls.default.Events.MANIFEST_PARSED, () => {
125
+ log("hls\u4E8B\u4EF6", "\u64AD\u653E\u6E05\u5355\u89E3\u6790\u5B8C\u6BD5");
100
126
  this.notifyTracks();
101
127
  });
128
+ this.hls.on(import_hls.default.Events.BUFFER_CREATED, () => {
129
+ log("hls\u4E8B\u4EF6", "\u7F13\u51B2\u533A\u521B\u5EFA");
130
+ });
131
+ this.hls.on(import_hls.default.Events.LEVEL_SWITCHED, (_event, data) => {
132
+ const level = this.hls?.levels[data.level];
133
+ const label = level?.height ? `${level.height}P` : `Level ${data.level}`;
134
+ log("hls\u4E8B\u4EF6", `\u753B\u8D28\u6863\u4F4D\u5207\u6362: ${label}`);
135
+ });
102
136
  this.hls.loadSource(src);
103
137
  this.hls.attachMedia(this.video);
104
138
  return;
@@ -633,33 +667,37 @@ function createVolumeControl(opts) {
633
667
 
634
668
  // src/ui/controls.ts
635
669
  function createControls(ctx) {
636
- const { video, actions, i18n } = ctx;
670
+ const { video, actions, i18n, hidden } = ctx;
671
+ const show = (name) => !hidden.has(name);
637
672
  const topEl = createEl("div", { className: "sp-top" });
638
- const titleEl = createEl("div", { className: "sp-title", text: ctx.title, parent: topEl });
639
- titleEl.addEventListener("click", actions.onTitleClick);
673
+ if (show("title")) createEl("div", { className: "sp-title", text: ctx.title, parent: topEl });
640
674
  const bottomEl = createEl("div", { className: "sp-bottom" });
641
675
  const progress = createProgressBar(video, (t) => {
642
676
  video.currentTime = t;
643
677
  });
644
- bottomEl.appendChild(progress.el);
678
+ if (show("progress")) bottomEl.appendChild(progress.el);
645
679
  const row = createEl("div", { className: "sp-controls", parent: bottomEl });
646
- const button = (html, title, onClick, disabled = false) => {
680
+ const button = (html, title, onClick, disabled = false, visible = true) => {
647
681
  const btn = createEl("button", {
648
682
  className: "sp-btn",
649
683
  html,
650
684
  attrs: { type: "button", title, "aria-label": title },
651
- parent: row
685
+ parent: visible ? row : void 0
652
686
  });
653
687
  btn.disabled = disabled;
654
688
  btn.addEventListener("click", onClick);
655
689
  return btn;
656
690
  };
657
- button(icons.prev, i18n.t("prev"), () => actions.onPrev?.(), !actions.onPrev);
658
- button(icons.seekBack, i18n.t("seekBack", { n: ctx.seekStep }), () => actions.seekBy(-ctx.seekStep));
659
- const playBtn = button(icons.play, i18n.t("playPause"), actions.togglePlay);
660
- button(icons.seekForward, i18n.t("seekForward", { n: ctx.seekStep }), () => actions.seekBy(ctx.seekStep));
661
- button(icons.next, i18n.t("next"), () => actions.onNext?.(), !actions.onNext);
662
- const timeEl = createEl("span", { className: "sp-time", text: "0:00 / 0:00", parent: row });
691
+ button(icons.prev, i18n.t("prev"), () => actions.onPrev?.(), !actions.onPrev, show("prev"));
692
+ button(icons.seekBack, i18n.t("seekBack", { n: ctx.seekStep }), () => actions.seekBy(-ctx.seekStep), false, show("seekBack"));
693
+ const playBtn = button(icons.play, i18n.t("playPause"), actions.togglePlay, false, show("play"));
694
+ button(icons.seekForward, i18n.t("seekForward", { n: ctx.seekStep }), () => actions.seekBy(ctx.seekStep), false, show("seekForward"));
695
+ button(icons.next, i18n.t("next"), () => actions.onNext?.(), !actions.onNext, show("next"));
696
+ const timeEl = createEl("span", {
697
+ className: "sp-time",
698
+ text: "0:00 / 0:00",
699
+ parent: show("time") ? row : void 0
700
+ });
663
701
  createEl("div", { className: "sp-controls-spacer", parent: row });
664
702
  const rateMenu = createPopupMenu({
665
703
  buttonHtml: "1x",
@@ -669,14 +707,14 @@ function createControls(ctx) {
669
707
  });
670
708
  rateMenu.setItems(ctx.playbackRates.map((r) => ({ label: `${r}x`, value: r })));
671
709
  rateMenu.setActive(video.playbackRate);
672
- row.appendChild(rateMenu.el);
710
+ if (show("rate")) row.appendChild(rateMenu.el);
673
711
  const qualityMenu = createPopupMenu({
674
712
  buttonHtml: i18n.t("quality"),
675
713
  title: i18n.t("quality"),
676
714
  emptyText: i18n.t("empty"),
677
715
  onSelect: (item) => actions.selectQuality(item.value)
678
716
  });
679
- row.appendChild(qualityMenu.el);
717
+ if (show("quality")) row.appendChild(qualityMenu.el);
680
718
  const ratioMenu = createPopupMenu({
681
719
  buttonHtml: i18n.t("aspectRatio"),
682
720
  title: i18n.t("aspectRatio"),
@@ -687,24 +725,24 @@ function createControls(ctx) {
687
725
  ctx.aspectRatios.map((r) => ({ label: r === "original" ? i18n.t("ratioOriginal") : r, value: r }))
688
726
  );
689
727
  ratioMenu.setActive("original");
690
- row.appendChild(ratioMenu.el);
728
+ if (show("ratio")) row.appendChild(ratioMenu.el);
691
729
  const audioMenu = createPopupMenu({
692
730
  buttonHtml: icons.audio,
693
731
  title: i18n.t("audioTrack"),
694
732
  emptyText: i18n.t("empty"),
695
733
  onSelect: (item) => actions.selectAudioTrack(item.value)
696
734
  });
697
- row.appendChild(audioMenu.el);
735
+ if (show("audioTrack")) row.appendChild(audioMenu.el);
698
736
  const volume = createVolumeControl({
699
737
  muteTitle: i18n.t("mute"),
700
738
  onVolumeChange: actions.setVolume,
701
739
  onToggleMute: actions.toggleMute
702
740
  });
703
- row.appendChild(volume.el);
704
- if (document.pictureInPictureEnabled) {
741
+ if (show("volume")) row.appendChild(volume.el);
742
+ if (show("pip") && document.pictureInPictureEnabled) {
705
743
  button(icons.pip, i18n.t("pip"), actions.togglePip);
706
744
  }
707
- const fsBtn = button(icons.fullscreen, i18n.t("fullscreen"), actions.toggleFullscreen);
745
+ const fsBtn = button(icons.fullscreen, i18n.t("fullscreen"), actions.toggleFullscreen, false, show("fullscreen"));
708
746
  return {
709
747
  topEl,
710
748
  bottomEl,
@@ -762,6 +800,94 @@ function createOsd() {
762
800
  };
763
801
  }
764
802
 
803
+ // src/ui/components/contextMenu.ts
804
+ function createContextMenu(container, items) {
805
+ const menu = createEl("div", { className: "sp-context-menu", parent: container });
806
+ for (const item of items) {
807
+ const btn = createEl("button", {
808
+ className: "sp-context-item",
809
+ text: item.label,
810
+ attrs: { type: "button" },
811
+ parent: menu
812
+ });
813
+ btn.addEventListener("click", () => {
814
+ hide();
815
+ item.onClick();
816
+ });
817
+ }
818
+ function show(clientX, clientY) {
819
+ menu.classList.add("sp-visible");
820
+ const rect = container.getBoundingClientRect();
821
+ const x = Math.max(4, Math.min(clientX - rect.left, rect.width - menu.offsetWidth - 4));
822
+ const y = Math.max(4, Math.min(clientY - rect.top, rect.height - menu.offsetHeight - 4));
823
+ menu.style.left = `${x}px`;
824
+ menu.style.top = `${y}px`;
825
+ }
826
+ function hide() {
827
+ menu.classList.remove("sp-visible");
828
+ }
829
+ const onContextMenu = (e) => {
830
+ e.preventDefault();
831
+ show(e.clientX, e.clientY);
832
+ };
833
+ const onPointerDown = (e) => {
834
+ if (!menu.contains(e.target)) hide();
835
+ };
836
+ container.addEventListener("contextmenu", onContextMenu);
837
+ document.addEventListener("pointerdown", onPointerDown, true);
838
+ return {
839
+ hide,
840
+ destroy() {
841
+ container.removeEventListener("contextmenu", onContextMenu);
842
+ document.removeEventListener("pointerdown", onPointerDown, true);
843
+ menu.remove();
844
+ }
845
+ };
846
+ }
847
+
848
+ // src/ui/components/shortcutsOverlay.ts
849
+ function createShortcutsOverlay(container, i18n, seekStep) {
850
+ let root = null;
851
+ function show() {
852
+ if (root) return;
853
+ root = createEl("div", { className: "sp-shortcuts", parent: container });
854
+ const closeBtn = createEl("button", {
855
+ className: "sp-shortcuts-close",
856
+ text: "\u2715",
857
+ attrs: { type: "button", "aria-label": i18n.t("close") },
858
+ parent: root
859
+ });
860
+ closeBtn.addEventListener("click", hide);
861
+ createEl("div", { className: "sp-shortcuts-title", text: i18n.t("shortcuts"), parent: root });
862
+ const dl = createEl("dl", { className: "sp-shortcuts-body", parent: root });
863
+ const rows = [
864
+ [i18n.t("scKeySpace"), i18n.t("scPlayPause")],
865
+ ["\u2190 / \u2192", i18n.t("scSeek", { n: seekStep })],
866
+ [i18n.t("scKeyHold"), i18n.t("scLongSeek")],
867
+ ["\u2191 / \u2193", i18n.t("scVolume")],
868
+ ["F", i18n.t("scFullscreen")],
869
+ ["M", i18n.t("scMute")]
870
+ ];
871
+ for (const [key, action] of rows) {
872
+ const row = createEl("div", { className: "sp-shortcuts-row", parent: dl });
873
+ const dt = createEl("dt", { parent: row });
874
+ createEl("span", { className: "sp-kbd", text: key, parent: dt });
875
+ createEl("dd", { text: action, parent: row });
876
+ }
877
+ }
878
+ function hide() {
879
+ root?.remove();
880
+ root = null;
881
+ }
882
+ return {
883
+ toggle() {
884
+ root ? hide() : show();
885
+ },
886
+ hide,
887
+ destroy: hide
888
+ };
889
+ }
890
+
765
891
  // src/ui/components/statsOverlay.ts
766
892
  function createStatsOverlay(container, video, media, i18n) {
767
893
  let root = null;
@@ -800,11 +926,31 @@ function createStatsOverlay(container, video, media, i18n) {
800
926
  function show() {
801
927
  if (root) return;
802
928
  root = createEl("div", { className: "sp-stats", parent: container });
929
+ const header = createEl("div", { className: "sp-stats-header", parent: root });
930
+ const copyBtn = createEl("button", {
931
+ className: "sp-stats-copy",
932
+ text: i18n.t("copyLog"),
933
+ attrs: { type: "button" },
934
+ parent: header
935
+ });
936
+ copyBtn.addEventListener("click", () => {
937
+ const text = getLogText();
938
+ void navigator.clipboard.writeText(text || "(empty)").then(
939
+ () => {
940
+ copyBtn.textContent = i18n.t("logCopied");
941
+ setTimeout(() => {
942
+ copyBtn.textContent = i18n.t("copyLog");
943
+ }, 1500);
944
+ },
945
+ () => {
946
+ }
947
+ );
948
+ });
803
949
  const closeBtn = createEl("button", {
804
950
  className: "sp-stats-close",
805
951
  text: "\u2715",
806
952
  attrs: { type: "button", "aria-label": i18n.t("closeStats") },
807
- parent: root
953
+ parent: header
808
954
  });
809
955
  closeBtn.addEventListener("click", hide);
810
956
  createEl("dl", { className: "sp-stats-body", parent: root });
@@ -969,7 +1115,21 @@ var zhCN = {
969
1115
  autoNextIn: "{n} \u79D2\u540E\u64AD\u653E\u4E0B\u4E00\u4E2A",
970
1116
  cancel: "\u53D6\u6D88",
971
1117
  loadError: "\u89C6\u9891\u52A0\u8F7D\u5931\u8D25",
972
- retry: "\u91CD\u8BD5"
1118
+ retry: "\u91CD\u8BD5",
1119
+ changelog: "\u66F4\u65B0\u8BB0\u5F55",
1120
+ videoInfo: "\u89C6\u9891\u4FE1\u606F",
1121
+ shortcuts: "\u5FEB\u6377\u952E",
1122
+ close: "\u5173\u95ED",
1123
+ scKeySpace: "\u7A7A\u683C",
1124
+ scKeyHold: "\u2190 / \u2192 \u957F\u6309",
1125
+ scPlayPause: "\u64AD\u653E / \u6682\u505C",
1126
+ scSeek: "\u5FEB\u9000 / \u5FEB\u8FDB {n} \u79D2",
1127
+ scLongSeek: "\u9636\u68AF\u7D2F\u8BA1\u5FEB\u8FDB\u5FEB\u9000\uFF0C\u677E\u5F00\u6267\u884C",
1128
+ scVolume: "\u97F3\u91CF \xB15",
1129
+ scFullscreen: "\u5168\u5C4F\u5207\u6362",
1130
+ scMute: "\u9759\u97F3\u5207\u6362",
1131
+ copyLog: "\u590D\u5236\u65E5\u5FD7",
1132
+ logCopied: "\u5DF2\u590D\u5236"
973
1133
  };
974
1134
  var en = {
975
1135
  play: "Play",
@@ -998,7 +1158,21 @@ var en = {
998
1158
  autoNextIn: "Playing next in {n}s",
999
1159
  cancel: "Cancel",
1000
1160
  loadError: "Failed to load video",
1001
- retry: "Retry"
1161
+ retry: "Retry",
1162
+ changelog: "Changelog",
1163
+ videoInfo: "Video info",
1164
+ shortcuts: "Shortcuts",
1165
+ close: "Close",
1166
+ scKeySpace: "Space",
1167
+ scKeyHold: "Hold \u2190 / \u2192",
1168
+ scPlayPause: "Play / Pause",
1169
+ scSeek: "Rewind / forward {n}s",
1170
+ scLongSeek: "Accelerating seek, release to apply",
1171
+ scVolume: "Volume \xB15",
1172
+ scFullscreen: "Toggle fullscreen",
1173
+ scMute: "Toggle mute",
1174
+ copyLog: "Copy log",
1175
+ logCopied: "Copied"
1002
1176
  };
1003
1177
  var locales = { "zh-CN": zhCN, en };
1004
1178
  var I18n = class {
@@ -1071,8 +1245,11 @@ function clearProgress(id) {
1071
1245
  }
1072
1246
  }
1073
1247
 
1248
+ // src/version.ts
1249
+ var VERSION = true ? "0.3.0" : "dev";
1250
+
1074
1251
  // src/styles/player.css
1075
- var player_default = ".sweet-player {\n --sp-accent: #ff4d6d;\n --sp-bg: #000;\n --sp-text: #fff;\n --sp-bar-height: 4px;\n --sp-bar-hover-height: 6px;\n --sp-control-size: 36px;\n --sp-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--sp-bg);\n color: var(--sp-text);\n font-family: var(--sp-font);\n font-size: 13px;\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n outline: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sweet-player *,\n.sweet-player *::before,\n.sweet-player *::after {\n box-sizing: border-box;\n}\n\n.sp-video {\n width: 100%;\n height: 100%;\n object-fit: contain;\n display: block;\n}\n\n/* \u5F3A\u5236\u753B\u9762\u6BD4\u4F8B\uFF1Avideo \u5C45\u4E2D\u3001\u6309\u6BD4\u4F8B\u9650\u5236\u5C3A\u5BF8\u5E76\u62C9\u4F38\u586B\u5145 */\n.sweet-player[data-ratio]:not([data-ratio='original']) .sp-video {\n width: auto;\n height: auto;\n max-width: 100%;\n max-height: 100%;\n aspect-ratio: var(--sp-forced-ratio);\n object-fit: fill;\n}\n\n/* ---------- \u9876\u90E8\u6807\u9898\u533A ---------- */\n.sp-top {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n padding: 12px 16px 28px;\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);\n transition: opacity 0.25s ease;\n pointer-events: none;\n}\n\n.sp-title {\n display: inline-block;\n max-width: 70%;\n font-size: 15px;\n font-weight: 500;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: auto;\n cursor: default;\n}\n\n/* ---------- \u7EDF\u8BA1\u4FE1\u606F\u8499\u5C42 ---------- */\n.sp-stats {\n position: absolute;\n top: 12px;\n left: 12px;\n z-index: 30;\n min-width: 300px;\n max-width: calc(100% - 24px);\n padding: 10px 14px;\n background: rgba(0, 0, 0, 0.75);\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.7;\n font-family: 'SFMono-Regular', Consolas, monospace;\n pointer-events: auto;\n}\n\n.sp-stats-close {\n position: absolute;\n top: 6px;\n right: 6px;\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n opacity: 0.7;\n font-size: 16px;\n line-height: 1;\n}\n\n.sp-stats-close:hover {\n opacity: 1;\n}\n\n.sp-stats-row {\n display: flex;\n gap: 12px;\n}\n\n.sp-stats-row dt {\n flex: 0 0 110px;\n opacity: 0.65;\n margin: 0;\n}\n\n.sp-stats-row dd {\n margin: 0;\n word-break: break-all;\n}\n\n/* ---------- \u4E2D\u592E OSD \u53CD\u9988 ---------- */\n.sp-osd {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 20;\n padding: 10px 18px;\n background: rgba(0, 0, 0, 0.65);\n border-radius: 6px;\n font-size: 16px;\n font-weight: 500;\n opacity: 0;\n transition: opacity 0.15s ease;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.sp-osd.sp-visible {\n opacity: 1;\n}\n\n/* ---------- \u5E95\u90E8\u63A7\u5236\u533A ---------- */\n.sp-bottom {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n padding: 24px 12px 6px;\n background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);\n transition: opacity 0.25s ease;\n z-index: 10;\n}\n\n.sweet-player.sp-controls-hidden .sp-bottom,\n.sweet-player.sp-controls-hidden .sp-top {\n opacity: 0;\n pointer-events: none;\n}\n\n.sweet-player.sp-controls-hidden {\n cursor: none;\n}\n\n/* ---------- \u8FDB\u5EA6\u6761 ---------- */\n.sp-progress {\n position: relative;\n height: 14px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-bottom: 4px;\n}\n\n.sp-progress-track {\n position: relative;\n width: 100%;\n height: var(--sp-bar-height);\n background: rgba(255, 255, 255, 0.25);\n border-radius: 2px;\n overflow: visible;\n transition: height 0.1s ease;\n}\n\n.sp-progress:hover .sp-progress-track {\n height: var(--sp-bar-hover-height);\n}\n\n.sp-progress-buffered,\n.sp-progress-played {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n border-radius: 2px;\n}\n\n.sp-progress-buffered {\n background: rgba(255, 255, 255, 0.35);\n}\n\n.sp-progress-played {\n background: var(--sp-accent);\n}\n\n.sp-progress-thumb {\n position: absolute;\n top: 50%;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--sp-accent);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.1s ease;\n pointer-events: none;\n}\n\n.sp-progress:hover .sp-progress-thumb,\n.sp-progress.sp-dragging .sp-progress-thumb {\n transform: translate(-50%, -50%) scale(1);\n}\n\n.sp-progress-tooltip {\n position: absolute;\n bottom: 18px;\n transform: translateX(-50%);\n padding: 2px 6px;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n font-size: 12px;\n display: none;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.sp-progress:hover .sp-progress-tooltip {\n display: block;\n}\n\n/* ---------- \u63A7\u5236\u6309\u94AE\u884C ---------- */\n.sp-controls {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.sp-controls-spacer {\n flex: 1;\n}\n\n.sp-btn {\n width: var(--sp-control-size);\n height: var(--sp-control-size);\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n padding: 0;\n opacity: 0.9;\n transition: opacity 0.1s ease, background 0.1s ease;\n}\n\n.sp-btn:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.15);\n}\n\n.sp-btn svg {\n width: 22px;\n height: 22px;\n}\n\n.sp-btn:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n}\n\n.sp-time {\n padding: 0 8px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n opacity: 0.95;\n}\n\n/* ---------- \u5F39\u51FA\u83DC\u5355 ---------- */\n.sp-menu {\n position: relative;\n}\n\n.sp-menu-btn {\n min-width: var(--sp-control-size);\n width: auto;\n padding: 0 8px;\n font-size: 13px;\n font-weight: 500;\n}\n\n.sp-menu-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n min-width: 80px;\n max-height: 220px;\n overflow-y: auto;\n background: rgba(28, 28, 28, 0.95);\n border-radius: 6px;\n padding: 4px 0;\n display: none;\n z-index: 25;\n}\n\n.sp-menu.sp-open .sp-menu-panel {\n display: block;\n}\n\n.sp-menu-item {\n display: block;\n width: 100%;\n padding: 7px 16px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n text-align: center;\n cursor: pointer;\n font-size: 13px;\n white-space: nowrap;\n}\n\n.sp-menu-item:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n\n.sp-menu-item.sp-active {\n color: var(--sp-accent);\n font-weight: 600;\n}\n\n.sp-menu-empty {\n padding: 7px 16px;\n opacity: 0.5;\n white-space: nowrap;\n}\n\n/* ---------- \u72B6\u6001\u8499\u5C42\uFF1Aloading / \u9519\u8BEF / \u7ED3\u675F ---------- */\n.sp-state {\n position: absolute;\n inset: 0;\n z-index: 15;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n}\n\n.sp-spinner {\n opacity: 0;\n transition: opacity 0.2s ease 0.3s; /* \u5EF6\u8FDF\u51FA\u73B0\uFF0C\u907F\u514D\u77ED\u6682 seek \u95EA\u70C1 */\n}\n\n.sp-state.sp-loading .sp-spinner {\n opacity: 1;\n}\n\n.sp-spinner-ring {\n width: 44px;\n height: 44px;\n border: 4px solid rgba(255, 255, 255, 0.25);\n border-top-color: var(--sp-accent);\n border-radius: 50%;\n animation: sp-spin 0.8s linear infinite;\n}\n\n@keyframes sp-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.sp-state-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n background: rgba(0, 0, 0, 0.55);\n pointer-events: auto;\n}\n\n.sp-state-message {\n font-size: 15px;\n opacity: 0.95;\n}\n\n.sp-state-actions {\n display: flex;\n gap: 12px;\n}\n\n.sp-state-btn {\n padding: 8px 22px;\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 18px;\n background: transparent;\n color: var(--sp-text);\n font-size: 14px;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-state-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.sp-state-btn-primary {\n background: var(--sp-accent);\n border-color: var(--sp-accent);\n}\n\n.sp-state-btn-primary:hover {\n background: var(--sp-accent);\n filter: brightness(1.1);\n}\n\n.sp-state-countdown {\n font-size: 13px;\n opacity: 0.85;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.sp-state-cancel {\n border: none;\n background: transparent;\n color: var(--sp-accent);\n cursor: pointer;\n font-size: 13px;\n padding: 2px 6px;\n}\n\n/* ---------- \u97F3\u91CF ---------- */\n.sp-volume {\n display: flex;\n align-items: center;\n}\n\n.sp-volume-slider {\n width: 0;\n overflow: hidden;\n transition: width 0.15s ease;\n display: flex;\n align-items: center;\n}\n\n.sp-volume:hover .sp-volume-slider,\n.sp-volume.sp-dragging .sp-volume-slider {\n width: 64px;\n}\n\n.sp-volume-track {\n position: relative;\n width: 56px;\n height: var(--sp-bar-height);\n margin: 0 4px;\n background: rgba(255, 255, 255, 0.25);\n border-radius: 2px;\n cursor: pointer;\n}\n\n.sp-volume-fill {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-text);\n border-radius: 2px;\n}\n\n.sp-volume-thumb {\n position: absolute;\n top: 50%;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--sp-text);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n";
1252
+ var player_default = ".sweet-player {\n --sp-accent: #ff4d6d;\n --sp-bg: #000;\n --sp-text: #fff;\n --sp-bar-height: 4px;\n --sp-bar-hover-height: 6px;\n --sp-control-size: 36px;\n --sp-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--sp-bg);\n color: var(--sp-text);\n font-family: var(--sp-font);\n font-size: 13px;\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n outline: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sweet-player *,\n.sweet-player *::before,\n.sweet-player *::after {\n box-sizing: border-box;\n}\n\n.sp-video {\n width: 100%;\n height: 100%;\n object-fit: contain;\n display: block;\n}\n\n/* \u5F3A\u5236\u753B\u9762\u6BD4\u4F8B\uFF1Avideo \u5C45\u4E2D\u3001\u6309\u6BD4\u4F8B\u9650\u5236\u5C3A\u5BF8\u5E76\u62C9\u4F38\u586B\u5145 */\n.sweet-player[data-ratio]:not([data-ratio='original']) .sp-video {\n width: auto;\n height: auto;\n max-width: 100%;\n max-height: 100%;\n aspect-ratio: var(--sp-forced-ratio);\n object-fit: fill;\n}\n\n/* ---------- \u9876\u90E8\u6807\u9898\u533A ---------- */\n.sp-top {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n padding: 12px 16px 28px;\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);\n transition: opacity 0.25s ease;\n pointer-events: none;\n}\n\n.sp-title {\n display: inline-block;\n max-width: 70%;\n font-size: 15px;\n font-weight: 500;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: auto;\n cursor: default;\n}\n\n/* ---------- \u7EDF\u8BA1\u4FE1\u606F\u8499\u5C42 ---------- */\n.sp-stats {\n position: absolute;\n top: 12px;\n left: 12px;\n z-index: 30;\n min-width: 300px;\n max-width: calc(100% - 24px);\n padding: 10px 14px;\n background: rgba(0, 0, 0, 0.75);\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.7;\n font-family: 'SFMono-Regular', Consolas, monospace;\n pointer-events: auto;\n}\n\n.sp-stats-header {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: 4px;\n margin: -4px -6px 4px 0;\n}\n\n.sp-stats-copy {\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n opacity: 0.7;\n font-size: 12px;\n line-height: 22px;\n padding: 0 6px;\n border-radius: 3px;\n white-space: nowrap;\n}\n\n.sp-stats-copy:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-stats-close {\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n opacity: 0.7;\n font-size: 16px;\n line-height: 1;\n flex-shrink: 0;\n}\n\n.sp-stats-close:hover {\n opacity: 1;\n}\n\n.sp-stats-row {\n display: flex;\n gap: 12px;\n}\n\n.sp-stats-row dt {\n flex: 0 0 110px;\n opacity: 0.65;\n margin: 0;\n}\n\n.sp-stats-row dd {\n margin: 0;\n word-break: break-all;\n}\n\n/* ---------- \u5FEB\u6377\u952E\u9762\u677F ---------- */\n.sp-shortcuts {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 30;\n min-width: 280px;\n max-width: calc(100% - 24px);\n max-height: calc(100% - 24px);\n overflow-y: auto;\n padding: 14px 20px 14px;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 8px;\n font-size: 13px;\n line-height: 2.1;\n pointer-events: auto;\n}\n\n.sp-shortcuts-title {\n font-size: 14px;\n font-weight: 600;\n margin-bottom: 6px;\n}\n\n.sp-shortcuts-body {\n margin: 0;\n}\n\n.sp-shortcuts-row {\n display: flex;\n align-items: center;\n gap: 16px;\n}\n\n.sp-shortcuts-row dt {\n flex: 0 0 100px;\n margin: 0;\n}\n\n.sp-shortcuts-row dd {\n margin: 0;\n opacity: 0.85;\n}\n\n.sp-kbd {\n display: inline-block;\n padding: 0 7px;\n background: rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n font-family: 'SFMono-Regular', Consolas, monospace;\n font-size: 12px;\n line-height: 20px;\n white-space: nowrap;\n}\n\n.sp-shortcuts-close {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n opacity: 0.7;\n font-size: 16px;\n line-height: 1;\n}\n\n.sp-shortcuts-close:hover {\n opacity: 1;\n}\n\n/* ---------- \u53F3\u952E\u83DC\u5355 ---------- */\n.sp-context-menu {\n position: absolute;\n z-index: 40;\n min-width: 150px;\n padding: 4px 0;\n background: rgba(28, 28, 28, 0.95);\n border-radius: 6px;\n display: none;\n}\n\n.sp-context-menu.sp-visible {\n display: block;\n}\n\n.sp-context-item {\n display: block;\n width: 100%;\n padding: 8px 16px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n text-align: left;\n cursor: pointer;\n font-size: 13px;\n white-space: nowrap;\n}\n\n.sp-context-item:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n\n/* ---------- \u4E2D\u592E OSD \u53CD\u9988 ---------- */\n.sp-osd {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 20;\n padding: 10px 18px;\n background: rgba(0, 0, 0, 0.65);\n border-radius: 6px;\n font-size: 16px;\n font-weight: 500;\n opacity: 0;\n transition: opacity 0.15s ease;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.sp-osd.sp-visible {\n opacity: 1;\n}\n\n/* ---------- \u5E95\u90E8\u63A7\u5236\u533A ---------- */\n.sp-bottom {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n padding: 24px 12px 6px;\n background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);\n transition: opacity 0.25s ease;\n z-index: 10;\n}\n\n.sweet-player.sp-controls-hidden .sp-bottom,\n.sweet-player.sp-controls-hidden .sp-top {\n opacity: 0;\n pointer-events: none;\n}\n\n.sweet-player.sp-controls-hidden {\n cursor: none;\n}\n\n/* ---------- \u8FDB\u5EA6\u6761 ---------- */\n.sp-progress {\n position: relative;\n height: 14px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-bottom: 4px;\n}\n\n.sp-progress-track {\n position: relative;\n width: 100%;\n height: var(--sp-bar-height);\n background: rgba(255, 255, 255, 0.25);\n border-radius: 2px;\n overflow: visible;\n transition: height 0.1s ease;\n}\n\n.sp-progress:hover .sp-progress-track {\n height: var(--sp-bar-hover-height);\n}\n\n.sp-progress-buffered,\n.sp-progress-played {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n border-radius: 2px;\n}\n\n.sp-progress-buffered {\n background: rgba(255, 255, 255, 0.35);\n}\n\n.sp-progress-played {\n background: var(--sp-accent);\n}\n\n.sp-progress-thumb {\n position: absolute;\n top: 50%;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--sp-accent);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.1s ease;\n pointer-events: none;\n}\n\n.sp-progress:hover .sp-progress-thumb,\n.sp-progress.sp-dragging .sp-progress-thumb {\n transform: translate(-50%, -50%) scale(1);\n}\n\n.sp-progress-tooltip {\n position: absolute;\n bottom: 18px;\n transform: translateX(-50%);\n padding: 2px 6px;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n font-size: 12px;\n display: none;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.sp-progress:hover .sp-progress-tooltip {\n display: block;\n}\n\n/* ---------- \u63A7\u5236\u6309\u94AE\u884C ---------- */\n.sp-controls {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.sp-controls-spacer {\n flex: 1;\n}\n\n.sp-btn {\n width: var(--sp-control-size);\n height: var(--sp-control-size);\n border: none;\n background: transparent;\n color: var(--sp-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n padding: 0;\n opacity: 0.9;\n transition: opacity 0.1s ease, background 0.1s ease;\n}\n\n.sp-btn:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.15);\n}\n\n.sp-btn svg {\n width: 22px;\n height: 22px;\n}\n\n.sp-btn:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n}\n\n.sp-time {\n padding: 0 8px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n opacity: 0.95;\n}\n\n/* ---------- \u5F39\u51FA\u83DC\u5355 ---------- */\n.sp-menu {\n position: relative;\n}\n\n.sp-menu-btn {\n min-width: var(--sp-control-size);\n width: auto;\n padding: 0 8px;\n font-size: 13px;\n font-weight: 500;\n}\n\n.sp-menu-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n min-width: 80px;\n max-height: 220px;\n overflow-y: auto;\n background: rgba(28, 28, 28, 0.95);\n border-radius: 6px;\n padding: 4px 0;\n display: none;\n z-index: 25;\n}\n\n.sp-menu.sp-open .sp-menu-panel {\n display: block;\n}\n\n.sp-menu-item {\n display: block;\n width: 100%;\n padding: 7px 16px;\n border: none;\n background: transparent;\n color: var(--sp-text);\n text-align: center;\n cursor: pointer;\n font-size: 13px;\n white-space: nowrap;\n}\n\n.sp-menu-item:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n\n.sp-menu-item.sp-active {\n color: var(--sp-accent);\n font-weight: 600;\n}\n\n.sp-menu-empty {\n padding: 7px 16px;\n opacity: 0.5;\n white-space: nowrap;\n}\n\n/* ---------- \u72B6\u6001\u8499\u5C42\uFF1Aloading / \u9519\u8BEF / \u7ED3\u675F ---------- */\n.sp-state {\n position: absolute;\n inset: 0;\n z-index: 15;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n}\n\n.sp-spinner {\n opacity: 0;\n transition: opacity 0.2s ease 0.3s; /* \u5EF6\u8FDF\u51FA\u73B0\uFF0C\u907F\u514D\u77ED\u6682 seek \u95EA\u70C1 */\n}\n\n.sp-state.sp-loading .sp-spinner {\n opacity: 1;\n}\n\n.sp-spinner-ring {\n width: 44px;\n height: 44px;\n border: 4px solid rgba(255, 255, 255, 0.25);\n border-top-color: var(--sp-accent);\n border-radius: 50%;\n animation: sp-spin 0.8s linear infinite;\n}\n\n@keyframes sp-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.sp-state-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n background: rgba(0, 0, 0, 0.55);\n pointer-events: auto;\n}\n\n.sp-state-message {\n font-size: 15px;\n opacity: 0.95;\n}\n\n.sp-state-actions {\n display: flex;\n gap: 12px;\n}\n\n.sp-state-btn {\n padding: 8px 22px;\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 18px;\n background: transparent;\n color: var(--sp-text);\n font-size: 14px;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-state-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.sp-state-btn-primary {\n background: var(--sp-accent);\n border-color: var(--sp-accent);\n}\n\n.sp-state-btn-primary:hover {\n background: var(--sp-accent);\n filter: brightness(1.1);\n}\n\n.sp-state-countdown {\n font-size: 13px;\n opacity: 0.85;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.sp-state-cancel {\n border: none;\n background: transparent;\n color: var(--sp-accent);\n cursor: pointer;\n font-size: 13px;\n padding: 2px 6px;\n}\n\n/* ---------- \u97F3\u91CF ---------- */\n.sp-volume {\n display: flex;\n align-items: center;\n}\n\n.sp-volume-slider {\n width: 0;\n overflow: hidden;\n transition: width 0.15s ease;\n display: flex;\n align-items: center;\n}\n\n.sp-volume:hover .sp-volume-slider,\n.sp-volume.sp-dragging .sp-volume-slider {\n width: 64px;\n}\n\n.sp-volume-track {\n position: relative;\n width: 56px;\n height: var(--sp-bar-height);\n margin: 0 4px;\n background: rgba(255, 255, 255, 0.25);\n border-radius: 2px;\n cursor: pointer;\n}\n\n.sp-volume-fill {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-text);\n border-radius: 2px;\n}\n\n.sp-volume-thumb {\n position: absolute;\n top: 50%;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--sp-text);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n";
1076
1253
 
1077
1254
  // src/player.ts
1078
1255
  var DEFAULT_RATES = [0.5, 1, 1.5, 2];
@@ -1081,19 +1258,17 @@ var DEFAULT_LONG_SEEK_STEPS = [10, 30, 60];
1081
1258
  var DEFAULT_STEP_UP_INTERVAL = 2e3;
1082
1259
  var DEFAULT_AUTO_NEXT_SECONDS = 5;
1083
1260
  var CONTROLS_HIDE_DELAY = 3e3;
1084
- var TITLE_CLICK_COUNT = 10;
1085
- var TITLE_CLICK_WINDOW = 1500;
1086
1261
  var SINGLE_CLICK_DELAY = 250;
1262
+ var NPM_URL = "https://www.npmjs.com/package/@sweet-player/core";
1087
1263
  var PROGRESS_SAVE_INTERVAL = 5e3;
1088
1264
  var PROGRESS_END_GUARD = 10;
1089
1265
  var SweetPlayer = class {
1090
1266
  constructor(options) {
1091
1267
  this.emitter = new EventEmitter();
1268
+ this.contextMenu = null;
1092
1269
  this.hideTimer = null;
1093
1270
  this.clickTimer = null;
1094
1271
  this.progressTimer = null;
1095
- this.titleClicks = 0;
1096
- this.lastTitleClick = 0;
1097
1272
  this.currentRatio = "original";
1098
1273
  /** 画质/音轨菜单当前是否由 hls.js 自动接管(业务 setQualities 会关闭) */
1099
1274
  this.hlsManagedQuality = false;
@@ -1126,9 +1301,27 @@ var SweetPlayer = class {
1126
1301
  onLevels: autoQuality && !options.qualities?.length ? (levels) => this.applyHlsLevels(levels) : void 0,
1127
1302
  onAudioTracks: autoQuality && !options.audioTracks?.length ? (tracks) => this.applyHlsAudioTracks(tracks) : void 0
1128
1303
  });
1304
+ const hidden = new Set(options.hiddenControls ?? []);
1129
1305
  this.osd = createOsd();
1130
1306
  this.stats = createStatsOverlay(this.container, this.video, this.media, this.i18n);
1307
+ this.shortcutsPanel = createShortcutsOverlay(this.container, this.i18n, options.seekStep ?? 10);
1131
1308
  this.state = createStateOverlay(this.i18n);
1309
+ if (!hidden.has("contextMenu")) {
1310
+ this.contextMenu = createContextMenu(this.container, [
1311
+ {
1312
+ label: `${this.i18n.t("changelog")}: v${VERSION}`,
1313
+ onClick: () => window.open(NPM_URL, "_blank", "noopener")
1314
+ },
1315
+ {
1316
+ label: this.i18n.t("videoInfo"),
1317
+ onClick: () => this.stats.toggle()
1318
+ },
1319
+ {
1320
+ label: this.i18n.t("shortcuts"),
1321
+ onClick: () => this.shortcutsPanel.toggle()
1322
+ }
1323
+ ]);
1324
+ }
1132
1325
  this.controls = createControls({
1133
1326
  video: this.video,
1134
1327
  title: options.title ?? "",
@@ -1136,6 +1329,7 @@ var SweetPlayer = class {
1136
1329
  playbackRates: options.playbackRates ?? DEFAULT_RATES,
1137
1330
  aspectRatios: options.aspectRatios ?? DEFAULT_RATIOS,
1138
1331
  seekStep: options.seekStep ?? 10,
1332
+ hidden,
1139
1333
  actions: {
1140
1334
  togglePlay: () => this.toggle(),
1141
1335
  seekBy: (d) => this.seekBy(d),
@@ -1148,8 +1342,7 @@ var SweetPlayer = class {
1148
1342
  selectQuality: (q) => this.handleQualitySelect(q),
1149
1343
  selectAudioTrack: (t) => this.handleAudioTrackSelect(t),
1150
1344
  onPrev: options.onPrev,
1151
- onNext: options.onNext,
1152
- onTitleClick: () => this.handleTitleClick()
1345
+ onNext: options.onNext
1153
1346
  }
1154
1347
  });
1155
1348
  this.container.appendChild(this.controls.topEl);
@@ -1205,6 +1398,7 @@ var SweetPlayer = class {
1205
1398
  this.bindActivityTracking();
1206
1399
  this.disposers.push(
1207
1400
  onFullscreenChange(this.container, (fs) => {
1401
+ log("\u64AD\u653E\u5668", fs ? "\u8FDB\u5165\u5168\u5C4F" : "\u9000\u51FA\u5168\u5C4F");
1208
1402
  this.controls.updateFullscreen(fs);
1209
1403
  this.emitter.emit("fullscreenchange", fs);
1210
1404
  })
@@ -1240,6 +1434,7 @@ var SweetPlayer = class {
1240
1434
  if (showOsd) this.osd.flash(`${delta > 0 ? "+" : ""}${delta} ${this.i18n.t("seconds")}`);
1241
1435
  }
1242
1436
  setRate(rate) {
1437
+ log("\u64AD\u653E\u5668", `\u500D\u901F\u5207\u6362: ${rate}x`);
1243
1438
  this.video.playbackRate = rate;
1244
1439
  }
1245
1440
  /** 0-100 */
@@ -1326,7 +1521,9 @@ var SweetPlayer = class {
1326
1521
  this.keyboard.destroy();
1327
1522
  this.gestures.destroy();
1328
1523
  this.stats.destroy();
1524
+ this.shortcutsPanel.destroy();
1329
1525
  this.state.destroy();
1526
+ this.contextMenu?.destroy();
1330
1527
  this.controls.destroy();
1331
1528
  this.disposers.forEach((d) => d());
1332
1529
  if (this.hideTimer) clearTimeout(this.hideTimer);
@@ -1349,6 +1546,7 @@ var SweetPlayer = class {
1349
1546
  this.hlsManagedAudio = true;
1350
1547
  }
1351
1548
  handleQualitySelect(quality) {
1549
+ log("\u64AD\u653E\u5668", `\u5207\u6362\u753B\u8D28: ${quality.label}`);
1352
1550
  this.controls.qualityMenu.setActive(quality);
1353
1551
  if (this.hlsManagedQuality && typeof quality.value === "number") {
1354
1552
  this.media.setLevel(quality.value);
@@ -1361,6 +1559,7 @@ var SweetPlayer = class {
1361
1559
  this.emitter.emit("qualitychange", quality);
1362
1560
  }
1363
1561
  handleAudioTrackSelect(track) {
1562
+ log("\u64AD\u653E\u5668", `\u5207\u6362\u97F3\u8F68: ${track.label}`);
1364
1563
  this.controls.audioMenu.setActive(track);
1365
1564
  if (this.hlsManagedAudio && typeof track.value === "number") {
1366
1565
  this.media.setAudioTrack(track.value);
@@ -1401,15 +1600,6 @@ var SweetPlayer = class {
1401
1600
  }
1402
1601
  }
1403
1602
  // ---------- 内部:交互 ----------
1404
- handleTitleClick() {
1405
- const now = Date.now();
1406
- this.titleClicks = now - this.lastTitleClick <= TITLE_CLICK_WINDOW ? this.titleClicks + 1 : 1;
1407
- this.lastTitleClick = now;
1408
- if (this.titleClicks >= TITLE_CLICK_COUNT) {
1409
- this.titleClicks = 0;
1410
- this.stats.toggle();
1411
- }
1412
- }
1413
1603
  bindMediaEvents() {
1414
1604
  const v = this.video;
1415
1605
  const listen = (event, fn) => {
@@ -1417,6 +1607,7 @@ var SweetPlayer = class {
1417
1607
  this.disposers.push(() => v.removeEventListener(event, fn));
1418
1608
  };
1419
1609
  listen("loadedmetadata", () => {
1610
+ log("\u539F\u751F\u4E8B\u4EF6", `\u83B7\u53D6\u5143\u6570\u636E (${v.videoWidth}x${v.videoHeight})`);
1420
1611
  this.controls.updateTime();
1421
1612
  this.emitter.emit("ready", void 0);
1422
1613
  });
@@ -1432,6 +1623,7 @@ var SweetPlayer = class {
1432
1623
  this.emitter.emit("pause", void 0);
1433
1624
  });
1434
1625
  listen("ended", () => {
1626
+ log("\u539F\u751F\u4E8B\u4EF6", "\u64AD\u653E\u7ED3\u675F");
1435
1627
  this.saveProgressNow();
1436
1628
  this.showControls();
1437
1629
  const onNext = this.options.onNext;
@@ -1461,23 +1653,47 @@ var SweetPlayer = class {
1461
1653
  this.controls.volume.update(volume, v.muted);
1462
1654
  this.emitter.emit("volumechange", { volume, muted: v.muted });
1463
1655
  });
1464
- listen("waiting", () => this.state.showLoading());
1465
- listen("stalled", () => this.state.showLoading());
1466
- listen("seeking", () => this.state.showLoading());
1467
- listen("canplay", () => this.state.hideLoading());
1656
+ listen("waiting", () => {
1657
+ log("\u539F\u751F\u4E8B\u4EF6", "\u7B49\u5F85\u7F13\u51B2\u4E2D");
1658
+ this.state.showLoading();
1659
+ });
1660
+ listen("stalled", () => {
1661
+ log("\u539F\u751F\u4E8B\u4EF6", "\u6570\u636E\u52A0\u8F7D\u505C\u6EDE");
1662
+ this.state.showLoading();
1663
+ });
1664
+ listen("seeking", () => {
1665
+ log("\u539F\u751F\u4E8B\u4EF6", "\u5F00\u59CB\u8DF3\u8F6C");
1666
+ this.state.showLoading();
1667
+ });
1668
+ listen("canplay", () => {
1669
+ log("\u539F\u751F\u4E8B\u4EF6", "\u51C6\u5907\u597D\u5F00\u59CB\u64AD\u653E");
1670
+ this.state.hideLoading();
1671
+ });
1468
1672
  listen("playing", () => {
1469
1673
  this.state.hideLoading();
1470
1674
  this.state.hideError();
1471
1675
  });
1472
- listen("seeked", () => this.state.hideLoading());
1473
- listen("error", () => this.showErrorState({ type: "media", detail: v.error }));
1676
+ listen("seeked", () => {
1677
+ log("\u539F\u751F\u4E8B\u4EF6", "\u8DF3\u8F6C\u5B8C\u6210");
1678
+ this.state.hideLoading();
1679
+ });
1680
+ listen("error", () => {
1681
+ log("\u539F\u751F\u4E8B\u4EF6", `\u64AD\u653E\u9519\u8BEF: ${v.error?.message ?? "\u672A\u77E5\u9519\u8BEF"}`);
1682
+ this.showErrorState({ type: "media", detail: v.error });
1683
+ });
1474
1684
  this.disposers.push(
1475
1685
  this.emitter.on("error", (payload) => {
1476
1686
  if (payload.type.startsWith("hls-")) this.showErrorStateUi();
1477
1687
  })
1478
1688
  );
1479
- listen("enterpictureinpicture", () => this.emitter.emit("pipchange", true));
1480
- listen("leavepictureinpicture", () => this.emitter.emit("pipchange", false));
1689
+ listen("enterpictureinpicture", () => {
1690
+ log("\u64AD\u653E\u5668", "\u8FDB\u5165\u753B\u4E2D\u753B");
1691
+ this.emitter.emit("pipchange", true);
1692
+ });
1693
+ listen("leavepictureinpicture", () => {
1694
+ log("\u64AD\u653E\u5668", "\u9000\u51FA\u753B\u4E2D\u753B");
1695
+ this.emitter.emit("pipchange", false);
1696
+ });
1481
1697
  const onVideoClick = (e) => {
1482
1698
  if (e.target !== this.video || e.pointerType === "touch") return;
1483
1699
  if (this.clickTimer) return;
@@ -1544,8 +1760,10 @@ var SweetPlayer = class {
1544
1760
  });
1545
1761
  }
1546
1762
  };
1763
+ SweetPlayer.version = VERSION;
1547
1764
  // Annotate the CommonJS export names for ESM import in node:
1548
1765
  0 && (module.exports = {
1549
1766
  SweetPlayer,
1767
+ VERSION,
1550
1768
  registerLocale
1551
1769
  });