dsh-plugin-animegarden 0.1.6 → 0.1.7

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
@@ -1,16 +1,23 @@
1
1
  # 🌸 Anime Garden for DeepSeek Harness
2
2
 
3
+ [![version](https://img.shields.io/npm/v/dsh-plugin-animegarden?label=dsh-plugin-animegarden&logo=deepseek&logoColor=white&color=4D6BFE)](https://www.npmjs.com/package/dsh-plugin-animegarden)
4
+ [![CI](https://github.com/yjl9903/AnimeGarden/actions/workflows/ci.yml/badge.svg)](https://github.com/yjl9903/AnimeGarden/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/npm/l/dsh-plugin-animegarden)](https://github.com/yjl9903/AnimeGarden/blob/main/LICENSE)
6
+
3
7
  在 DeepSeek Harness 里搜索 Anime Garden 的动画 BT 资源.
4
8
 
5
9
  - **会话搜索**:让 AI 能搜索 Anime Garden 资源
6
10
  - **搜索面板**:输入即搜, 可按字幕组和类型筛选动画资源
7
11
  - **新番周历**:查看当季新番动画, 快速筛选动画资源
12
+ - **浮窗播放**:播放链接直接在 DSH 内以浮窗打开, 边看边聊
8
13
 
9
14
  ## 安装
10
15
 
11
16
  把下面这句发给你的 DeepSeek Harness:
12
17
 
13
- > 从 npm 安装最新版 dsh-plugin-animegarden 到当前运行的 DeepSeek Harness
18
+ ```text
19
+ 从 npm 安装最新版 dsh-plugin-animegarden 到当前运行的 DeepSeek Harness
20
+ ```
14
21
 
15
22
  也可以手动安装: **Plugins → Add plugin** 输入包名, 或执行 `dsh plugin add dsh-plugin-animegarden`.
16
23
 
package/dist/client.mjs CHANGED
@@ -4339,6 +4339,29 @@ function AnimeGardenIcon({ size = 18, active = false }) {
4339
4339
  );
4340
4340
  }
4341
4341
 
4342
+ const playbackZh = {
4343
+ playbackTitle: "\u64AD\u653E\u7A97\u53E3",
4344
+ playbackLoading: "\u6B63\u5728\u52A0\u8F7D\u64AD\u653E\u9875\u2026",
4345
+ playbackReload: "\u91CD\u65B0\u52A0\u8F7D",
4346
+ playbackOpenExternal: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00",
4347
+ playbackClose: "\u5173\u95ED\u64AD\u653E\u7A97\u53E3",
4348
+ playbackMinimize: "\u6700\u5C0F\u5316",
4349
+ playbackRestore: "\u8FD8\u539F\u7A97\u53E3",
4350
+ playbackActions: "\u64AD\u653E\u7A97\u53E3\u64CD\u4F5C",
4351
+ playbackMore: "\u66F4\u591A\u64CD\u4F5C"
4352
+ };
4353
+ const playbackEn = {
4354
+ playbackTitle: "Playback",
4355
+ playbackLoading: "Loading playback page\u2026",
4356
+ playbackReload: "Reload",
4357
+ playbackOpenExternal: "Open in browser",
4358
+ playbackClose: "Close player",
4359
+ playbackMinimize: "Minimize",
4360
+ playbackRestore: "Restore window",
4361
+ playbackActions: "Playback window actions",
4362
+ playbackMore: "More actions"
4363
+ };
4364
+
4342
4365
  const NS = "animegarden";
4343
4366
  const zh = {
4344
4367
  panel: "Anime Garden",
@@ -4385,7 +4408,8 @@ const zh = {
4385
4408
  size: "\u5927\u5C0F",
4386
4409
  download: "\u4E0B\u8F7D",
4387
4410
  copyMagnetSuccess: "\u78C1\u529B\u94FE\u63A5\u5DF2\u590D\u5236",
4388
- copyMagnetFailed: "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u78C1\u529B\u94FE\u63A5"
4411
+ copyMagnetFailed: "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u78C1\u529B\u94FE\u63A5",
4412
+ ...playbackZh
4389
4413
  };
4390
4414
  const en = {
4391
4415
  panel: "Anime Garden",
@@ -4432,7 +4456,8 @@ const en = {
4432
4456
  size: "Size",
4433
4457
  download: "Download",
4434
4458
  copyMagnetSuccess: "Magnet link copied",
4435
- copyMagnetFailed: "Copy failed, copy the magnet link manually"
4459
+ copyMagnetFailed: "Copy failed, copy the magnet link manually",
4460
+ ...playbackEn
4436
4461
  };
4437
4462
  function registerLocale(ctx) {
4438
4463
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "animegarden: dictionaries");
@@ -4530,7 +4555,7 @@ const ErrorIcon$1 = /*#__PURE__*/ lazyReact.createElement("svg", {
4530
4555
  d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z",
4531
4556
  clipRule: "evenodd"
4532
4557
  }));
4533
- const CloseIcon = /*#__PURE__*/ lazyReact.createElement("svg", {
4558
+ const CloseIcon$1 = /*#__PURE__*/ lazyReact.createElement("svg", {
4534
4559
  xmlns: "http://www.w3.org/2000/svg",
4535
4560
  width: "12",
4536
4561
  height: "12",
@@ -5290,7 +5315,7 @@ const Toast = (props)=>{
5290
5315
  toast.onDismiss == null ? void 0 : toast.onDismiss.call(toast, toast);
5291
5316
  },
5292
5317
  className: cn(classNames == null ? void 0 : classNames.closeButton, toast == null ? void 0 : (_toast_classNames2 = toast.classNames) == null ? void 0 : _toast_classNames2.closeButton)
5293
- }, (_icons_close = icons == null ? void 0 : icons.close) != null ? _icons_close : CloseIcon) : null, (toastType || toast.icon || toast.promise) && toast.icon !== null && ((icons == null ? void 0 : icons[toastType]) !== null || toast.icon) ? /*#__PURE__*/ lazyReact.createElement("div", {
5318
+ }, (_icons_close = icons == null ? void 0 : icons.close) != null ? _icons_close : CloseIcon$1) : null, (toastType || toast.icon || toast.promise) && toast.icon !== null && ((icons == null ? void 0 : icons[toastType]) !== null || toast.icon) ? /*#__PURE__*/ lazyReact.createElement("div", {
5294
5319
  "data-icon": "",
5295
5320
  className: cn(classNames == null ? void 0 : classNames.icon, toast == null ? void 0 : (_toast_classNames3 = toast.classNames) == null ? void 0 : _toast_classNames3.icon)
5296
5321
  }, toastType === 'loading' ? toast.icon || getLoadingIcon() : toast.promise ? getLoadingIcon() : null, toastType !== 'loading' ? icon : null) : null, /*#__PURE__*/ lazyReact.createElement("div", {
@@ -12491,6 +12516,7 @@ function ResourceRow({ t, resource, onRefineFansub, onCopyMagnet }) {
12491
12516
  href: resource.keepshareUrl,
12492
12517
  target: "_blank",
12493
12518
  rel: "noreferrer noopener",
12519
+ "data-agp-playback-title": resource.title,
12494
12520
  onClick: () => track("pikpak", { resource: resourceKey, source: "title" })
12495
12521
  },
12496
12522
  resource.title
@@ -12524,6 +12550,7 @@ function ResourceRow({ t, resource, onRefineFansub, onCopyMagnet }) {
12524
12550
  rel: "noreferrer noopener",
12525
12551
  "aria-label": t("play"),
12526
12552
  title: t("play"),
12553
+ "data-agp-playback-title": resource.title,
12527
12554
  onClick: () => track("pikpak", { resource: resourceKey, source: "play" })
12528
12555
  },
12529
12556
  /* @__PURE__ */ React.createElement(PlayIcon, { size: 18 })
@@ -13002,6 +13029,794 @@ function AnimeGardenPanelContent({ t: translate }) {
13002
13029
  ))), /* @__PURE__ */ React.createElement(Toaster, { position: "bottom-right", theme: "system" }));
13003
13030
  }
13004
13031
 
13032
+ const PLAYBACK_TITLE_ATTRIBUTE = "data-agp-playback-title";
13033
+ const PLAYBACK_IGNORE_ATTRIBUTE = "data-agp-playback-ignore";
13034
+ function isKeepShareHost(hostname) {
13035
+ return hostname === "keepshare.cc" || hostname.endsWith(".keepshare.cc");
13036
+ }
13037
+ function parsePlaybackHref(href) {
13038
+ try {
13039
+ const url = new URL(href, window.location.href);
13040
+ return url.protocol === "https:" && isKeepShareHost(url.hostname) ? url.toString() : null;
13041
+ } catch {
13042
+ return null;
13043
+ }
13044
+ }
13045
+ const GENERIC_LABELS = /* @__PURE__ */ new Set(["\u64AD\u653E", "play", "watch", "view"]);
13046
+ function playbackTitle(anchor) {
13047
+ const stamped = (anchor.getAttribute(PLAYBACK_TITLE_ATTRIBUTE) ?? "").trim();
13048
+ if (stamped !== "") return stamped;
13049
+ const attribute = (anchor.getAttribute("title") ?? "").trim();
13050
+ if (attribute !== "") return attribute;
13051
+ const text = (anchor.textContent ?? "").replace(/\s+/g, " ").replace(/^[▶►\s]+/, "").trim();
13052
+ return GENERIC_LABELS.has(text.toLowerCase()) ? "" : text;
13053
+ }
13054
+ function playbackClickFromEvent(event) {
13055
+ if (event.defaultPrevented || event.button !== 0) return null;
13056
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return null;
13057
+ const node = event.target instanceof Element ? event.target : null;
13058
+ const anchor = node?.closest("a[href]");
13059
+ if (!(anchor instanceof HTMLAnchorElement)) return null;
13060
+ if (anchor.hasAttribute(PLAYBACK_IGNORE_ATTRIBUTE)) return null;
13061
+ const url = parsePlaybackHref(anchor.getAttribute("href") ?? "");
13062
+ if (url === null) return null;
13063
+ return {
13064
+ target: { url, title: playbackTitle(anchor) },
13065
+ ownLink: anchor.hasAttribute(PLAYBACK_TITLE_ATTRIBUTE)
13066
+ };
13067
+ }
13068
+ function installPlaybackLinkInterceptor(open) {
13069
+ const onClick = (event) => {
13070
+ const click = playbackClickFromEvent(event);
13071
+ if (click === null) return;
13072
+ event.preventDefault();
13073
+ if (!click.ownLink) event.stopPropagation();
13074
+ open(click.target);
13075
+ };
13076
+ document.addEventListener("click", onClick, true);
13077
+ return () => document.removeEventListener("click", onClick, true);
13078
+ }
13079
+
13080
+ const GEOMETRY_STORAGE_KEY = "animegarden.playback.geometry";
13081
+ const MIN_PLAYBACK_WIDTH = 360;
13082
+ const MIN_PLAYBACK_HEIGHT = 240;
13083
+ const MINIMIZED_HEIGHT = 40;
13084
+ const PLAYBACK_INSET = 8;
13085
+ const MINIMIZED_MARGIN = PLAYBACK_INSET * 2;
13086
+ function readViewportSize() {
13087
+ return { width: window.innerWidth, height: window.innerHeight };
13088
+ }
13089
+ function clampGeometry(geometry, viewport) {
13090
+ const maxWidth = Math.max(240, viewport.width - PLAYBACK_INSET * 2);
13091
+ const maxHeight = Math.max(160, viewport.height - PLAYBACK_INSET * 2);
13092
+ const width = Math.min(Math.max(MIN_PLAYBACK_WIDTH, geometry.width), maxWidth);
13093
+ const height = Math.min(Math.max(MIN_PLAYBACK_HEIGHT, geometry.height), maxHeight);
13094
+ const x = Math.min(Math.max(PLAYBACK_INSET, geometry.x), viewport.width - width - PLAYBACK_INSET);
13095
+ const y = Math.min(
13096
+ Math.max(PLAYBACK_INSET, geometry.y),
13097
+ viewport.height - height - PLAYBACK_INSET
13098
+ );
13099
+ return {
13100
+ x: Math.round(x),
13101
+ y: Math.round(y),
13102
+ width: Math.round(width),
13103
+ height: Math.round(height)
13104
+ };
13105
+ }
13106
+ function defaultGeometry(viewport) {
13107
+ const width = Math.round(Math.min(1080, Math.max(560, viewport.width * 0.62)));
13108
+ const height = Math.round(Math.min(760, Math.max(420, viewport.height * 0.72)));
13109
+ return clampGeometry(
13110
+ {
13111
+ x: (viewport.width - width) / 2,
13112
+ y: Math.max(PLAYBACK_INSET, (viewport.height - height) / 2 - 24),
13113
+ width,
13114
+ height
13115
+ },
13116
+ viewport
13117
+ );
13118
+ }
13119
+ function minimizedGeometry(viewport, at) {
13120
+ const maxWidth = Math.max(200, viewport.width - MINIMIZED_MARGIN * 2);
13121
+ const width = Math.round(Math.min(380, Math.max(260, viewport.width * 0.28), maxWidth));
13122
+ const height = MINIMIZED_HEIGHT;
13123
+ const maxX = Math.max(MINIMIZED_MARGIN, viewport.width - width - MINIMIZED_MARGIN);
13124
+ const maxY = Math.max(MINIMIZED_MARGIN, viewport.height - height - MINIMIZED_MARGIN);
13125
+ const x = at === null ? maxX : Math.min(Math.max(MINIMIZED_MARGIN, at.x), maxX);
13126
+ const y = at === null ? maxY : Math.min(Math.max(MINIMIZED_MARGIN, at.y), maxY);
13127
+ return { x: Math.round(x), y: Math.round(y), width, height };
13128
+ }
13129
+ function resizeGeometry(origin, direction, dx, dy, viewport) {
13130
+ const west = direction === "nw" || direction === "sw";
13131
+ const north = direction === "nw" || direction === "ne";
13132
+ let width;
13133
+ let x;
13134
+ if (west) {
13135
+ const maxWidth = Math.max(MIN_PLAYBACK_WIDTH, origin.x + origin.width - PLAYBACK_INSET);
13136
+ width = Math.min(Math.max(MIN_PLAYBACK_WIDTH, origin.width - dx), maxWidth);
13137
+ x = origin.x + origin.width - width;
13138
+ } else {
13139
+ const maxWidth = Math.max(MIN_PLAYBACK_WIDTH, viewport.width - origin.x - PLAYBACK_INSET);
13140
+ width = Math.min(Math.max(MIN_PLAYBACK_WIDTH, origin.width + dx), maxWidth);
13141
+ x = origin.x;
13142
+ }
13143
+ let height;
13144
+ let y;
13145
+ if (north) {
13146
+ const maxHeight = Math.max(MIN_PLAYBACK_HEIGHT, origin.y + origin.height - PLAYBACK_INSET);
13147
+ height = Math.min(Math.max(MIN_PLAYBACK_HEIGHT, origin.height - dy), maxHeight);
13148
+ y = origin.y + origin.height - height;
13149
+ } else {
13150
+ const maxHeight = Math.max(MIN_PLAYBACK_HEIGHT, viewport.height - origin.y - PLAYBACK_INSET);
13151
+ height = Math.min(Math.max(MIN_PLAYBACK_HEIGHT, origin.height + dy), maxHeight);
13152
+ y = origin.y;
13153
+ }
13154
+ return {
13155
+ x: Math.round(x),
13156
+ y: Math.round(y),
13157
+ width: Math.round(width),
13158
+ height: Math.round(height)
13159
+ };
13160
+ }
13161
+ function loadGeometry() {
13162
+ try {
13163
+ const raw = window.localStorage.getItem(GEOMETRY_STORAGE_KEY);
13164
+ if (raw === null) return null;
13165
+ const parsed = JSON.parse(raw);
13166
+ if (typeof parsed !== "object" || parsed === null) return null;
13167
+ const value = parsed;
13168
+ const fields = ["x", "y", "width", "height"];
13169
+ if (!fields.every((field) => typeof value[field] === "number")) return null;
13170
+ return {
13171
+ x: value.x,
13172
+ y: value.y,
13173
+ width: value.width,
13174
+ height: value.height
13175
+ };
13176
+ } catch {
13177
+ return null;
13178
+ }
13179
+ }
13180
+ function saveGeometry(geometry) {
13181
+ try {
13182
+ window.localStorage.setItem(GEOMETRY_STORAGE_KEY, JSON.stringify(geometry));
13183
+ } catch {
13184
+ }
13185
+ }
13186
+ function createPlaybackStore() {
13187
+ const store = new Store({
13188
+ target: null,
13189
+ geometry: loadGeometry(),
13190
+ minimized: false,
13191
+ minimizedAt: null,
13192
+ nonce: 0
13193
+ });
13194
+ return {
13195
+ store,
13196
+ /** Show one playback page, replacing whatever the window currently holds. */
13197
+ open(target) {
13198
+ store.setState((state) => ({
13199
+ ...state,
13200
+ target,
13201
+ minimized: false,
13202
+ nonce: state.nonce + 1
13203
+ }));
13204
+ },
13205
+ /** Close the window; unmounting the iframe also stops playback. */
13206
+ close() {
13207
+ store.setState((state) => ({ ...state, target: null, minimized: false }));
13208
+ },
13209
+ /** Collapse to the title bar while the iframe keeps playing. */
13210
+ minimize() {
13211
+ store.setState((state) => ({ ...state, minimized: true }));
13212
+ },
13213
+ /** Expand back to the last floating rectangle. */
13214
+ restore() {
13215
+ store.setState((state) => ({ ...state, minimized: false }));
13216
+ },
13217
+ /** Remount the iframe with the same URL, matching a browser reload. */
13218
+ reload() {
13219
+ store.setState((state) => ({ ...state, target: state.target, nonce: state.nonce + 1 }));
13220
+ },
13221
+ /** Remember a user-chosen rectangle so the next open restores the placement. */
13222
+ commitGeometry(geometry) {
13223
+ const clamped = clampGeometry(geometry, readViewportSize());
13224
+ store.setState((state) => ({ ...state, geometry: clamped }));
13225
+ saveGeometry(clamped);
13226
+ },
13227
+ /** Remember where the user dragged the minimized title bar. */
13228
+ commitMinimizedAt(point) {
13229
+ const bar = minimizedGeometry(readViewportSize(), point);
13230
+ store.setState((state) => ({ ...state, minimizedAt: { x: bar.x, y: bar.y } }));
13231
+ }
13232
+ };
13233
+ }
13234
+
13235
+ const PLAYBACK_CSS = `
13236
+ .agp-play {
13237
+ position: absolute;
13238
+ z-index: 30;
13239
+ display: flex;
13240
+ flex-direction: column;
13241
+ box-sizing: border-box;
13242
+ overflow: hidden;
13243
+ color: var(--dsw-alias-label-primary);
13244
+ font-size: 13px;
13245
+ background: var(--dsw-alias-bg-overlay);
13246
+ border: 0.5px solid var(--dsw-alias-border-l1);
13247
+ border-radius: var(--dsw-radius-md, 10px);
13248
+ box-shadow: var(--dsw-elevation-panel);
13249
+ pointer-events: auto;
13250
+ animation: agp-play-in 140ms ease-out;
13251
+ }
13252
+ .agp-play:focus {
13253
+ outline: none;
13254
+ }
13255
+ .agp-play-head {
13256
+ display: flex;
13257
+ flex: none;
13258
+ align-items: center;
13259
+ gap: 8px;
13260
+ box-sizing: border-box;
13261
+ height: 40px;
13262
+ padding: 0 6px 0 12px;
13263
+ cursor: grab;
13264
+ user-select: none;
13265
+ -webkit-user-select: none;
13266
+ background: var(--dsw-alias-bg-layer-1);
13267
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
13268
+ touch-action: none;
13269
+ }
13270
+ .agp-play-head-dragging {
13271
+ cursor: grabbing;
13272
+ }
13273
+ .agp-play-mark {
13274
+ flex: none;
13275
+ font-size: 13px;
13276
+ line-height: 1;
13277
+ }
13278
+ .agp-play-title {
13279
+ flex: 1;
13280
+ min-width: 0;
13281
+ overflow: hidden;
13282
+ font-size: 13px;
13283
+ font-weight: 500;
13284
+ line-height: 18px;
13285
+ text-overflow: ellipsis;
13286
+ white-space: nowrap;
13287
+ }
13288
+ .agp-play-actions {
13289
+ display: flex;
13290
+ flex: none;
13291
+ align-items: center;
13292
+ gap: 2px;
13293
+ }
13294
+ /* The overflow anchor keeps secondary actions out of the title row. */
13295
+ .agp-play-more {
13296
+ position: relative;
13297
+ display: inline-flex;
13298
+ margin-right: 6px;
13299
+ }
13300
+ .agp-play-menu {
13301
+ position: absolute;
13302
+ top: calc(100% + 4px);
13303
+ right: 0;
13304
+ z-index: 3;
13305
+ box-sizing: border-box;
13306
+ min-width: 168px;
13307
+ padding: 4px;
13308
+ background: var(--dsw-alias-bg-layer-1);
13309
+ border: 0.5px solid var(--dsw-alias-border-l1);
13310
+ border-radius: var(--dsw-radius-sm, 6px);
13311
+ box-shadow: var(--dsw-elevation-panel);
13312
+ }
13313
+ .agp-play-menu-item {
13314
+ box-sizing: border-box;
13315
+ display: flex;
13316
+ align-items: center;
13317
+ gap: 8px;
13318
+ width: 100%;
13319
+ height: 28px;
13320
+ padding: 0 8px;
13321
+ color: var(--dsw-alias-label-primary);
13322
+ font: inherit;
13323
+ font-size: 12px;
13324
+ line-height: 18px;
13325
+ text-align: left;
13326
+ text-decoration: none;
13327
+ white-space: nowrap;
13328
+ cursor: pointer;
13329
+ background: transparent;
13330
+ border: none;
13331
+ border-radius: var(--dsw-radius-sm, 6px);
13332
+ }
13333
+ .agp-play-menu-item:hover {
13334
+ background: var(--dsw-alias-interactive-bg-hover);
13335
+ }
13336
+ .agp-play-menu-item:focus-visible {
13337
+ outline: var(--dsw-focus-ring-width, 2px) solid
13338
+ var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary));
13339
+ outline-offset: -1px;
13340
+ }
13341
+ .agp-play-action {
13342
+ box-sizing: border-box;
13343
+ display: inline-flex;
13344
+ align-items: center;
13345
+ justify-content: center;
13346
+ width: 28px;
13347
+ height: 28px;
13348
+ padding: 0;
13349
+ color: var(--dsw-alias-label-secondary);
13350
+ font: inherit;
13351
+ text-decoration: none;
13352
+ cursor: pointer;
13353
+ background: transparent;
13354
+ border: none;
13355
+ border-radius: var(--dsw-radius-sm, 6px);
13356
+ }
13357
+ .agp-play-action:hover {
13358
+ color: var(--dsw-alias-label-primary);
13359
+ background: var(--dsw-alias-interactive-bg-hover);
13360
+ }
13361
+ /* Close is the destructive window control: last, and tinted on hover. */
13362
+ .agp-play-action-close:hover {
13363
+ color: var(--dsw-alias-state-error-primary);
13364
+ }
13365
+ .agp-play-action:focus-visible {
13366
+ outline: var(--dsw-focus-ring-width, 2px) solid
13367
+ var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary));
13368
+ outline-offset: 1px;
13369
+ }
13370
+ .agp-play-body {
13371
+ position: relative;
13372
+ flex: 1;
13373
+ min-height: 0;
13374
+ background: var(--dsw-alias-bg-base);
13375
+ }
13376
+ .agp-play-frame {
13377
+ display: block;
13378
+ width: 100%;
13379
+ height: 100%;
13380
+ background: var(--dsw-alias-bg-base);
13381
+ border: 0;
13382
+ }
13383
+ .agp-play-loading {
13384
+ position: absolute;
13385
+ inset: 0;
13386
+ display: flex;
13387
+ align-items: center;
13388
+ justify-content: center;
13389
+ background: var(--dsw-alias-bg-base);
13390
+ }
13391
+ /* Four invisible corner resize handles. The top two are L-clipped so the header's
13392
+ * trailing controls keep their own pointer area; the directional resize cursor is
13393
+ * the only affordance, so no corner marks are drawn. */
13394
+ .agp-play-corner {
13395
+ position: absolute;
13396
+ z-index: 2;
13397
+ width: 16px;
13398
+ height: 16px;
13399
+ touch-action: none;
13400
+ }
13401
+ .agp-play-corner-nw {
13402
+ top: 0;
13403
+ left: 0;
13404
+ cursor: nwse-resize;
13405
+ clip-path: polygon(0 0, 100% 0, 100% 8px, 8px 8px, 8px 100%, 0 100%);
13406
+ }
13407
+ .agp-play-corner-ne {
13408
+ top: 0;
13409
+ right: 0;
13410
+ cursor: nesw-resize;
13411
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 8px 100%, 8px 8px, 0 8px);
13412
+ }
13413
+ .agp-play-corner-sw {
13414
+ bottom: 0;
13415
+ left: 0;
13416
+ cursor: nesw-resize;
13417
+ }
13418
+ .agp-play-corner-se {
13419
+ right: 0;
13420
+ bottom: 0;
13421
+ cursor: nwse-resize;
13422
+ }
13423
+ @keyframes agp-play-in {
13424
+ from {
13425
+ opacity: 0;
13426
+ transform: translateY(4px) scale(0.995);
13427
+ }
13428
+ to {
13429
+ opacity: 1;
13430
+ transform: none;
13431
+ }
13432
+ }
13433
+ /* Minimized: only the title bar is visible. The body stays mounted (so the iframe
13434
+ * keeps playing) and is hidden with display: none; the corner handles are unmounted
13435
+ * by the component and this rule is a belt-and-braces guard. */
13436
+ .agp-play[data-minimized='true'] .agp-play-body,
13437
+ .agp-play[data-minimized='true'] .agp-play-corner {
13438
+ display: none;
13439
+ }
13440
+ .agp-play[data-minimized='true'] .agp-play-head {
13441
+ border-bottom: 0;
13442
+ }
13443
+ .agp-play[data-minimized='true'] .agp-play-title {
13444
+ color: var(--dsw-alias-label-secondary);
13445
+ }
13446
+ @media (prefers-reduced-motion: reduce) {
13447
+ .agp-play {
13448
+ animation: none;
13449
+ }
13450
+ }
13451
+ `;
13452
+
13453
+ function CloseIcon({ size = 16 }) {
13454
+ const React = getReact();
13455
+ return /* @__PURE__ */ React.createElement(
13456
+ "svg",
13457
+ {
13458
+ viewBox: "0 0 24 24",
13459
+ width: size,
13460
+ height: size,
13461
+ fill: "none",
13462
+ "aria-hidden": "true",
13463
+ focusable: "false"
13464
+ },
13465
+ /* @__PURE__ */ React.createElement(
13466
+ "path",
13467
+ {
13468
+ d: "M6 6l12 12M18 6L6 18",
13469
+ stroke: "currentColor",
13470
+ strokeWidth: 1.8,
13471
+ strokeLinecap: "round"
13472
+ }
13473
+ )
13474
+ );
13475
+ }
13476
+ function MinimizeIcon({ size = 16 }) {
13477
+ const React = getReact();
13478
+ return /* @__PURE__ */ React.createElement(
13479
+ "svg",
13480
+ {
13481
+ viewBox: "0 0 24 24",
13482
+ width: size,
13483
+ height: size,
13484
+ fill: "none",
13485
+ "aria-hidden": "true",
13486
+ focusable: "false"
13487
+ },
13488
+ /* @__PURE__ */ React.createElement("path", { d: "M5 12h14", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round" })
13489
+ );
13490
+ }
13491
+ function RestoreIcon({ size = 16 }) {
13492
+ const React = getReact();
13493
+ return /* @__PURE__ */ React.createElement(
13494
+ "svg",
13495
+ {
13496
+ viewBox: "0 0 24 24",
13497
+ width: size,
13498
+ height: size,
13499
+ fill: "none",
13500
+ "aria-hidden": "true",
13501
+ focusable: "false"
13502
+ },
13503
+ /* @__PURE__ */ React.createElement(
13504
+ "path",
13505
+ {
13506
+ d: "M6 14l6-6 6 6",
13507
+ stroke: "currentColor",
13508
+ strokeWidth: 1.8,
13509
+ strokeLinecap: "round",
13510
+ strokeLinejoin: "round"
13511
+ }
13512
+ )
13513
+ );
13514
+ }
13515
+ function MoreIcon({ size = 16 }) {
13516
+ const React = getReact();
13517
+ return /* @__PURE__ */ React.createElement(
13518
+ "svg",
13519
+ {
13520
+ viewBox: "0 0 24 24",
13521
+ width: size,
13522
+ height: size,
13523
+ fill: "currentColor",
13524
+ "aria-hidden": "true",
13525
+ focusable: "false"
13526
+ },
13527
+ /* @__PURE__ */ React.createElement("circle", { cx: "5.5", cy: "12", r: "1.6" }),
13528
+ /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "1.6" }),
13529
+ /* @__PURE__ */ React.createElement("circle", { cx: "18.5", cy: "12", r: "1.6" })
13530
+ );
13531
+ }
13532
+
13533
+ function usePlaybackState(store, selector) {
13534
+ const React = getReact();
13535
+ return React.useSyncExternalStore(
13536
+ (onStoreChange) => {
13537
+ const subscription = store.store.subscribe(onStoreChange);
13538
+ return () => subscription.unsubscribe();
13539
+ },
13540
+ () => selector(store.store.state),
13541
+ () => selector(store.store.state)
13542
+ );
13543
+ }
13544
+
13545
+ const RESIZE_DIRECTIONS = ["nw", "ne", "sw", "se"];
13546
+ const CLICK_SLOP = 4;
13547
+ const INTERACTIVE_SELECTOR = 'button, a, input, select, textarea, [role="button"], [role="menu"]';
13548
+ function createPlaybackWindow(playback, fallbackT) {
13549
+ return function PlaybackWindow(props) {
13550
+ const React = getReact();
13551
+ const t = typeof props.t === "function" ? props.t : fallbackT;
13552
+ const target = usePlaybackState(playback, (state) => state.target);
13553
+ const storedGeometry = usePlaybackState(playback, (state) => state.geometry);
13554
+ const minimized = usePlaybackState(playback, (state) => state.minimized);
13555
+ const minimizedAt = usePlaybackState(playback, (state) => state.minimizedAt);
13556
+ const nonce = usePlaybackState(playback, (state) => state.nonce);
13557
+ const [viewport, setViewport] = React.useState(readViewportSize);
13558
+ const [dragGeometry, setDragGeometry] = React.useState(null);
13559
+ const [loaded, setLoaded] = React.useState(false);
13560
+ const [menuOpen, setMenuOpen] = React.useState(false);
13561
+ const drag = React.useRef(null);
13562
+ const root = React.useRef(null);
13563
+ const more = React.useRef(null);
13564
+ const menu = React.useRef(null);
13565
+ React.useEffect(() => {
13566
+ const onResize = () => setViewport(readViewportSize());
13567
+ window.addEventListener("resize", onResize);
13568
+ return () => window.removeEventListener("resize", onResize);
13569
+ }, []);
13570
+ React.useEffect(() => {
13571
+ setLoaded(false);
13572
+ }, [target, nonce]);
13573
+ React.useEffect(() => {
13574
+ if (target !== null) root.current?.focus();
13575
+ }, [target]);
13576
+ React.useEffect(() => {
13577
+ setMenuOpen(false);
13578
+ }, [target, nonce, minimized]);
13579
+ React.useEffect(() => {
13580
+ if (!menuOpen) return;
13581
+ const onPointerDown = (event) => {
13582
+ const node = event.target instanceof Node ? event.target : null;
13583
+ if (node !== null && (menu.current?.contains(node) || more.current?.contains(node))) {
13584
+ return;
13585
+ }
13586
+ setMenuOpen(false);
13587
+ };
13588
+ const onKeyDown = (event) => {
13589
+ if (event.key !== "Escape") return;
13590
+ event.preventDefault();
13591
+ event.stopPropagation();
13592
+ setMenuOpen(false);
13593
+ };
13594
+ document.addEventListener("pointerdown", onPointerDown, true);
13595
+ document.addEventListener("keydown", onKeyDown, true);
13596
+ return () => {
13597
+ document.removeEventListener("pointerdown", onPointerDown, true);
13598
+ document.removeEventListener("keydown", onKeyDown, true);
13599
+ };
13600
+ }, [menuOpen]);
13601
+ if (target === null) return null;
13602
+ const expandedGeometry = clampGeometry(
13603
+ dragGeometry ?? storedGeometry ?? defaultGeometry(viewport),
13604
+ viewport
13605
+ );
13606
+ const geometry = minimized ? dragGeometry === null ? minimizedGeometry(viewport, minimizedAt) : minimizedGeometry(viewport, { x: dragGeometry.x, y: dragGeometry.y }) : expandedGeometry;
13607
+ const title = target.title === "" ? t("playbackTitle") : target.title;
13608
+ const beginDrag = (mode) => (event) => {
13609
+ if (event.button !== 0) return;
13610
+ const control = event.target instanceof Element ? event.target.closest(INTERACTIVE_SELECTOR) : null;
13611
+ if (mode === "move" && control !== null) return;
13612
+ drag.current = {
13613
+ mode,
13614
+ pointerId: event.pointerId,
13615
+ startX: event.clientX,
13616
+ startY: event.clientY,
13617
+ origin: geometry,
13618
+ moved: false
13619
+ };
13620
+ event.currentTarget.setPointerCapture(event.pointerId);
13621
+ event.preventDefault();
13622
+ };
13623
+ const onPointerMove = (event) => {
13624
+ const session = drag.current;
13625
+ if (session === null || session.pointerId !== event.pointerId) return;
13626
+ if (minimized && session.mode !== "move") return;
13627
+ const dx = event.clientX - session.startX;
13628
+ const dy = event.clientY - session.startY;
13629
+ if (!session.moved && Math.abs(dx) <= CLICK_SLOP && Math.abs(dy) <= CLICK_SLOP) return;
13630
+ session.moved = true;
13631
+ if (session.mode === "move") {
13632
+ const next = { ...session.origin, x: session.origin.x + dx, y: session.origin.y + dy };
13633
+ setDragGeometry(
13634
+ minimized ? minimizedGeometry(viewport, next) : clampGeometry(next, viewport)
13635
+ );
13636
+ return;
13637
+ }
13638
+ setDragGeometry(resizeGeometry(session.origin, session.mode, dx, dy, viewport));
13639
+ };
13640
+ const endDrag = (event) => {
13641
+ const session = drag.current;
13642
+ if (session === null || session.pointerId !== event.pointerId) return;
13643
+ drag.current = null;
13644
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
13645
+ event.currentTarget.releasePointerCapture(event.pointerId);
13646
+ }
13647
+ const barClick = minimized && session.mode === "move" && !session.moved;
13648
+ if (dragGeometry !== null) {
13649
+ if (minimized) playback.commitMinimizedAt({ x: dragGeometry.x, y: dragGeometry.y });
13650
+ else playback.commitGeometry(dragGeometry);
13651
+ }
13652
+ setDragGeometry(null);
13653
+ if (barClick) playback.restore();
13654
+ };
13655
+ return /* @__PURE__ */ React.createElement(
13656
+ "div",
13657
+ {
13658
+ ref: root,
13659
+ className: "agp-play",
13660
+ role: "dialog",
13661
+ "aria-modal": "false",
13662
+ "aria-label": title,
13663
+ tabIndex: -1,
13664
+ "data-minimized": minimized ? "true" : void 0,
13665
+ style: {
13666
+ left: `${geometry.x}px`,
13667
+ top: `${geometry.y}px`,
13668
+ width: `${geometry.width}px`,
13669
+ height: `${geometry.height}px`
13670
+ },
13671
+ onKeyDown: (event) => {
13672
+ if (event.key !== "Escape") return;
13673
+ event.stopPropagation();
13674
+ playback.close();
13675
+ }
13676
+ },
13677
+ /* @__PURE__ */ React.createElement("style", null, PLAYBACK_CSS),
13678
+ /* @__PURE__ */ React.createElement(
13679
+ "header",
13680
+ {
13681
+ className: dragGeometry === null ? "agp-play-head" : "agp-play-head agp-play-head-dragging",
13682
+ onPointerDown: beginDrag("move"),
13683
+ onPointerMove,
13684
+ onPointerUp: endDrag,
13685
+ onPointerCancel: endDrag
13686
+ },
13687
+ /* @__PURE__ */ React.createElement("span", { className: "agp-play-mark", "aria-hidden": "true" }, "\u{1F338}"),
13688
+ /* @__PURE__ */ React.createElement("span", { className: "agp-play-title", title }, title),
13689
+ /* @__PURE__ */ React.createElement("div", { className: "agp-play-actions", role: "toolbar", "aria-label": t("playbackActions") }, minimized ? null : /* @__PURE__ */ React.createElement("div", { className: "agp-play-more", ref: more }, /* @__PURE__ */ React.createElement(
13690
+ "button",
13691
+ {
13692
+ type: "button",
13693
+ className: "agp-play-action",
13694
+ "aria-label": t("playbackMore"),
13695
+ title: t("playbackMore"),
13696
+ "aria-haspopup": "menu",
13697
+ "aria-expanded": menuOpen,
13698
+ onClick: () => setMenuOpen((open) => !open)
13699
+ },
13700
+ /* @__PURE__ */ React.createElement(MoreIcon, null)
13701
+ ), menuOpen ? /* @__PURE__ */ React.createElement(
13702
+ "div",
13703
+ {
13704
+ className: "agp-play-menu",
13705
+ role: "menu",
13706
+ "aria-label": t("playbackActions"),
13707
+ ref: menu
13708
+ },
13709
+ /* @__PURE__ */ React.createElement(
13710
+ "button",
13711
+ {
13712
+ type: "button",
13713
+ role: "menuitem",
13714
+ className: "agp-play-menu-item",
13715
+ onClick: () => {
13716
+ setMenuOpen(false);
13717
+ playback.reload();
13718
+ }
13719
+ },
13720
+ /* @__PURE__ */ React.createElement(RefreshIcon, null),
13721
+ /* @__PURE__ */ React.createElement("span", null, t("playbackReload"))
13722
+ ),
13723
+ /* @__PURE__ */ React.createElement(
13724
+ "a",
13725
+ {
13726
+ role: "menuitem",
13727
+ className: "agp-play-menu-item",
13728
+ href: target.url,
13729
+ target: "_blank",
13730
+ rel: "noreferrer noopener",
13731
+ "data-agp-playback-ignore": "",
13732
+ onClick: () => setMenuOpen(false)
13733
+ },
13734
+ /* @__PURE__ */ React.createElement(LaunchIcon, { size: 14 }),
13735
+ /* @__PURE__ */ React.createElement("span", null, t("playbackOpenExternal"))
13736
+ )
13737
+ ) : null), minimized ? /* @__PURE__ */ React.createElement(
13738
+ "button",
13739
+ {
13740
+ type: "button",
13741
+ className: "agp-play-action",
13742
+ "aria-label": t("playbackRestore"),
13743
+ title: t("playbackRestore"),
13744
+ onClick: () => playback.restore()
13745
+ },
13746
+ /* @__PURE__ */ React.createElement(RestoreIcon, null)
13747
+ ) : /* @__PURE__ */ React.createElement(
13748
+ "button",
13749
+ {
13750
+ type: "button",
13751
+ className: "agp-play-action",
13752
+ "aria-label": t("playbackMinimize"),
13753
+ title: t("playbackMinimize"),
13754
+ onClick: () => playback.minimize()
13755
+ },
13756
+ /* @__PURE__ */ React.createElement(MinimizeIcon, null)
13757
+ ), /* @__PURE__ */ React.createElement(
13758
+ "button",
13759
+ {
13760
+ type: "button",
13761
+ className: "agp-play-action agp-play-action-close",
13762
+ "aria-label": t("playbackClose"),
13763
+ title: t("playbackClose"),
13764
+ onClick: () => playback.close()
13765
+ },
13766
+ /* @__PURE__ */ React.createElement(CloseIcon, null)
13767
+ ))
13768
+ ),
13769
+ /* @__PURE__ */ React.createElement("div", { className: "agp-play-body" }, /* @__PURE__ */ React.createElement(
13770
+ "iframe",
13771
+ {
13772
+ key: `${target.url}#${String(nonce)}`,
13773
+ className: "agp-play-frame",
13774
+ src: target.url,
13775
+ title,
13776
+ allow: "autoplay; fullscreen; picture-in-picture; clipboard-read; clipboard-write; encrypted-media",
13777
+ allowFullScreen: true,
13778
+ referrerPolicy: "no-referrer",
13779
+ onLoad: () => setLoaded(true)
13780
+ }
13781
+ ), loaded ? null : /* @__PURE__ */ React.createElement("div", { className: "agp-play-loading" }, /* @__PURE__ */ React.createElement(LoadingStatus, { label: t("playbackLoading") }))),
13782
+ minimized ? null : RESIZE_DIRECTIONS.map((direction) => /* @__PURE__ */ React.createElement(
13783
+ "div",
13784
+ {
13785
+ key: direction,
13786
+ className: `agp-play-corner agp-play-corner-${direction}`,
13787
+ "aria-hidden": "true",
13788
+ onPointerDown: beginDrag(direction),
13789
+ onPointerMove,
13790
+ onPointerUp: endDrag,
13791
+ onPointerCancel: endDrag
13792
+ }
13793
+ ))
13794
+ );
13795
+ };
13796
+ }
13797
+
13798
+ const PLAYBACK_SLOT_ID = "animegarden.playback";
13799
+ function registerPlaybackFeature(ctx, t) {
13800
+ const playback = createPlaybackStore();
13801
+ const PlaybackWindow = createPlaybackWindow(playback, t);
13802
+ ctx.effect(
13803
+ () => installPlaybackLinkInterceptor((target) => playback.open(target)),
13804
+ "animegarden: playback link interceptor"
13805
+ );
13806
+ ctx.slots.inject(
13807
+ "shell.overlay",
13808
+ () => ctx.slots.register(
13809
+ {
13810
+ name: "shell.overlay",
13811
+ id: PLAYBACK_SLOT_ID,
13812
+ order: 30,
13813
+ locale: NS
13814
+ },
13815
+ PlaybackWindow
13816
+ )
13817
+ );
13818
+ }
13819
+
13005
13820
  const PANEL_ID = "animegarden";
13006
13821
  function createPlugin(ReactRuntime) {
13007
13822
  setReact(ReactRuntime);
@@ -13019,6 +13834,7 @@ function createPlugin(ReactRuntime) {
13019
13834
  inject: ["slots", "locale"],
13020
13835
  apply(ctx) {
13021
13836
  const t = registerLocale(ctx);
13837
+ registerPlaybackFeature(ctx, t);
13022
13838
  ctx.slots.inject(
13023
13839
  "sidebar.panellist",
13024
13840
  () => ctx.slots.register(
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Anime Garden",
4
- "description": "Search anime torrent resources from 動漫花園, 蜜柑计划, 萌番组, and ANi through Anime Garden."
4
+ "description": "Anime Garden is an anime torrent aggregator that lets you search for anime resources from 動漫花園, 蜜柑计划, 萌番组, and ANi in one place."
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-animegarden",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Anime Garden 是一个动画 BT 资源聚合站, 你可以在这里聚合搜索来自動漫花園、蜜柑计划、萌番组和 ANi 的动画资源.",
5
5
  "keywords": [
6
6
  "anime",