Initial snapshot before step 6 options.js split
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
host/__pycache__/
|
||||
host/logs/
|
||||
host/state/
|
||||
@@ -0,0 +1,169 @@
|
||||
# rclone-jav (Brave extension + native messaging host)
|
||||
|
||||
Session memory for Claude. Read before making changes here.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Brave tab title -> content script extracts JAV ID
|
||||
-> background.js connectNative("com.rcjav.host")
|
||||
-> host/rcjav-host.bat (portable: py launcher or python on PATH)
|
||||
-> host/rcjav-host.py
|
||||
-> subprocess python rc-jav.py --search ID --basic --no-color --format json
|
||||
-> structured hits back through native port
|
||||
-> popup or in-page overlay
|
||||
```
|
||||
|
||||
Two separate codebases:
|
||||
- This repo: Brave extension + native messaging host.
|
||||
- `D:\DEV\Project\rclone-jav\` — Python rc-jav CLI. The host shells out to `rc-jav.py` here.
|
||||
|
||||
## Folder layout (post-rename)
|
||||
|
||||
```
|
||||
D:\DEV\Extensions\Production\rclone-jav\ (PC 1)
|
||||
D:\DEV\Extensions\Staging\rclone-jav\ (PC 2)
|
||||
├── manifest.json
|
||||
├── background.js
|
||||
├── content.js
|
||||
├── popup.{html,js,css}
|
||||
├── options.{html,js}
|
||||
├── host\
|
||||
│ ├── rcjav-host.py
|
||||
│ ├── rcjav-host.bat (portable: py launcher fallback)
|
||||
│ ├── install-host.ps1 (self-elevates to HKLM)
|
||||
│ ├── register-host.bat (prompts for ID, calls install-host.ps1)
|
||||
│ ├── com.rcjav.host.json (generated; UTF-8 NO BOM)
|
||||
│ └── (logs)
|
||||
└── docs\
|
||||
├── INSTALL.md (gotcha table at the bottom)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Critical gotchas (learned the hard way)
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| "Specified native messaging host not found" | UTF-8 BOM in com.rcjav.host.json | `WriteAllText` with `UTF8Encoding($false)` |
|
||||
| Same error after registering HKCU | Brave on Windows ignores HKCU on some installs | Register HKLM too. `install-host.ps1` does both. |
|
||||
| Host launches then disconnects | Python text-mode stdio mangles 4-byte length prefix | `msvcrt.setmode(stdin/stdout, O_BINARY)` at host startup |
|
||||
| Host log says "stdin closed, exiting" immediately | bat-side stderr leak corrupts protocol | `python -u` + redirect stderr to log file |
|
||||
| `Missing closing '}'` in install-host.ps1 | Em-dashes in comments + LF endings + Windows PS 5.1 (cp1252 fallback) | Strip em-dashes from .ps1 files, or save with BOM, or use pwsh |
|
||||
| Brave reload != Brave restart | NM cache survives extension reload | Kill all brave.exe processes then reopen |
|
||||
| `IBW-902z` page title fails to parse | `\b` after `\d` blocked by following word char | Extension regex uses `[a-zA-Z]?\b` trailing — captured but discarded |
|
||||
| Delete safety too broad | Allowlist reduced `cq:JAV` to `cq:` | Match full configured prefixes, not remote roots |
|
||||
| Overlay feels ~1.5s late on SPA pages | `SPA_SETTLE_MS` waits before auto-check | Current value is 800ms; tune carefully if detection gets flaky |
|
||||
|
||||
## Internal names — keep as-is
|
||||
|
||||
- Native messaging host: `com.rcjav.host` (NOT renamed despite extension rename)
|
||||
- Window flag in content.js: `__rclonex_loaded__` (idempotency guard for content script re-injection)
|
||||
- CSS IDs starting with `rclonex-` (overlay)
|
||||
- Host logs: `host/logs/rcjav-host.log`, `host/logs/rcjav-host-events.log`, `host/logs/rcjav-host-stderr.log`, `host/logs/deletes.log`
|
||||
- Host scan progress state: `host/state/scan-state.json`
|
||||
|
||||
Don't rename these unless there's a real reason. They're orthogonal to the user-facing extension name.
|
||||
|
||||
## Settings
|
||||
|
||||
Stored in `chrome.storage.sync` under key `settings`. Per-extension-ID namespacing → if extension is reloaded under a different path, settings are wiped.
|
||||
|
||||
**Backup/restore lives in Options → Setup & Transfer** — JSON export/import to survive reloads or PC migrations. Use it before renaming or relocating the extension.
|
||||
|
||||
DEFAULT_SETTINGS lives in background.js. Keep in sync with options.html defaults.
|
||||
|
||||
## Decision log
|
||||
|
||||
### Deletion allowlist uses full prefixes (2026-05-20)
|
||||
|
||||
**Decision:** host delete allowlist must use full configured path prefixes (`cq:JAV`, trash dir, etc.), not only remote roots like `cq:`.
|
||||
|
||||
**Reasoning:** Reducing `cq:JAV` to `cq:` lets any path on the same rclone remote pass the safety check. Deletion is opt-in but must be tightly scoped.
|
||||
|
||||
**Important:** extension delete calls must forward `rcjav_path`, or the host may read the wrong `config.json` and derive the wrong allowlist.
|
||||
|
||||
### Toolbar popup setting gates auto-check (2026-05-20)
|
||||
|
||||
**Decision:** `triggers.toolbarClick` does not remove the MV3 popup, but it does gate whether the popup auto-runs `checkTab` on open. If disabled, popup stays idle until user clicks Re-Scan.
|
||||
|
||||
### Quick search and ID padding (2026-05-20)
|
||||
|
||||
**Decision:** rc-jav canonical JAV IDs use at least 3 digits (`ABC-027`) and preserve 4+ digit IDs (`ABCD-1294`). Quick search emits canonical uppercase globs only.
|
||||
|
||||
**Reasoning:** user clarified real JAV filenames are never `ABC-27` or `ABC-0027`; they are `ABC-027`. User also never uses lowercase filenames, so quick search should not use rclone `--ignore-case` because it added noticeable delay.
|
||||
|
||||
**Operational note:** this changes cache keys. Run `python rc-jav.py --scan` in `D:\DEV\Project\rclone-jav` after this change.
|
||||
|
||||
### No-match overlay metadata (2026-05-20)
|
||||
|
||||
**Decision:** host search response includes `cache_meta` and `scanned_remotes` from rc-jav JSON so no-match overlays can show what was scanned instead of falling back to "library".
|
||||
|
||||
### IBW-902z trailing letter (2026-05-20)
|
||||
|
||||
**Decision:** minimal regex fix in extension only. NOT a full variant-suffix rewrite of the index.
|
||||
|
||||
**Reasoning:** User's library uses one ID per number (either `IBW-902` OR `IBW-902z`, not both). Page titles failing on `IBW-902z` is the real bug. Extension regex now matches optional trailing letter and discards it. rc-jav's index continues to strip trailing letters at extract_id time. Effective: extension queries `IBW-902` for any title `IBW-902` or `IBW-902z`, finds the file regardless of how it's named on rclone.
|
||||
|
||||
**Revisit if:** both `IBW-902.mp4` and `IBW-902z.mp4` ever coexist in library — they'd collide on the same ID. Then implement variant suffix (#var_Z) end-to-end.
|
||||
|
||||
### Native messaging host name stayed `com.rcjav.host`
|
||||
|
||||
When extension was renamed `rclonex` → `rclone-jav`, the NM host name was NOT renamed. Reason: zero user impact (it's an internal identifier in registry/manifest), but every rename costs registry rewrites + script churn. Not worth it.
|
||||
|
||||
### WinCatalog backslash normalization
|
||||
|
||||
Done in rc-jav catalog loading. Catalog CSV/XML paths are normalized from Windows `\` to rclone-style `/` before the extension sees them.
|
||||
|
||||
## When making changes
|
||||
|
||||
- Extension settings schema change → update `DEFAULT_SETTINGS` in background.js AND defaults in options.html + options.js load()
|
||||
- New native messaging action → handler in rcjav-host.py + DISPATCH map + extension code that sends it
|
||||
- New options pane → sidebar item in options.html + new `.pane` div + load/save bindings in options.js
|
||||
- Any rc-jav.py CLI change → host invocation in rcjav-host.py handle_search must keep pace
|
||||
|
||||
---
|
||||
|
||||
## Console consolidation refactor — execution status
|
||||
|
||||
**Spec / blueprint:**
|
||||
- `D:\DEV\Project\rclone-jav\mockups\console-consolidation-claude.html` (refactor spec — decision table, sequence, acceptance criteria)
|
||||
- `D:\DEV\Project\rclone-jav\mockups\console-consolidation-options.html` (Codex's visual annotation variant)
|
||||
|
||||
**Shipped (in execution order):**
|
||||
|
||||
1. **Sim Dupe deleted from popup.** Button + click handler removed from `popup.html` / `popup.js`. Payload preserved in `samples/sim-dupe.js` for future layout work.
|
||||
2. **CSS extracted from options.html.** Embedded `<style>` block moved to `options.css`, linked via `<link rel="stylesheet">`. options.html went 1179 → 794 lines. Inline `style="..."` attributes intentionally left for later (step 6 territory).
|
||||
3. **Transfer Assistant wizard deleted.** "Setup & Transfer" pane renamed to "Setup". Replacement: Extension ID display + Copy button added to Diagnostics → Native host registration fieldset (always visible, not failure-gated). Sidebar entry, fieldset, modal, and ~107 lines of JS removed.
|
||||
5. **Recent Activity + Search Troubleshooting moved to new Debug Tools pane.** Verified Recent Activity is search-trigger-only by reading `background.js` — `recordActivity()` is NOT called from `delete-file` handler. No audit-value split needed. New sidebar entry "Debug Tools" under System group; new `pane-debug` houses both fieldsets.
|
||||
|
||||
(Step 4 in the plan is a paired-extraction sub-task of step 6; not a separate ship.)
|
||||
|
||||
**Pending (in execution order):**
|
||||
|
||||
- **Step 6 — options.js split, Cache & Scans + Duplicate Review paired.** Biggest, riskiest step. `options.js` is currently 3133 lines. Pair these two together because Dup Review reads cache state — extracting one while the other stays in monolith creates cross-module gap. Continue with Debug Tools, Library Issues, Settings sub-tabs after the pair lands.
|
||||
- **Step 7a — Bulk Check standalone window.** New `bulk-check.html` opened as detached `chrome.windows.create({ type: 'popup', width: 640, height: 540 })` from a "Bulk Check" launcher button in the popup. Single canonical entry path — NOT a Console sidebar tab. Window dedup via `chrome.storage.session`, last-paste persisted via `chrome.storage.local`.
|
||||
- **Step 8 — Shared fixture corpus.** Top-level `D:\DEV\Project\rclone-jav\fixtures\` (neutral location, NOT inside Python or extension repo). JSON cases for query-ID extraction (extension), filename ID extraction (Python), shared normalization.
|
||||
- **Step 9 — Cache contract design.** CACHE_VERSION already exists (currently 3). Add ID_RULES_VERSION concept: schema bump = force rebuild, rules bump = warn-and-mark-stale.
|
||||
- **Step 10 — `rc-jav.py` module split** into `rcjav/` package (ids, cache, dupes, catalog, rclone_io, output, cli). Keep `rc-jav.py` as thin entrypoint that imports from `rcjav.cli.main`.
|
||||
- **Step 11 — Host fast-path benchmark and decide.** Measure popup search latency under (a) idle Python and (b) Python actively scanning. If host fast path is the only thing keeping popup responsive under scan = narrow to dict lookup only and document. If not needed = delete entirely.
|
||||
|
||||
**Architecture (locked — do not relitigate):**
|
||||
|
||||
- Sidebar = Console / Settings / Support tri-split. No dashboard pane. Status carried by badges on tab labels (`Duplicate Review [27]`, `Cache & Scans [28m]`, `Library Issues [4]`).
|
||||
- Default landing = Duplicate Review.
|
||||
- Bulk ID Check = detached `chrome.windows.create` popup, NOT a Console sidebar tab. Single canonical entry path = popup launcher button.
|
||||
- Keep Ranking Rules nested INSIDE Duplicate Review as a sub-tab, NOT a separate Settings tab.
|
||||
- Sim Dupe: deleted from extension. Repo HTML harness in `samples/` only.
|
||||
- Transfer Assistant: deleted. Diagnostics' Native host registration fieldset is the replacement (Extension ID copy + Repair Registration + Verify Registration buttons).
|
||||
- Vanilla JS + ordered `<script>` tags. No framework, no build system.
|
||||
- Inline rule tests stay next to rule editors (Matching Rules, Site Extraction). Standalone benches go to Debug Tools.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Repo is NOT git-initialized. Rollback for shipped steps = manual restore from this conversation's diffs. Worth running `git init` in this folder before step 6 (the big one) for safer iteration.
|
||||
- Three pre-execution handoffs from the original plan have been resolved:
|
||||
- Recent Activity scope test → settled by code read (single role, all to Debug).
|
||||
- Diagnostics replacement for Transfer wizard → present (Extension ID, Repair, Verify all visible in one fieldset).
|
||||
- Popup launcher button label → defer until step 7a; text + emoji currently in mockup.
|
||||
|
||||
If a future session wants to continue: read this status block + open the mockup HTML files for the full spec. Resume on step 6.
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# rclone-jav (Brave extension + native messaging host)
|
||||
|
||||
Session memory for Claude. Read before making changes here.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Brave tab title -> content script extracts JAV ID
|
||||
-> background.js connectNative("com.rcjav.host")
|
||||
-> host/rcjav-host.bat (portable: py launcher or python on PATH)
|
||||
-> host/rcjav-host.py
|
||||
-> subprocess python rc-jav.py --search ID --basic --no-color --format json
|
||||
-> structured hits back through native port
|
||||
-> popup or in-page overlay
|
||||
```
|
||||
|
||||
Two separate codebases:
|
||||
- This repo: Brave extension + native messaging host.
|
||||
- `D:\DEV\Project\rclone-jav\` — Python rc-jav CLI. The host shells out to `rc-jav.py` here.
|
||||
|
||||
## Folder layout (post-rename)
|
||||
|
||||
```
|
||||
D:\DEV\Extensions\Production\rclone-jav\ (PC 1)
|
||||
D:\DEV\Extensions\Staging\rclone-jav\ (PC 2)
|
||||
├── manifest.json
|
||||
├── background.js
|
||||
├── content.js
|
||||
├── popup.{html,js,css}
|
||||
├── options.{html,js}
|
||||
├── host\
|
||||
│ ├── rcjav-host.py
|
||||
│ ├── rcjav-host.bat (portable: py launcher fallback)
|
||||
│ ├── install-host.ps1 (self-elevates to HKLM)
|
||||
│ ├── register-host.bat (prompts for ID, calls install-host.ps1)
|
||||
│ ├── com.rcjav.host.json (generated; UTF-8 NO BOM)
|
||||
│ └── (logs)
|
||||
└── docs\
|
||||
├── INSTALL.md (gotcha table at the bottom)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Critical gotchas (learned the hard way)
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| "Specified native messaging host not found" | UTF-8 BOM in com.rcjav.host.json | `WriteAllText` with `UTF8Encoding($false)` |
|
||||
| Same error after registering HKCU | Brave on Windows ignores HKCU on some installs | Register HKLM too. `install-host.ps1` does both. |
|
||||
| Host launches then disconnects | Python text-mode stdio mangles 4-byte length prefix | `msvcrt.setmode(stdin/stdout, O_BINARY)` at host startup |
|
||||
| Host log says "stdin closed, exiting" immediately | bat-side stderr leak corrupts protocol | `python -u` + redirect stderr to log file |
|
||||
| `Missing closing '}'` in install-host.ps1 | Em-dashes in comments + LF endings + Windows PS 5.1 (cp1252 fallback) | Strip em-dashes from .ps1 files, or save with BOM, or use pwsh |
|
||||
| Brave reload != Brave restart | NM cache survives extension reload | Kill all brave.exe processes then reopen |
|
||||
| `IBW-902z` page title fails to parse | `\b` after `\d` blocked by following word char | Extension regex uses `[a-zA-Z]?\b` trailing — captured but discarded |
|
||||
| Delete safety too broad | Allowlist reduced `cq:JAV` to `cq:` | Match full configured prefixes, not remote roots |
|
||||
| Overlay feels ~1.5s late on SPA pages | `SPA_SETTLE_MS` waits before auto-check | Current value is 800ms; tune carefully if detection gets flaky |
|
||||
|
||||
## Internal names — keep as-is
|
||||
|
||||
- Native messaging host: `com.rcjav.host` (NOT renamed despite extension rename)
|
||||
- Window flag in content.js: `__rclonex_loaded__` (idempotency guard for content script re-injection)
|
||||
- CSS IDs starting with `rclonex-` (overlay)
|
||||
- Host logs: `host/logs/rcjav-host.log`, `host/logs/rcjav-host-events.log`, `host/logs/rcjav-host-stderr.log`, `host/logs/deletes.log`
|
||||
- Host scan progress state: `host/state/scan-state.json`
|
||||
|
||||
Don't rename these unless there's a real reason. They're orthogonal to the user-facing extension name.
|
||||
|
||||
## Settings
|
||||
|
||||
Stored in `chrome.storage.sync` under key `settings`. Per-extension-ID namespacing → if extension is reloaded under a different path, settings are wiped.
|
||||
|
||||
**Backup/restore lives in Options → Paths → "Backup & restore"** — JSON export/import to survive reloads or PC migrations. Use it before renaming or relocating the extension.
|
||||
|
||||
DEFAULT_SETTINGS lives in background.js. Keep in sync with options.html defaults.
|
||||
|
||||
## Decision log
|
||||
|
||||
### Deletion allowlist uses full prefixes (2026-05-20)
|
||||
|
||||
**Decision:** host delete allowlist must use full configured path prefixes (`cq:JAV`, trash dir, etc.), not only remote roots like `cq:`.
|
||||
|
||||
**Reasoning:** Reducing `cq:JAV` to `cq:` lets any path on the same rclone remote pass the safety check. Deletion is opt-in but must be tightly scoped.
|
||||
|
||||
**Important:** extension delete calls must forward `rcjav_path`, or the host may read the wrong `config.json` and derive the wrong allowlist.
|
||||
|
||||
### Toolbar popup setting gates auto-check (2026-05-20)
|
||||
|
||||
**Decision:** `triggers.toolbarClick` does not remove the MV3 popup, but it does gate whether the popup auto-runs `checkTab` on open. If disabled, popup stays idle until user clicks Re-Scan.
|
||||
|
||||
### Quick search and ID padding (2026-05-20)
|
||||
|
||||
**Decision:** rc-jav canonical JAV IDs use at least 3 digits (`ABC-027`) and preserve 4+ digit IDs (`ABCD-1294`). Quick search emits canonical uppercase globs only.
|
||||
|
||||
**Reasoning:** user clarified real JAV filenames are never `ABC-27` or `ABC-0027`; they are `ABC-027`. User also never uses lowercase filenames, so quick search should not use rclone `--ignore-case` because it added noticeable delay.
|
||||
|
||||
**Operational note:** this changes cache keys. Run `python rc-jav.py --scan` in `D:\DEV\Project\rclone-jav` after this change.
|
||||
|
||||
### No-match overlay metadata (2026-05-20)
|
||||
|
||||
**Decision:** host search response includes `cache_meta` and `scanned_remotes` from rc-jav JSON so no-match overlays can show what was scanned instead of falling back to "library".
|
||||
|
||||
### IBW-902z trailing letter (2026-05-20)
|
||||
|
||||
**Decision:** minimal regex fix in extension only. NOT a full variant-suffix rewrite of the index.
|
||||
|
||||
**Reasoning:** User's library uses one ID per number (either `IBW-902` OR `IBW-902z`, not both). Page titles failing on `IBW-902z` is the real bug. Extension regex now matches optional trailing letter and discards it. rc-jav's index continues to strip trailing letters at extract_id time. Effective: extension queries `IBW-902` for any title `IBW-902` or `IBW-902z`, finds the file regardless of how it's named on rclone.
|
||||
|
||||
**Revisit if:** both `IBW-902.mp4` and `IBW-902z.mp4` ever coexist in library — they'd collide on the same ID. Then implement variant suffix (#var_Z) end-to-end.
|
||||
|
||||
### Native messaging host name stayed `com.rcjav.host`
|
||||
|
||||
When extension was renamed `rclonex` → `rclone-jav`, the NM host name was NOT renamed. Reason: zero user impact (it's an internal identifier in registry/manifest), but every rename costs registry rewrites + script churn. Not worth it.
|
||||
|
||||
### WinCatalog backslash normalization
|
||||
|
||||
Deferred. See `D:\DEV\Project\rclone-jav\TODO.md`. Catalog CSV paths use Windows `\` which leaks through to popup display, breaking filename split. Fix is 2 lines in rc-jav.py `load_catalog_csv` / `load_catalog_xml` — apply when catalog feature is used heavily.
|
||||
|
||||
## When making changes
|
||||
|
||||
- Extension settings schema change → update `DEFAULT_SETTINGS` in background.js AND defaults in options.html + options.js load()
|
||||
- New native messaging action → handler in rcjav-host.py + DISPATCH map + extension code that sends it
|
||||
- New options pane → sidebar item in options.html + new `.pane` div + load/save bindings in options.js
|
||||
- Any rc-jav.py CLI change → host invocation in rcjav-host.py handle_search must keep pace
|
||||
+1077
File diff suppressed because it is too large
Load Diff
+542
@@ -0,0 +1,542 @@
|
||||
// rclonex content script.
|
||||
// Responsibilities:
|
||||
// 1. On request, extract a JAV ID from the page via site adapters → URL → title.
|
||||
// 2. Implement an interactive element picker so the user can fill in a selector
|
||||
// by clicking on the page from the options UI.
|
||||
//
|
||||
// Wrapped in an IIFE + global flag so re-injection (e.g. via chrome.scripting.executeScript
|
||||
// when manifest content_scripts already loaded it) is a no-op instead of a SyntaxError.
|
||||
|
||||
if (!window.__rclonex_loaded__) {
|
||||
window.__rclonex_loaded__ = true;
|
||||
(() => {
|
||||
|
||||
// Optional single trailing letter (e.g. IBW-902z) is matched but discarded —
|
||||
// rc-jav's index already drops trailing letters too, so query "IBW-902" finds the file.
|
||||
const ID_RE_DASHED = /\b([A-Za-z][A-Za-z0-9]{1,})-(\d{2,7})[a-zA-Z]?\b/;
|
||||
const ID_RE_UNDASHED = /\b([A-Za-z][A-Za-z0-9]{1,})(\d{3,5})[a-zA-Z]?\b/;
|
||||
|
||||
// Built-in studio normalizers — applied BEFORE generic ID regex.
|
||||
// Each entry: { re: RegExp, fmt: string ($1, $2 = capture groups) }.
|
||||
// User-added normalizers from settings are tried before these.
|
||||
const BUILTIN_ID_NORMALIZERS = [
|
||||
// FC2-PPV in any dash configuration: FC2PPV12345, FC2-PPV12345, FC2-PPV-12345
|
||||
{ re: /\bFC2-?PPV-?(\d{4,})\b/i, fmt: "FC2-PPV-$1" },
|
||||
// Some sites display FC2 IDs without the PPV segment: FC2-1841460.
|
||||
{ re: /\bFC2-(\d{4,})\b/i, fmt: "FC2-PPV-$1" },
|
||||
];
|
||||
|
||||
const BUILTIN_SITE_ADAPTERS = [
|
||||
{ host: "clearjav.com", selector: "div.meta-chip > h3.meta-chip__value" },
|
||||
];
|
||||
|
||||
function applyNormalizers(text, userList) {
|
||||
const all = [...(userList || []), ...BUILTIN_ID_NORMALIZERS];
|
||||
for (const n of all) {
|
||||
let re;
|
||||
try { re = n.re instanceof RegExp ? n.re : new RegExp(n.re, "i"); } catch { continue; }
|
||||
const m = text.match(re);
|
||||
if (m) {
|
||||
// Apply fmt with $1..$9 substitution
|
||||
return n.fmt.replace(/\$(\d)/g, (_, i) => m[+i] || "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let _userNormalizers = [];
|
||||
async function loadUserNormalizers() {
|
||||
try {
|
||||
const { settings = {} } = await chrome.storage.sync.get("settings");
|
||||
_userNormalizers = Array.isArray(settings.idNormalizers) ? settings.idNormalizers : [];
|
||||
} catch { _userNormalizers = []; }
|
||||
}
|
||||
chrome.storage.onChanged?.addListener?.((changes, area) => {
|
||||
if (area === "sync" && changes.settings) {
|
||||
_userNormalizers = Array.isArray(changes.settings.newValue?.idNormalizers)
|
||||
? changes.settings.newValue.idNormalizers : [];
|
||||
}
|
||||
});
|
||||
loadUserNormalizers();
|
||||
|
||||
function normalizeId(text, userNormalizers = _userNormalizers) {
|
||||
if (!text) return null;
|
||||
// Try user-defined + built-in normalizers first (FC2-PPV-style oddballs).
|
||||
const fromNormalizer = applyNormalizers(text, userNormalizers);
|
||||
if (fromNormalizer) return fromNormalizer.toUpperCase();
|
||||
let m = text.match(ID_RE_DASHED);
|
||||
if (!m) m = text.match(ID_RE_UNDASHED);
|
||||
if (!m) return null;
|
||||
// Preserve the digits exactly as they appear (incl. leading zeros) — rc-jav --quick
|
||||
// hands the glob "<ID>*" to rclone --include, which is literal, not numeric.
|
||||
return `${m[1].toUpperCase()}-${m[2]}`;
|
||||
}
|
||||
|
||||
function hostMatches(pattern, host) {
|
||||
// Glob: '*' = any chars. Case-insensitive.
|
||||
// Convenience: a bare domain (no '*.') ALSO matches any subdomain — and vice versa.
|
||||
// "clearjav.com" matches both "clearjav.com" and "www.clearjav.com"
|
||||
// "*.clearjav.com" matches both "www.clearjav.com" and bare "clearjav.com"
|
||||
const patterns = [pattern];
|
||||
if (pattern.startsWith("*.")) patterns.push(pattern.slice(2));
|
||||
else if (!pattern.includes("*")) patterns.push("*." + pattern);
|
||||
for (const p of patterns) {
|
||||
const re = new RegExp("^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$", "i");
|
||||
if (re.test(host)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryAdapters(adapters, userNormalizers = _userNormalizers) {
|
||||
const host = location.hostname;
|
||||
for (const a of adapters) {
|
||||
if (!a.host || !a.selector) continue;
|
||||
if (!hostMatches(a.host, host)) continue;
|
||||
const selectors = a.selector.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
for (const sel of selectors) {
|
||||
let el;
|
||||
try { el = document.querySelector(sel); } catch { continue; }
|
||||
if (!el) continue;
|
||||
const text = (el.textContent || el.innerText || "").trim();
|
||||
const id = normalizeId(text, userNormalizers);
|
||||
if (id) return { id, source: "adapter", adapter: a.host, selector: sel, raw: text.slice(0, 200) };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function tryUrl(userNormalizers = _userNormalizers) {
|
||||
const raw = decodeURIComponent(location.pathname + " " + location.search);
|
||||
const id = normalizeId(raw, userNormalizers);
|
||||
return id ? { id, source: "url", raw: raw.slice(0, 200) } : null;
|
||||
}
|
||||
|
||||
function tryTitle(userNormalizers = _userNormalizers) {
|
||||
const id = normalizeId(document.title, userNormalizers);
|
||||
return id ? { id, source: "title", raw: document.title.slice(0, 200) } : null;
|
||||
}
|
||||
|
||||
async function getAdapters() {
|
||||
const { settings = {} } = await chrome.storage.sync.get("settings");
|
||||
const user = Array.isArray(settings.siteAdapters) ? settings.siteAdapters : [];
|
||||
const builtin = [...BUILTIN_SITE_ADAPTERS, ...(window.__RCLONEX_BUILTIN_ADAPTERS__ || [])];
|
||||
// User entries override built-ins for the same host (user list iterated first).
|
||||
return [...user, ...builtin];
|
||||
}
|
||||
|
||||
async function tracePageExtraction(overrides = {}) {
|
||||
const savedAdapters = await getAdapters();
|
||||
const adapters = Array.isArray(overrides.adapters)
|
||||
? [...overrides.adapters, ...BUILTIN_SITE_ADAPTERS, ...(window.__RCLONEX_BUILTIN_ADAPTERS__ || [])]
|
||||
: savedAdapters;
|
||||
const normalizers = Array.isArray(overrides.normalizers) ? overrides.normalizers : _userNormalizers;
|
||||
const adapter = tryAdapters(adapters, normalizers);
|
||||
const title = tryTitle(normalizers);
|
||||
const url = tryUrl(normalizers);
|
||||
return {
|
||||
id: (adapter || title || url || {}).id || null,
|
||||
source: (adapter || title || url || {}).source || "none",
|
||||
selected: adapter || title || url || null,
|
||||
stages: { adapter, title, url },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- picker ----------
|
||||
|
||||
let pickerActive = false;
|
||||
let pickerOverlay = null;
|
||||
let pickerHighlight = null;
|
||||
|
||||
function buildSelector(el) {
|
||||
if (!el || el.nodeType !== 1) return null;
|
||||
|
||||
// Helper: does selector hit an element with the same JAV-ID-bearing text?
|
||||
// We don't require strict uniqueness — adapter uses first match anyway, so as long
|
||||
// as the FIRST match has the right textContent (or contains the same ID), we win.
|
||||
const wantedText = (el.textContent || "").trim();
|
||||
const wantedId = normalizeId(wantedText);
|
||||
const matchesTarget = (sel) => {
|
||||
let first;
|
||||
try { first = document.querySelector(sel); } catch { return false; }
|
||||
if (!first) return false;
|
||||
if (first === el) return true;
|
||||
const t = (first.textContent || "").trim();
|
||||
if (wantedId && normalizeId(t) === wantedId) return true;
|
||||
return t === wantedText;
|
||||
};
|
||||
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const candidates = [];
|
||||
|
||||
// 1. #id if any
|
||||
if (el.id) candidates.push("#" + CSS.escape(el.id));
|
||||
|
||||
// 2. tag + single class (each class on its own — pick shortest that works)
|
||||
if (el.classList.length) {
|
||||
for (const cls of el.classList) candidates.push(`${tag}.${CSS.escape(cls)}`);
|
||||
// 3. tag + all classes combined
|
||||
candidates.push(tag + Array.from(el.classList).map((c) => "." + CSS.escape(c)).join(""));
|
||||
// 4. just .class (no tag) for each class
|
||||
for (const cls of el.classList) candidates.push("." + CSS.escape(cls));
|
||||
}
|
||||
|
||||
// 5. just tag (rarely unique but cheap)
|
||||
candidates.push(tag);
|
||||
|
||||
// Try simplest first.
|
||||
candidates.sort((a, b) => a.length - b.length);
|
||||
for (const sel of candidates) {
|
||||
if (matchesTarget(sel)) return sel;
|
||||
}
|
||||
|
||||
// Fall back: build qualified ancestor path.
|
||||
const parts = [];
|
||||
let cur = el;
|
||||
while (cur && cur.nodeType === 1 && cur.tagName.toLowerCase() !== "html") {
|
||||
let part = cur.tagName.toLowerCase();
|
||||
if (cur.id) { part += "#" + CSS.escape(cur.id); parts.unshift(part); break; }
|
||||
if (cur.classList.length) {
|
||||
part += Array.from(cur.classList).slice(0, 3).map((c) => "." + CSS.escape(c)).join("");
|
||||
}
|
||||
const siblings = cur.parentNode ? Array.from(cur.parentNode.children).filter((s) => s.tagName === cur.tagName) : [];
|
||||
if (siblings.length > 1) {
|
||||
part += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
|
||||
}
|
||||
parts.unshift(part);
|
||||
const candidate = parts.join(" > ");
|
||||
if (matchesTarget(candidate)) return candidate;
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return parts.join(" > ");
|
||||
}
|
||||
|
||||
function startPicker() {
|
||||
if (pickerActive) return;
|
||||
pickerActive = true;
|
||||
pickerOverlay = document.createElement("div");
|
||||
pickerOverlay.style.cssText = "position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#ff8800;color:#000;padding:10px 16px;font:bold 14px/1.3 -apple-system,Segoe UI,sans-serif;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,.5);pointer-events:none;max-width:90vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
||||
pickerOverlay.textContent = "rclonex picker: hover an element, click to pick — Esc to cancel";
|
||||
document.body.appendChild(pickerOverlay);
|
||||
pickerHighlight = document.createElement("div");
|
||||
pickerHighlight.style.cssText = "position:fixed;pointer-events:none;z-index:2147483646;border:3px dashed #ff8800;background:rgba(255,136,0,.25);box-shadow:0 0 0 9999px rgba(0,0,0,.35);box-sizing:border-box;transition:all 0.05s;";
|
||||
document.body.appendChild(pickerHighlight);
|
||||
document.addEventListener("mousemove", onPickerMove, true);
|
||||
document.addEventListener("click", onPickerClick, true);
|
||||
document.addEventListener("keydown", onPickerKey, true);
|
||||
}
|
||||
|
||||
function stopPicker() {
|
||||
if (!pickerActive) return;
|
||||
pickerActive = false;
|
||||
document.removeEventListener("mousemove", onPickerMove, true);
|
||||
document.removeEventListener("click", onPickerClick, true);
|
||||
document.removeEventListener("keydown", onPickerKey, true);
|
||||
if (pickerOverlay) pickerOverlay.remove();
|
||||
if (pickerHighlight) pickerHighlight.remove();
|
||||
pickerOverlay = null;
|
||||
pickerHighlight = null;
|
||||
}
|
||||
|
||||
function onPickerMove(e) {
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || el === pickerHighlight || el === pickerOverlay) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
pickerHighlight.style.top = r.top + "px";
|
||||
pickerHighlight.style.left = r.left + "px";
|
||||
pickerHighlight.style.width = r.width + "px";
|
||||
pickerHighlight.style.height = r.height + "px";
|
||||
pickerOverlay.textContent = `rclonex: ${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}${el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : ""} — click to pick, Esc to cancel`;
|
||||
}
|
||||
|
||||
function onPickerClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || el === pickerHighlight || el === pickerOverlay) { stopPicker(); return; }
|
||||
const selector = buildSelector(el);
|
||||
const text = (el.textContent || "").trim().slice(0, 200);
|
||||
const id = normalizeId(text);
|
||||
chrome.runtime.sendMessage({
|
||||
type: "picker-result",
|
||||
host: location.hostname,
|
||||
selector,
|
||||
sample: text,
|
||||
detectedId: id,
|
||||
});
|
||||
stopPicker();
|
||||
}
|
||||
|
||||
function onPickerKey(e) {
|
||||
if (e.key === "Escape") { stopPicker(); chrome.runtime.sendMessage({ type: "picker-cancelled" }); }
|
||||
}
|
||||
|
||||
// ---------- message dispatch ----------
|
||||
|
||||
// ---------- overlay (auto-trigger hit toast) ----------
|
||||
|
||||
const OVERLAY_ID = "rclonex-overlay";
|
||||
|
||||
function showOverlay(result, opts = {}) {
|
||||
if (!result) return;
|
||||
const kind = opts.kind || "match";
|
||||
if (kind === "match" && (!result.structured || result.structured.length === 0)) return;
|
||||
document.getElementById(OVERLAY_ID)?.remove();
|
||||
document.getElementById("rclonex-overlay-style")?.remove();
|
||||
|
||||
const position = opts.position || "top-right";
|
||||
const durationMs = Math.max(500, Math.round((opts.duration || 5) * 1000));
|
||||
const glow = !!opts.glow;
|
||||
const glowColor = opts.glowColor || "#6ec1ff";
|
||||
const glowBlur = Number.isFinite(opts.glowBlur) ? opts.glowBlur : 10;
|
||||
const glowSpread = Number.isFinite(opts.glowSpread) ? opts.glowSpread : 0;
|
||||
const glowOpacity = Number.isFinite(opts.glowOpacity) ? opts.glowOpacity : 0.35;
|
||||
|
||||
const hexToRgba = (hex, a) => {
|
||||
const m = String(hex).match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
||||
if (!m) return `rgba(110,193,255,${a})`;
|
||||
return `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})`;
|
||||
};
|
||||
|
||||
const positions = {
|
||||
"top-right": { top: "16px", right: "16px", bottom: "auto", left: "auto", enter: "translateY(-10px)" },
|
||||
"top-left": { top: "16px", left: "16px", bottom: "auto", right: "auto", enter: "translateY(-10px)" },
|
||||
"bottom-right": { bottom: "16px", right: "16px", top: "auto", left: "auto", enter: "translateY(10px)" },
|
||||
"bottom-left": { bottom: "16px", left: "16px", top: "auto", right: "auto", enter: "translateY(10px)" },
|
||||
};
|
||||
const p = positions[position] || positions["top-right"];
|
||||
|
||||
const glowCss = glow
|
||||
? `box-shadow: 0 6px 20px rgba(0,0,0,0.55), 0 0 ${glowBlur}px ${glowSpread}px ${hexToRgba(glowColor, glowOpacity)};`
|
||||
: "box-shadow: 0 8px 30px rgba(0,0,0,0.6);";
|
||||
|
||||
const css = `
|
||||
#${OVERLAY_ID} {
|
||||
position: fixed;
|
||||
top: ${p.top}; right: ${p.right}; bottom: ${p.bottom}; left: ${p.left};
|
||||
width: 380px;
|
||||
z-index: 2147483647; background: #1a1a1a; color: #ddd;
|
||||
border: 1px solid #2a2a2a; border-radius: 6px;
|
||||
font-family: -apple-system, Segoe UI, sans-serif; font-size: 13px;
|
||||
${glowCss}
|
||||
overflow: hidden;
|
||||
opacity: 0; transform: ${p.enter};
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
}
|
||||
#${OVERLAY_ID}.show { opacity: 1; transform: translateY(0); }
|
||||
#${OVERLAY_ID} .rx-header {
|
||||
padding: 8px 12px; background: #1e3a1e; color: #afa;
|
||||
font-size: 13px; font-weight: 600;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-title {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-mode {
|
||||
margin-left: auto;
|
||||
background: rgba(0,0,0,0.28);
|
||||
border: 1px solid rgba(255,255,255,0.18);
|
||||
color: currentColor;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-header.rx-no-match { background: #3a1e1e; color: #faa; }
|
||||
#${OVERLAY_ID} .rx-subline {
|
||||
padding: 8px 12px; background: #0d0d0d; color: #aaa;
|
||||
font-size: 12px; font-family: Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-miss-detail {
|
||||
padding: 8px 12px; background: #15110d; color: #ccb98f;
|
||||
font-size: 11px; line-height: 1.45;
|
||||
border-top: 1px solid #30251a;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-close {
|
||||
cursor: pointer; color: #8a8a8a; font-size: 16px;
|
||||
width: 18px; height: 18px; line-height: 16px; text-align: center;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
#${OVERLAY_ID} .rx-body { padding: 6px; background: #0d0d0d; max-height: 360px; overflow-y: auto; }
|
||||
#${OVERLAY_ID} .rx-hit {
|
||||
background: #161616; border: 1px solid #2a2a2a; border-radius: 4px;
|
||||
padding: 10px; margin-bottom: 6px;
|
||||
}
|
||||
#${OVERLAY_ID} .rx-hit:last-child { margin-bottom: 0; }
|
||||
#${OVERLAY_ID} .rx-file { color: #fff; font-weight: 600; font-size: 13px; word-break: break-all; margin: 0 0 4px; }
|
||||
#${OVERLAY_ID} .rx-path { color: #aaa; font-size: 12px; font-family: Consolas, monospace; word-break: break-all; margin: 0 0 6px; line-height: 1.45; }
|
||||
#${OVERLAY_ID} .rx-plabel { color: #555; font-weight: 600; }
|
||||
#${OVERLAY_ID} .rx-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1; }
|
||||
#${OVERLAY_ID} .rx-size { color: #6ec1ff; font-weight: 600; }
|
||||
#${OVERLAY_ID} .rx-src { background: #2a2a1a; color: #ffcc44; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
|
||||
#${OVERLAY_ID} .rx-src.source { background: #1a3a1a; color: #66dd66; }
|
||||
#${OVERLAY_ID} .rx-src.catalog { background: #1a2a3a; color: #66bbff; }
|
||||
#${OVERLAY_ID} .rx-reason { background: #202a32; border: 1px solid #314453; color: #9dccff; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; }
|
||||
#${OVERLAY_ID} .rx-bar-wrap { height: 3px; background: #222; }
|
||||
#${OVERLAY_ID} .rx-bar {
|
||||
height: 100%; background: linear-gradient(90deg, #6ec1ff, #66dd66);
|
||||
transform-origin: left; transform: scaleX(1);
|
||||
animation: rxshrink ${durationMs}ms linear forwards;
|
||||
}
|
||||
@keyframes rxshrink { from { transform: scaleX(1); } to { transform: scaleX(0); } }
|
||||
`;
|
||||
let styleEl = document.getElementById("rclonex-overlay-style");
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement("style");
|
||||
styleEl.id = "rclonex-overlay-style";
|
||||
styleEl.textContent = css;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.id = OVERLAY_ID;
|
||||
|
||||
const idLabel = result.id || result.structured?.[0]?.jav_id || "?";
|
||||
const cacheTag = result.cached && result.ts ? ` [session ${fmtOverlayAge(result.ts)}]` : "";
|
||||
const modeLabel = result.cached ? "SESSION" : result.search_mode === "quick" ? "LIVE" : result.search_mode === "cached" ? "CACHE" : "";
|
||||
const header = document.createElement("div");
|
||||
header.className = "rx-header" + (kind === "no-match" ? " rx-no-match" : "");
|
||||
const titleText = kind === "match"
|
||||
? `✓ ${idLabel} — ${result.hits} hit(s)${cacheTag}`
|
||||
: `✗ ${idLabel} — NOT IN LIBRARY${cacheTag}`;
|
||||
if (kind === "match") {
|
||||
header.innerHTML = `<span class="rx-title">${escapeOverlay(titleText)}</span>${modeLabel ? `<span class="rx-mode">${escapeOverlay(modeLabel)}</span>` : ""}<span class="rx-close" title="dismiss">×</span>`;
|
||||
} else {
|
||||
header.innerHTML = `<span class="rx-title">${escapeOverlay(titleText)}</span>${modeLabel ? `<span class="rx-mode">${escapeOverlay(modeLabel)}</span>` : ""}<span class="rx-close" title="dismiss">×</span>`;
|
||||
}
|
||||
root.appendChild(header);
|
||||
|
||||
if (kind === "match") {
|
||||
const body = document.createElement("div");
|
||||
body.className = "rx-body";
|
||||
for (const h of result.structured) {
|
||||
const filename = h.path.split("/").pop();
|
||||
const idx = h.full_path.lastIndexOf("/");
|
||||
const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path;
|
||||
const srcCls = h.source === "Source" ? "rx-src source"
|
||||
: h.source === "Catalog" ? "rx-src catalog" : "rx-src";
|
||||
const confidence = h.match_confidence ? ` · ${h.match_confidence}` : "";
|
||||
const reason = h.match_reason
|
||||
? `<span class="rx-reason" title="Matched ${escapeOverlay(h.matched_query || h.jav_id || "")}${escapeOverlay(confidence)}">${escapeOverlay(h.match_reason)}</span>`
|
||||
: "";
|
||||
const card = document.createElement("div");
|
||||
card.className = "rx-hit";
|
||||
card.innerHTML = `
|
||||
<div class="rx-file">${escapeOverlay(filename)}</div>
|
||||
<div class="rx-path"><span class="rx-plabel">Path:</span> ${escapeOverlay(dir)}</div>
|
||||
<div class="rx-meta">
|
||||
<span class="${srcCls}">${escapeOverlay(h.source.toUpperCase())}</span>
|
||||
<span class="rx-size">${escapeOverlay(h.size_human)}</span>
|
||||
${reason}
|
||||
</div>
|
||||
`;
|
||||
body.appendChild(card);
|
||||
}
|
||||
root.appendChild(body);
|
||||
} else {
|
||||
// No-match: a single sub-line saying what was scanned.
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "rx-subline";
|
||||
const remotes = (result.scanned_remotes && result.scanned_remotes.length)
|
||||
? result.scanned_remotes.join(", ")
|
||||
: (Object.keys(result.cache_meta || {})[0] || "library");
|
||||
sub.innerHTML = `<span class="rx-plabel">Scanned:</span> ${escapeOverlay(remotes)}`;
|
||||
root.appendChild(sub);
|
||||
if (result.no_match_title || result.no_match_detail) {
|
||||
const detail = document.createElement("div");
|
||||
detail.className = "rx-miss-detail";
|
||||
detail.innerHTML = `<strong>${escapeOverlay(result.no_match_title || "No match")}</strong>${result.no_match_detail ? `<div>${escapeOverlay(result.no_match_detail)}</div>` : ""}`;
|
||||
root.appendChild(detail);
|
||||
}
|
||||
}
|
||||
|
||||
const barWrap = document.createElement("div");
|
||||
barWrap.className = "rx-bar-wrap";
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "rx-bar";
|
||||
barWrap.appendChild(bar);
|
||||
root.appendChild(barWrap);
|
||||
|
||||
document.body.appendChild(root);
|
||||
requestAnimationFrame(() => root.classList.add("show"));
|
||||
|
||||
// Hover pauses the countdown
|
||||
root.addEventListener("mouseenter", () => { bar.style.animationPlayState = "paused"; });
|
||||
root.addEventListener("mouseleave", () => { bar.style.animationPlayState = "running"; });
|
||||
// Close button
|
||||
header.querySelector(".rx-close").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
dismissOverlay(root);
|
||||
});
|
||||
// Animation end → fade out
|
||||
bar.addEventListener("animationend", () => dismissOverlay(root));
|
||||
}
|
||||
|
||||
function dismissOverlay(root) {
|
||||
root.classList.remove("show");
|
||||
setTimeout(() => root.remove(), 300);
|
||||
}
|
||||
|
||||
function escapeOverlay(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
|
||||
function fmtOverlayAge(ts) {
|
||||
if (!ts) return "";
|
||||
const s = Math.round((Date.now() - ts) / 1000);
|
||||
if (s < 60) return `${s}s ago`;
|
||||
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
||||
return `${Math.round(s / 3600)}h ago`;
|
||||
}
|
||||
|
||||
// ---------- message dispatch ----------
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === "show-overlay") {
|
||||
showOverlay(msg.result, {
|
||||
kind: msg.kind || "match",
|
||||
position: msg.position,
|
||||
duration: msg.duration,
|
||||
glow: msg.glow,
|
||||
glowColor: msg.glowColor,
|
||||
glowBlur: msg.glowBlur,
|
||||
glowSpread: msg.glowSpread,
|
||||
glowOpacity: msg.glowOpacity,
|
||||
});
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
if (msg.type === "extract-id") {
|
||||
(async () => {
|
||||
const trace = await tracePageExtraction();
|
||||
sendResponse(trace.selected || { id: null, source: "none" });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === "trace-extract-id") {
|
||||
(async () => {
|
||||
sendResponse(await tracePageExtraction({
|
||||
adapters: msg.adapters,
|
||||
normalizers: msg.normalizers,
|
||||
}));
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === "start-picker") {
|
||||
startPicker();
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
if (msg.type === "stop-picker") {
|
||||
stopPicker();
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
} // end if !__rclonex_loaded__
|
||||
@@ -0,0 +1,37 @@
|
||||
# Stable Extension ID
|
||||
|
||||
Chrome and Brave derive an extension ID from the extension public key. If the
|
||||
key changes, the extension ID changes too.
|
||||
|
||||
Why this matters for rclone-jav:
|
||||
|
||||
- `chrome.storage.sync` settings are scoped to the extension ID.
|
||||
- The native messaging host manifest authorizes specific extension IDs in
|
||||
`allowed_origins`.
|
||||
- Moving or reloading an unpacked extension without a fixed key can make settings
|
||||
appear to vanish and can break native messaging.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Export settings from Options -> Backup before changing install paths.
|
||||
2. Keep Production loaded from a stable folder path.
|
||||
3. For a permanently stable unpacked ID, add a fixed Chrome extension public key
|
||||
to `manifest.json` under the `key` field.
|
||||
4. After the ID changes intentionally, update the native host manifest
|
||||
`allowed_origins` and re-register the host.
|
||||
5. Import the settings backup.
|
||||
|
||||
## Packed extension workflow
|
||||
|
||||
If you pack the extension as a `.crx`, save the generated `.pem` private key.
|
||||
Packing future versions with the same `.pem` produces the same extension ID.
|
||||
|
||||
Do not lose or share the `.pem`; it controls the extension identity.
|
||||
|
||||
## Unpacked extension workflow
|
||||
|
||||
For unpacked installs, use the public key derived from the `.pem` as
|
||||
`manifest.json`'s `key` value. The value must be the Chrome extension public key
|
||||
format, not an arbitrary string.
|
||||
|
||||
Same manifest key = same extension ID.
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
# rclone-jav — Install Guide
|
||||
|
||||
End-to-end setup for a fresh PC. Tested on Windows 11 + Brave Stable. Should also work for Chrome with minor path tweaks.
|
||||
|
||||
> **PC2 / Staging note:** Paths below use `D:\DEV\Extensions\Production\rclone-jav\`. On the Staging PC, substitute `Production` → `Staging` throughout.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Python 3.9+** on PATH. Verify:
|
||||
```
|
||||
python --version
|
||||
```
|
||||
Note the install location (e.g. `C:\Python314\python.exe`). Needed in step 6.
|
||||
|
||||
2. **rclone** on PATH with your remotes configured (`rclone config`). Verify:
|
||||
```
|
||||
rclone version
|
||||
rclone lsf <your-remote>: -R --include "*IPZZ*" # sanity check
|
||||
```
|
||||
|
||||
**If "rclone not found" in a fresh PowerShell** despite being installed: Windows Terminal sometimes caches the old PATH across new pwsh sessions. Quick fix for the current session:
|
||||
```powershell
|
||||
$env:Path += ';C:\Programs\rclone' # or wherever rclone.exe lives
|
||||
rclone version
|
||||
```
|
||||
Permanent fix: close Windows Terminal completely (all windows, not just tabs) and reopen — fresh pwsh will inherit the registered user PATH. If still missing, sign out/in once.
|
||||
|
||||
3. **Brave** (Stable, Beta, Dev, or Nightly — install registers all four channels).
|
||||
|
||||
4. **rich** (Python package, used by `rc-jav.py`):
|
||||
```
|
||||
pip install rich
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Place the files
|
||||
|
||||
Copy both project folders:
|
||||
|
||||
```
|
||||
D:\DEV\Project\rclone-jav\ ← rc-jav.py + cache.json + config.json + wincatalog\
|
||||
D:\DEV\Extensions\Production\rclone-jav\ ← manifest + host\
|
||||
```
|
||||
|
||||
`rcjav-host.bat` is now portable — it uses Windows' `py` launcher or falls back to `python` on PATH. **No per-machine Python path edits needed** as long as Python is on PATH.
|
||||
|
||||
If you put the project folder elsewhere than `D:\DEV\Project\rclone-jav`, you'll either:
|
||||
- Edit the `RC_JAV` constant at the top of `D:\DEV\Extensions\Production\rclone-jav\host\rcjav-host.py`, OR
|
||||
- Set it via the extension's Options → **Setup & Transfer → rc-jav script path** (works without editing files).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Configure rc-jav defaults
|
||||
|
||||
Edit `D:\DEV\Project\rclone-jav\config.json` (create if missing):
|
||||
|
||||
```json
|
||||
{
|
||||
"default_target": ["cq:personal-files/JAV"],
|
||||
"default_source": [],
|
||||
"default_catalog": []
|
||||
}
|
||||
```
|
||||
|
||||
Or set interactively:
|
||||
|
||||
```
|
||||
cd D:\DEV\Project\rclone-jav
|
||||
python rc-jav.py --target cq:personal-files/JAV --save
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```
|
||||
python rc-jav.py --search IPZZ-860 -q --basic
|
||||
```
|
||||
|
||||
Should return a hit in ~1–2 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Load the Brave extension
|
||||
|
||||
1. Open `brave://extensions`.
|
||||
2. Toggle **Developer mode** ON (top-right).
|
||||
3. Click **Load unpacked**.
|
||||
4. Pick `D:\DEV\Extensions\Production\rclone-jav` (the folder containing `manifest.json`).
|
||||
5. **Copy the Extension ID** shown on the card. Example: `gnimjpgbgehbefdkdjodcimefdedgeho`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Register the native messaging host
|
||||
|
||||
Easiest: double-click `D:\DEV\Extensions\Production\rclone-jav\host\register-host.bat`. It prompts for the ID and runs the installer.
|
||||
|
||||
Or manually in PowerShell (any window — script self-elevates):
|
||||
|
||||
```powershell
|
||||
cd D:\DEV\Extensions\Production\rclone-jav\host
|
||||
.\install-host.ps1 -ExtensionId <paste-the-extension-id>
|
||||
```
|
||||
|
||||
UAC will prompt — click **Yes**. The script writes:
|
||||
|
||||
- **Manifest:** `D:\DEV\Extensions\Production\rclone-jav\host\com.rcjav.host.json` (UTF-8 no BOM)
|
||||
- **Registry (HKLM + HKCU):**
|
||||
- `\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host`
|
||||
- `\Software\Google\Chrome\NativeMessagingHosts\com.rcjav.host`
|
||||
- `\Software\WOW6432Node\Google\Chrome\NativeMessagingHosts\com.rcjav.host`
|
||||
- `\Software\Chromium\NativeMessagingHosts\com.rcjav.host`
|
||||
|
||||
HKLM is **required** — Brave on Windows often ignores HKCU. The script writes both as belt-and-suspenders.
|
||||
|
||||
Verify registry:
|
||||
|
||||
```powershell
|
||||
reg query "HKLM\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host"
|
||||
```
|
||||
|
||||
Should print `(Default) REG_SZ D:\DEV\Extensions\Production\rclone-jav\host\com.rcjav.host.json`.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Fully restart Brave
|
||||
|
||||
Closing windows is not enough. Brave caches NM host registrations at process startup.
|
||||
|
||||
```powershell
|
||||
Get-Process brave | Stop-Process -Force
|
||||
```
|
||||
|
||||
Or Task Manager → kill every `brave.exe`. Then reopen Brave.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Verify
|
||||
|
||||
1. Click the rclone-jav toolbar icon.
|
||||
2. Popup opens, auto-runs a check on the current tab.
|
||||
3. Click **Ping host**. Expect green banner: `host ok: 0.1.0`.
|
||||
|
||||
If green, you're done.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Choose triggers (optional)
|
||||
|
||||
Click the gear icon in the popup (or right-click extension → Options) to enable:
|
||||
|
||||
- Auto-check every page load
|
||||
- Auto-check on listed JAV sites (configurable host patterns)
|
||||
- Toolbar icon popup auto-check
|
||||
- Right-click context menu
|
||||
- Keyboard shortcut (default `Alt+J`, rebind at `brave://extensions/shortcuts`)
|
||||
|
||||
Enable any combination.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Popup shows "host unreachable" / "Specified native messaging host not found":**
|
||||
- Did UAC prompt complete? Re-run `install-host.ps1`.
|
||||
- `reg query "HKLM\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host"` must return the manifest path. If empty: install script failed silently.
|
||||
- Manifest file must exist at the registered path AND be valid JSON AND have **no BOM**. Check: `python -c "import json; json.load(open(r'D:\DEV\Extensions\Production\rclone-jav\host\com.rcjav.host.json'))"` — no error = valid.
|
||||
- Fully kill all `brave.exe`, then reopen.
|
||||
- `allowed_origins` in the manifest must match the current extension ID. If you reloaded the extension and the ID changed, re-run `install-host.ps1`.
|
||||
|
||||
**Popup shows "host error: ...":**
|
||||
- Host launched but Python died. Read `D:\DEV\Extensions\Production\rclone-jav\host\logs\rcjav-host-stderr.log`.
|
||||
- Common cause: wrong `python.exe` path in `rcjav-host.bat`, or wrong `RC_JAV` path in `rcjav-host.py`.
|
||||
|
||||
**Badge stays `?`:**
|
||||
- Page title has no JAV ID. Extension regex: `[A-Z]{2,}-?[0-9]{2,5}`.
|
||||
|
||||
**Badge stays `!`:**
|
||||
- Host returned non-zero exit code. Check `host\logs\rcjav-host-stderr.log` and `host\logs\rcjav-host.log`.
|
||||
|
||||
**Search returns no hits but the file exists:**
|
||||
- Cache may be stale. Run `python rc-jav.py --scan` to refresh, or pass `--quick` (extension does this by default).
|
||||
|
||||
**Brave verbose logging for deep debug:**
|
||||
```powershell
|
||||
& "C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --enable-logging=stderr --vmodule="*native_messaging*=2" 2>&1 | Tee-Object D:\brave-nm.log
|
||||
# Click ping in extension, then:
|
||||
Select-String -Path D:\brave-nm.log -Pattern "native|rcjav|com\.rc" -Context 0,2
|
||||
```
|
||||
|
||||
`Can't find manifest for native messaging host com.rcjav.host` = registry not seen → re-run installer + restart Brave.
|
||||
|
||||
---
|
||||
|
||||
## Uninstall
|
||||
|
||||
```powershell
|
||||
# Remove registry entries (admin)
|
||||
$keys = @(
|
||||
'HKLM:\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\WOW6432Node\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\Chromium\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKCU:\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKCU:\Software\Chromium\NativeMessagingHosts\com.rcjav.host'
|
||||
)
|
||||
foreach ($k in $keys) { Remove-Item -Path $k -Force -ErrorAction SilentlyContinue }
|
||||
```
|
||||
|
||||
Then remove the extension at `brave://extensions`, and delete `D:\DEV\Extensions\Production\rclone-jav\`.
|
||||
|
||||
---
|
||||
|
||||
## Key gotchas learned the hard way
|
||||
|
||||
| Gotcha | Symptom | Fix |
|
||||
|---|---|---|
|
||||
| UTF-8 BOM in manifest | "Specified native messaging host not found." | Write file with `[System.Text.UTF8Encoding]::new($false)` |
|
||||
| HKCU-only registration | Same error, even with valid manifest | Register HKLM (admin) |
|
||||
| Buffered Python stdio | Host launches but Brave times out | `python -u` in .bat |
|
||||
| Text-mode stdio on Windows | First message corrupts immediately | `msvcrt.setmode(..., os.O_BINARY)` in host script |
|
||||
| stderr on stdout | Garbage bytes break length prefix | Redirect stderr to file in .bat |
|
||||
| Extension reload != Brave restart | Stale NM cache | Kill all `brave.exe` then reopen |
|
||||
| Extension ID changes on reload | allowed_origins mismatch | Re-run `install-host.ps1` (or `register-host.bat`) with new ID |
|
||||
| em-dashes (—) in .ps1 file + Windows PS 5.1 + no BOM | `Missing closing '}'` parse errors at random lines | Strip em-dashes (replace with `-`), or save .ps1 with UTF-8 BOM, or invoke via `pwsh` instead of `powershell.exe` |
|
||||
@@ -0,0 +1,79 @@
|
||||
# rclone-jav
|
||||
|
||||
Brave extension that extracts JAV IDs from the active page and checks them against your local rc-jav library via a native-messaging Python host.
|
||||
|
||||
> **Setup:** see [`INSTALL.md`](./INSTALL.md). This file documents how it works once installed.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Brave tab title (e.g. "MILK-257")
|
||||
─► content/background script extracts ID
|
||||
─► chrome.runtime.connectNative("com.rcjav.host")
|
||||
─► rcjav-host.bat → rcjav-host.py
|
||||
─► python rc-jav.py --search MILK-257 --basic --no-color
|
||||
─► result back through the port
|
||||
─► popup / notification / badge
|
||||
```
|
||||
|
||||
Long-lived port: Brave keeps the host process alive while the connection is open, so subsequent lookups don't pay Python startup cost.
|
||||
|
||||
## Triggers (all available, enable any combination in Options)
|
||||
|
||||
| Trigger | Behavior |
|
||||
|---|---|
|
||||
| **Auto-check every page load** | Fires `chrome.tabs.onUpdated`. Badge shows hit count or `?` when no JAV ID detected. Noisy. |
|
||||
| **Auto-check on known JAV sites** | Same as above but gated by host patterns (e.g. `*.javdb.com`). |
|
||||
| **Toolbar icon popup** | Click the icon → popup can run a check on the active tab. The Scan Behavior toggle gates popup auto-check. |
|
||||
| **Right-click context menu** | "rclone-jav: check this page". Shows a desktop notification. |
|
||||
| **Keyboard shortcut (Alt+J)** | Rebind at `brave://extensions/shortcuts`. |
|
||||
|
||||
Enable them all to compare — they don't conflict.
|
||||
|
||||
## ID extraction
|
||||
|
||||
Uses site adapters, URL text, and the page title plus ID normalizers. Common title forms include:
|
||||
- `MILK-257` → `MILK-257`
|
||||
- `MILK257` → `MILK-257`
|
||||
- `Some site - MILK-257 [4K]` → `MILK-257`
|
||||
|
||||
## Search behavior
|
||||
|
||||
By default the extension passes `--quick` to rc-jav (single-ID live `rclone lsjson --include`, ~1–2s, fresh data, no cache). Uncheck **Use --quick mode** in options to use the cache (faster on warm cache, may be stale).
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
manifest.json MV3 manifest
|
||||
background.js service worker, native-messaging port, triggers
|
||||
content.js page extraction, element picker, and in-page overlays
|
||||
popup.html / .css / .js click-the-icon UI
|
||||
options.html / .js trigger toggles + known-site patterns
|
||||
icons/ extension icons
|
||||
host/
|
||||
rcjav-host.py native messaging host (length-prefixed JSON)
|
||||
rcjav-host.bat shim so the manifest path is stable
|
||||
install-host.ps1 writes the manifest into Brave's NM dir
|
||||
register-host.bat prompts for ID, calls install-host.ps1
|
||||
com.rcjav.host.json generated by install-host.ps1 (UTF-8 no BOM)
|
||||
logs/ host logs and delete audit log
|
||||
state/ scan progress state
|
||||
docs/
|
||||
INSTALL.md setup guide
|
||||
README.md this file
|
||||
EXTENSION_ID.md extension ID stability notes
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- The host manifest's `allowed_origins` is pinned to your extension ID. Only that extension can connect.
|
||||
- No network ports opened.
|
||||
- Host only spawns `python rc-jav.py …`; no shell, no arbitrary code execution.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Quick reference — full troubleshooting in [`INSTALL.md`](./INSTALL.md#troubleshooting).
|
||||
|
||||
- Badge `?` → no JAV ID in title.
|
||||
- Badge `!` → host error. Read `host\logs\rcjav-host-stderr.log`.
|
||||
- "host unreachable" → re-run `install-host.ps1`, fully restart Brave.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "com.rcjav.host",
|
||||
"description": "rclonex native messaging host (rc-jav bridge)",
|
||||
"path": "D:\\DEV\\Extensions\\Production\\rclone-jav\\host\\rcjav-host.bat",
|
||||
"type": "stdio",
|
||||
"allowed_origins": [
|
||||
"chrome-extension://afbnfamppannbmhgphbbgdkmilijfagp/"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "com.rcjav.host",
|
||||
"description": "rclonex native messaging host (rc-jav bridge)",
|
||||
"path": "__HOST_BAT__",
|
||||
"type": "stdio",
|
||||
"allowed_origins": [
|
||||
"chrome-extension://__EXTENSION_ID__/"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# install-host.ps1
|
||||
# Registers rclonex native-messaging host so Brave can launch it.
|
||||
#
|
||||
# Usage: .\install-host.ps1 -ExtensionId <id-from-brave://extensions>
|
||||
#
|
||||
# Writes manifest to host\com.rcjav.host.json with the correct path + extension ID baked in,
|
||||
# then registers it in HKLM (requires admin - script self-elevates if needed) AND HKCU.
|
||||
# HKLM is required on some Brave installs; HKCU alone is not always honored.
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExtensionId
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$hostDir = $PSScriptRoot
|
||||
$batPath = Join-Path $hostDir "rcjav-host.bat"
|
||||
if (-not (Test-Path $batPath)) { throw "Host bat not found: $batPath" }
|
||||
|
||||
$manifestPath = Join-Path $hostDir "com.rcjav.host.json"
|
||||
$template = Join-Path $hostDir "com.rcjav.host.json.template"
|
||||
if (-not (Test-Path $template)) { throw "Template not found: $template" }
|
||||
|
||||
$content = Get-Content $template -Raw
|
||||
$content = $content.Replace("__HOST_BAT__", ($batPath -replace "\\", "\\"))
|
||||
$content = $content.Replace("__EXTENSION_ID__", $ExtensionId)
|
||||
|
||||
# UTF-8 WITHOUT BOM - Chrome/Brave rejects manifests with a BOM.
|
||||
[System.IO.File]::WriteAllText($manifestPath, $content, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Host "Manifest written: $manifestPath"
|
||||
|
||||
# Self-elevate for HKLM if not already admin.
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host "Not running as admin - relaunching elevated to write HKLM..."
|
||||
Start-Process pwsh -Verb RunAs -ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-File", $PSCommandPath,
|
||||
"-ExtensionId", $ExtensionId
|
||||
)
|
||||
exit
|
||||
}
|
||||
|
||||
# Register in HKLM - required on some Brave installs.
|
||||
$keys = @(
|
||||
'HKLM:\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\WOW6432Node\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKLM:\Software\Chromium\NativeMessagingHosts\com.rcjav.host',
|
||||
# HKCU as belt-and-suspenders for installs that prefer it
|
||||
'HKCU:\Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.rcjav.host',
|
||||
'HKCU:\Software\Chromium\NativeMessagingHosts\com.rcjav.host'
|
||||
)
|
||||
foreach ($k in $keys) {
|
||||
try {
|
||||
New-Item -Path $k -Force -ErrorAction Stop | Out-Null
|
||||
Set-Item -Path $k -Value $manifestPath
|
||||
Write-Host " Set: $k"
|
||||
} catch {
|
||||
Write-Host " FAILED: $k ($($_.Exception.Message))"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Manifest contents:"
|
||||
Write-Host "-------------------"
|
||||
Write-Host $content
|
||||
Write-Host "-------------------"
|
||||
Write-Host ""
|
||||
Write-Host "Fully restart Brave (kill all brave.exe processes, then reopen) for the registry"
|
||||
Write-Host "entries to be picked up. Then click rclonex toolbar > Ping host."
|
||||
Read-Host "Press Enter to close"
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
REM Portable: uses Windows py launcher if present, falls back to python on PATH.
|
||||
REM Stderr redirected to log file so it can't pollute stdout (native messaging is binary).
|
||||
setlocal
|
||||
set "PYBIN=python"
|
||||
where py >nul 2>&1 && set "PYBIN=py"
|
||||
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
||||
"%PYBIN%" -u "%~dp0rcjav-host.py" 2>>"%~dp0logs\rcjav-host-stderr.log"
|
||||
@@ -0,0 +1 @@
|
||||
stdin closed, exiting
|
||||
+2041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
REM Double-click this to register the native messaging host with Brave.
|
||||
REM Prompts for the extension ID, then runs install-host.ps1.
|
||||
|
||||
setlocal
|
||||
set /p EXT_ID="Paste the rclone-jav extension ID from brave://extensions: "
|
||||
|
||||
if "%EXT_ID%"=="" (
|
||||
echo No ID entered. Aborting.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-host.ps1" -ExtensionId "%EXT_ID%"
|
||||
echo.
|
||||
echo Done. Press any key to close.
|
||||
pause >nul
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 810 B |
Binary file not shown.
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "rclone-jav",
|
||||
"version": "0.1.0",
|
||||
"description": "Check current page title against your rc-jav library via native messaging.",
|
||||
"permissions": [
|
||||
"nativeMessaging",
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"notifications",
|
||||
"activeTab",
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "rclone-jav — check page",
|
||||
"default_icon": {
|
||||
"32": "icons/icon-32.png",
|
||||
"128": "icons/icon-128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"32": "icons/icon-32.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"options_page": "options.html",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"check-current-page": {
|
||||
"suggested_key": { "default": "Alt+J" },
|
||||
"description": "rclone-jav: check current page title"
|
||||
}
|
||||
}
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
/* rclone-jav options page styles
|
||||
* Extracted from options.html step 2 of the console consolidation refactor.
|
||||
* See mockups/console-consolidation-claude.html for sequence + rationale.
|
||||
* Per-pane split happens later (step 6) alongside per-pane JS extraction. */
|
||||
|
||||
body { font-family: -apple-system, Segoe UI, sans-serif; background: #0f0f0f; color: #ddd; margin: 0; padding: 24px; }
|
||||
.shell {
|
||||
max-width: 1040px; margin: 0 auto;
|
||||
background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,.4);
|
||||
}
|
||||
.layout { display: grid; grid-template-columns: 220px 1fr; min-height: 620px; }
|
||||
|
||||
/* sidebar */
|
||||
.side { background: #131313; border-right: 1px solid #222; padding: 14px 0; position: sticky; top: 0; align-self: start; height: 100vh; max-height: 720px; overflow-y: auto; }
|
||||
.side .brand { padding: 2px 22px 16px; border-bottom: 1px solid #202020; margin-bottom: 14px; }
|
||||
.side .brand strong { display:block; color:#f3f3f3; font-size:14px; letter-spacing:.2px; }
|
||||
.side .brand span { display:block; color:#666; font-size:11px; margin-top:3px; }
|
||||
.side .group { padding: 0 14px; margin-bottom: 16px; }
|
||||
.side .group:last-child { margin-bottom: 0; }
|
||||
.side .gtitle { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px; padding: 0 8px; }
|
||||
.side .item { padding: 8px 10px; font-size: 13px; color: #aaa; cursor: pointer; border-radius: 5px; user-select: none; display: flex; align-items: center; gap: 9px; }
|
||||
.side .item .icon { width: 16px; opacity: 0.72; text-align:center; font-size:12px; }
|
||||
.side .item:hover { background: #1f1f1f; color: #ddd; }
|
||||
.side .item.active { background: #2a2a2a; color: #fff; box-shadow: inset 2px 0 #6ec1ff; }
|
||||
.side .item.danger { color: #faa; }
|
||||
.side .item.danger:hover { background: #2a1a1a; }
|
||||
.side .item.danger.active { background: #3a1a1a; color: #ffbbbb; }
|
||||
|
||||
/* main pane */
|
||||
.main { padding: 26px 32px; overflow-y: auto; }
|
||||
.pane-head { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid #242424; }
|
||||
.main h1 { margin: 0 0 5px; font-size: 19px; letter-spacing:.2px; }
|
||||
.main .pdesc { color: #888; font-size: 12px; margin: 0; line-height: 1.5; max-width: 700px; }
|
||||
.pane { display: none; }
|
||||
.pane.active { display: block; }
|
||||
|
||||
/* common form bits */
|
||||
label { display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: 13px; cursor: pointer; }
|
||||
label .sublabel { color: #777; font-size: 11px; display: block; margin-top: 2px; }
|
||||
input[type=text], textarea {
|
||||
width: 100%; background: #0d0d0d; color: #ddd; border: 1px solid #2a2a2a;
|
||||
padding: 8px 10px; box-sizing: border-box; border-radius: 4px;
|
||||
font-family: Consolas, monospace; font-size: 12px;
|
||||
}
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
input[type=text]:focus, textarea:focus { border-color: #ff6f3c; outline: none; }
|
||||
.fieldset { background: #161616; border: 1px solid #2a2a2a; border-radius: 5px; padding: 13px 15px; margin-bottom: 12px; }
|
||||
.fieldset .ftitle { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; font-weight: 650; }
|
||||
.help { color: #777; font-size: 11px; margin: 4px 0 10px; line-height: 1.4; }
|
||||
.help code { background: #0d0d0d; padding: 2px 4px; border-radius: 3px; font-size: 11px; }
|
||||
select, input[type=number], input[type=color] { background:#0d0d0d; color:#ddd; border:1px solid #2a2a2a; border-radius:4px; font-family:Consolas,monospace; font-size:12px; }
|
||||
select { padding: 6px 8px; }
|
||||
input[type=number] { padding:6px 8px; }
|
||||
.setting-list { display:grid; gap: 2px; }
|
||||
.setting-list label { border-bottom: 1px solid #222; padding: 9px 0; }
|
||||
.setting-list label:last-child { border-bottom: 0; }
|
||||
.split-grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.button-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
.section-note { background:#101820; border:1px solid #24303a; color:#9dccff; border-radius:5px; padding:9px 11px; font-size:12px; line-height:1.45; margin-bottom:12px; }
|
||||
.section-note.warn { background:#242410; border-color:#4a4420; color:#ffdd77; }
|
||||
.section-note.danger { background:#2a1414; border-color:#5a2525; color:#ffb3b3; }
|
||||
.mono-output { margin-top:10px; font-family:Consolas,monospace; font-size:11px; color:#aaa; line-height:1.5; overflow-wrap:anywhere; }
|
||||
.compact-grid { display:grid; grid-template-columns: minmax(0, 1fr) auto; gap:8px; align-items:center; }
|
||||
.chip-row { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
|
||||
.chip-btn { padding: 4px 9px; font-size:11px; border-radius: 10px; }
|
||||
.activity-filters { display:flex; gap:6px; align-items:center; flex-wrap:wrap; margin-top:9px; }
|
||||
.activity-filter { padding:4px 10px; border-radius:12px; font-size:11px; }
|
||||
.activity-filter.active { background:#1a2430; border-color:#36526a; color:#9dccff; }
|
||||
.activity-entry { margin-top:8px; }
|
||||
.activity-head { display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
|
||||
.activity-pill { border:1px solid #333; border-radius:11px; padding:2px 8px; font-size:10px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; white-space:nowrap; }
|
||||
.activity-pill.hit { background:#15351e; border-color:#285c38; color:#8ff0ae; }
|
||||
.activity-pill.miss { background:#383315; border-color:#655a24; color:#ffe487; }
|
||||
.activity-pill.no-id { background:#172839; border-color:#2d4f70; color:#9dccff; }
|
||||
.activity-pill.paused { background:#292334; border-color:#493b62; color:#d0b5ff; }
|
||||
.activity-pill.error { background:#3a1a1a; border-color:#722; color:#faa; }
|
||||
.activity-meta { color:#aaa; }
|
||||
/* Non-JAV files panel (skipped files in cache status) */
|
||||
.nonjav-panel { margin-top: 8px; background: rgba(255,255,255,.02); border: 1px solid #1e1e2a; border-radius: 6px; overflow: hidden; }
|
||||
.nonjav-panel-head { display: flex; align-items: center; gap: 8px; padding: 7px 12px; background: rgba(255,255,255,.03); border-bottom: 1px solid #1e1e2a; }
|
||||
.nonjav-panel-title { font-size: 12px; color: #b0b0c8; flex: 1; }
|
||||
.nonjav-del-all { font-size: 11px; padding: 3px 10px; border-radius: 10px; background: rgba(248,113,113,.12); border: 1px solid rgba(248,113,113,.25); color: #fca5a5; cursor: pointer; }
|
||||
.nonjav-del-all:hover { background: rgba(248,113,113,.22); }
|
||||
.nonjav-del-all:disabled { opacity: 0.4; cursor: default; }
|
||||
.nonjav-list { max-height: 220px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #2a2a3a #0d0d1a; }
|
||||
.nonjav-item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; border-bottom: 1px solid rgba(255,255,255,.04); font-size: 11px; }
|
||||
.nonjav-item:last-child { border-bottom: none; }
|
||||
.nonjav-item.deleted { opacity: 0.35; pointer-events: none; }
|
||||
.nonjav-ext { font-family: Consolas, monospace; font-size: 10px; padding: 1px 5px; border-radius: 3px; background: rgba(255,255,255,.06); color: #888; flex-shrink: 0; min-width: 36px; text-align: center; }
|
||||
.nonjav-path { flex: 1; font-family: Consolas, monospace; color: #9090b0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.nonjav-del-one { font-size: 10px; padding: 2px 8px; border-radius: 8px; background: rgba(248,113,113,.08); border: 1px solid rgba(248,113,113,.2); color: #f87171; cursor: pointer; flex-shrink: 0; }
|
||||
.nonjav-del-one:hover { background: rgba(248,113,113,.18); }
|
||||
.nonjav-status { font-size: 11px; padding: 5px 12px; color: #8888aa; }
|
||||
.muted { color:#777; font-size:11px; }
|
||||
.disabled-soft { opacity:.48; }
|
||||
.danger-zone { border-color:#5a2525; background:#201414; }
|
||||
.scan-job-head { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:8px; }
|
||||
.scan-pill { border-radius:10px; padding:2px 8px; background:#1a2430; color:#9dccff; border:1px solid #2d4258; font-size:10px; font-weight:700; text-transform:uppercase; }
|
||||
.scan-pill.ok { background:#1a3a1a; color:#afa; border-color:#2e5a2e; }
|
||||
.scan-pill.fail { background:#3a1a1a; color:#faa; border-color:#722; }
|
||||
.scan-remote { border-top:1px solid #242424; padding:7px 0; }
|
||||
.scan-remote:first-of-type { border-top:0; }
|
||||
.scan-track { height:4px; margin-top:5px; background:#202020; border-radius:3px; overflow:hidden; }
|
||||
.scan-fill { height:100%; background:#6ec1ff; min-width:0; }
|
||||
|
||||
/* buttons */
|
||||
button { background: #2a2a2a; color: #ddd; border: 1px solid #3a3a3a; border-radius: 4px; padding: 6px 14px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
||||
button:hover { background: #333; }
|
||||
button.primary { background: #1a3a1a; border-color: #2e5a2e; color: #afa; font-weight: 600; letter-spacing: 0.3px; }
|
||||
button.primary:hover { background: #235023; }
|
||||
button.danger { background: #511; border-color: #722; color: #faa; font-weight: 600; letter-spacing: 0.5px; }
|
||||
button.danger:hover { background: #722; }
|
||||
.actions { display: flex; gap: 8px; align-items: center; padding-top: 14px; border-top: 1px solid #222; margin-top: 20px; position: sticky; bottom: 0; background: linear-gradient(180deg, rgba(26,26,26,0), #1a1a1a 30%); }
|
||||
.actions .saved { color: #afa; font-size: 12px; margin-left: 8px; opacity: 0; transition: opacity .2s; }
|
||||
.actions .saved.show { opacity: 1; }
|
||||
|
||||
/* adapter table */
|
||||
.adapters { width: 100%; border-collapse: collapse; }
|
||||
.adapters th, .adapters td { padding: 6px 6px; text-align: left; font-size: 12px; border-bottom: 1px solid #232323; vertical-align: middle; }
|
||||
.adapters th { color: #888; font-weight: normal; }
|
||||
.adapters th:nth-child(1), .adapters td:nth-child(1) { width: 200px; }
|
||||
.adapters th:nth-child(3), .adapters td:nth-child(3) { width: 32px; text-align: right; }
|
||||
.adapters td input { width: 100%; height: 28px; padding: 4px 6px; }
|
||||
.adapters button.del { background: #511; border: 1px solid #722; color: #faa; padding: 0; width: 26px; height: 26px; line-height: 24px; font-size: 14px; border-radius: 3px; }
|
||||
|
||||
/* diagnostics rows */
|
||||
.diag-row { padding: 6px 10px; border-radius: 4px; margin-bottom: 4px; font-size: 12px; display: grid; grid-template-columns: 30px 150px 1fr; gap: 10px; align-items: start; }
|
||||
.diag-row.ok { background: #1a3a1a; color: #afa; }
|
||||
.diag-row.warn { background: #3a3a1a; color: #ffa; }
|
||||
.diag-row.fail { background: #3a1a1a; color: #faa; }
|
||||
.diag-row.info { background: #1a2430; color: #9dccff; }
|
||||
.diag-row .icon { font-weight: 700; }
|
||||
.diag-row .name { font-weight: 600; }
|
||||
.diag-row .detail { color: rgba(255,255,255,0.72); font-family: Consolas, monospace; font-size: 11px; overflow-wrap: anywhere; }
|
||||
.diag-row details { margin: 0; }
|
||||
.diag-row summary { cursor: pointer; color: rgba(255,255,255,0.85); }
|
||||
.diag-row pre { white-space: pre-wrap; margin: 6px 0 0; font-family: Consolas, monospace; font-size: 11px; color: rgba(255,255,255,0.72); }
|
||||
.diag-action { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.diag-action button { padding: 3px 8px; font-size: 11px; }
|
||||
|
||||
/* radio chips */
|
||||
.radio-group { display: flex; gap: 6px; margin: 10px 0; }
|
||||
.radio-group label { background: #161616; border: 1px solid #2a2a2a; padding: 6px 12px; border-radius: 4px; font-size: 12px; padding-top: 6px; padding-bottom: 6px; }
|
||||
.radio-group label input { margin: 0 6px 0 0; }
|
||||
.radio-group label.selected { background: #1a3a1a; border-color: #2e5a2e; color: #afa; }
|
||||
.radio-group label.selected.danger { background: #3a1a1a; border-color: #722; color: #faa; }
|
||||
|
||||
code { background: #0d0d0d; padding: 2px 5px; border-radius: 3px; font-size: 11px; font-family: Consolas, monospace; }
|
||||
|
||||
/* Overlay horizontal tabs */
|
||||
.overlay-tabs { display: flex; gap: 0; margin-bottom: 16px; border-bottom: 1px solid #2a2a2a; }
|
||||
.otab { background: none; border: none; border-bottom: 2px solid transparent; color: #666; font-size: 13px; font-weight: 600; padding: 8px 18px; cursor: pointer; border-radius: 0; margin-bottom: -1px; letter-spacing: 0.3px; transition: color .15s, border-color .15s; }
|
||||
.otab:hover { color: #aaa; background: none; }
|
||||
.otab.active { color: #ddd; border-bottom-color: #6ec1ff; }
|
||||
.otab-panel { display: none; }
|
||||
.otab-panel.active { display: block; }
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.72); z-index: 30; display: none; align-items: center; justify-content: center; padding: 24px; }
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal { width: min(860px, 100%); max-height: min(760px, calc(100vh - 48px)); background:#161616; border:1px solid #343434; border-radius:8px; box-shadow:0 18px 80px rgba(0,0,0,.6); display:flex; flex-direction:column; overflow:hidden; }
|
||||
.modal-head, .modal-actions { display:flex; gap:10px; align-items:center; justify-content:space-between; padding:14px 16px; border-bottom:1px solid #262626; }
|
||||
.modal-actions { border-bottom:0; border-top:1px solid #262626; justify-content:flex-end; }
|
||||
.modal-title { font-size:15px; font-weight:650; color:#f1f1f1; }
|
||||
.modal-subtitle { margin-top:3px; color:#777; font-size:11px; font-family:Consolas,monospace; overflow-wrap:anywhere; }
|
||||
.modal-body { padding:14px 16px; overflow:auto; }
|
||||
.modal-help { color:#888; font-size:12px; line-height:1.45; margin-bottom:12px; }
|
||||
.modal-field { margin-bottom:12px; }
|
||||
.modal-field > label { display:block; color:#bbb; font-size:12px; padding:0; margin-bottom:5px; cursor:default; }
|
||||
.modal-field .prof-remote-group { background:#0a0a0a; border:1px solid #222; border-radius:4px; padding:8px 10px; }
|
||||
.profile-card { background:#161616; border:1px solid #2a2a2a; border-radius:5px; padding:12px 14px; margin-bottom:10px; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:12px; align-items:start; }
|
||||
.profile-card .name { color:#f1f1f1; font-weight:650; margin-bottom:5px; }
|
||||
.profile-card .roots { color:#888; font-family:Consolas,monospace; font-size:11px; line-height:1.5; overflow-wrap:anywhere; }
|
||||
.profile-card .actions { margin:0; display:flex; gap:6px; align-items:center; }
|
||||
.profile-card .actions button { padding:4px 9px; font-size:11px; }
|
||||
.modal-confirm { background:#201414; border:1px solid #5a2525; border-radius:5px; color:#ffb3b3; font-size:12px; line-height:1.5; padding:11px 12px; }
|
||||
.part-detector-list { display:grid; gap:9px; }
|
||||
.part-detector-row { background:#101010; border:1px solid #252525; border-radius:5px; padding:10px; }
|
||||
.part-detector-row.builtin { background:#101820; border-color:#24303a; }
|
||||
.part-detector-head { display:grid; grid-template-columns:minmax(0,1fr) 28px; gap:8px; align-items:center; }
|
||||
.part-detector-row.builtin .part-detector-head { grid-template-columns:minmax(0,1fr) auto; }
|
||||
.part-detector-head input { min-width:0; }
|
||||
.part-detector-head input:read-only { color:#9dccff; }
|
||||
.part-detector-head button { background:#511; border-color:#722; color:#faa; width:28px; height:28px; padding:0; font-size:14px; }
|
||||
.part-detector-kind { color:#9dccff; border:1px solid #314453; border-radius:10px; padding:2px 8px; font-size:10px; font-weight:650; white-space:nowrap; }
|
||||
.part-detector-feedback { margin-top:7px; font-size:11px; line-height:1.45; color:#888; }
|
||||
.part-detector-feedback .ok { color:#afa; }
|
||||
.part-detector-feedback .warn { color:#ffdd77; }
|
||||
.part-detector-feedback .fail { color:#faa; }
|
||||
.part-detector-match { color:#9dccff; font-family:Consolas,monospace; margin-top:3px; overflow-wrap:anywhere; }
|
||||
.skip-summary { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:12px; }
|
||||
.skip-summary span { background:#242410; border:1px solid #4a4420; color:#ffdd77; border-radius:10px; padding:3px 8px; font-size:11px; }
|
||||
.skip-row { border-top:1px solid #242424; padding:9px 0; }
|
||||
.skip-row:first-of-type { border-top:0; }
|
||||
.skip-row .name { color:#ddd; font-family:Consolas,monospace; font-size:12px; }
|
||||
.skip-row .reason { color:#ffdd77; font-size:11px; margin-top:3px; }
|
||||
.skip-row .path { color:#777; font-family:Consolas,monospace; font-size:11px; margin-top:3px; overflow-wrap:anywhere; }
|
||||
.cache-freshness { display:flex; gap:14px; align-items:center; flex-wrap:wrap; margin:8px 0 10px; }
|
||||
.cache-freshness label { display:inline-flex; gap:8px; align-items:center; padding:0; white-space:nowrap; cursor:default; }
|
||||
.cache-freshness input { width:66px; }
|
||||
.cache-freshness .sublabel { max-width:340px; margin:0; line-height:1.35; }
|
||||
|
||||
/* ── Duplicate Review — Variant 9 (Frosted Modern) ─────────────────── */
|
||||
#dupe-review-modal .modal { background: linear-gradient(135deg, #0f0f1a 0%, #0d1117 100%); border-color: #2a2a3a; width: 65vw; }
|
||||
#dupe-review-modal .modal-head { background: linear-gradient(90deg, #12122a, #0d0d1a); border-bottom-color: #1e1e3a; }
|
||||
#dupe-review-modal .modal-title { color: #e8e8ff; }
|
||||
#dupe-review-modal .modal-subtitle { color: #8888aa; font-family: system-ui, sans-serif; }
|
||||
#dupe-review-modal .modal-head button { color: #6060aa; border-color: #2a2a4a; }
|
||||
#dupe-review-modal .modal-head button:hover { color: #b0b0d0; background: rgba(255,255,255,.05); }
|
||||
#dupe-review-modal .modal-actions { border-top-color: #1a1a2a; background: rgba(0,0,0,.3); }
|
||||
#dupe-review-modal .modal-actions button { background: rgba(255,255,255,.07); border-color: #3a3a5a; color: #c8c8e8; }
|
||||
#dupe-review-modal .modal-actions button:hover { background: rgba(255,255,255,.13); }
|
||||
#dupe-review-modal .modal-body { padding: 0; scrollbar-width: thin; scrollbar-color: #2a2a4a #0d0d1a; }
|
||||
#dupe-review-modal .modal-body::-webkit-scrollbar { width: 6px; }
|
||||
#dupe-review-modal .modal-body::-webkit-scrollbar-track { background: #0d0d1a; }
|
||||
#dupe-review-modal .modal-body::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
|
||||
#dupe-review-modal .modal-body::-webkit-scrollbar-thumb:hover { background: #3a3a6a; }
|
||||
|
||||
.dr-stats { display: flex; border-bottom: 1px solid #1e1e32; }
|
||||
.dr-stat { flex: 1; padding: 11px 16px; border-right: 1px solid #1e1e32; }
|
||||
.dr-stat:last-child { border-right: none; }
|
||||
.dr-stat .val { font-size: 17px; font-weight: 700; color: #fff; }
|
||||
.dr-stat .val.red { color: #f87171; }
|
||||
.dr-stat .val.blue { color: #60a5fa; }
|
||||
.dr-stat .key { font-size: 10px; color: #aaaacc; margin-top: 2px; text-transform: uppercase; letter-spacing: .07em; }
|
||||
|
||||
.dr-roots { font-size: 12px; color: #9999bb; padding: 6px 16px; border-bottom: 1px solid #1a1a2a; font-family: Consolas, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.dr-body { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
.dr-card-wrap { display: flex; align-items: stretch; }
|
||||
.dr-card-wrap.dr-hidden { display: none; }
|
||||
.dr-card-wrap.skipped .dr-card { opacity: 0.38; pointer-events: none; }
|
||||
.dr-card-wrap.skipped .dr-skip-ear { background: rgba(251,191,36,.07); border-color: rgba(251,191,36,.25); }
|
||||
.dr-card-wrap.skipped .dr-skip-ear span { color: #fbbf24; }
|
||||
.dr-card-wrap.dr-risk .dr-card { border-color: rgba(251,191,36,.28); }
|
||||
.dr-skip-ear { width: 24px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; background: rgba(255,255,255,.02); border: 1px solid rgba(255,255,255,.06); border-left: none; border-radius: 0 6px 6px 0; transition: background .15s; }
|
||||
.dr-skip-ear:hover { background: rgba(255,255,255,.07); }
|
||||
.dr-skip-ear span { font-size: 8px; color: #333; writing-mode: vertical-rl; letter-spacing: 1px; text-transform: uppercase; font-weight: 700; user-select: none; transition: color .15s; }
|
||||
.dr-skip-ear:hover span { color: #777; }
|
||||
.dr-card { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.06); border-radius: 8px 0 0 8px; overflow: hidden; flex: 1; min-width: 0; }
|
||||
.dr-card-head { display: flex; align-items: center; padding: 9px 14px; gap: 8px; border-bottom: 1px solid rgba(255,255,255,.04); }
|
||||
.dr-card-id { font-weight: 700; color: #e0e0ff; font-family: Consolas, monospace; font-size: 13px; }
|
||||
.dr-card-reclaim { margin-left: auto; font-size: 11px; color: #ff7171; background: rgba(255,100,100,.1); padding: 2px 9px; border-radius: 12px; white-space: nowrap; }
|
||||
.dr-card-body { padding: 9px 14px; display: flex; flex-direction: column; gap: 7px; }
|
||||
|
||||
.dr-badge { font-size: 9px; font-weight: 700; letter-spacing: .07em; padding: 2px 7px; border-radius: 10px; white-space: nowrap; }
|
||||
.dr-badge.b4k { background: rgba(96,165,250,.12); color: #60a5fa; border: 1px solid rgba(96,165,250,.2); }
|
||||
.dr-badge.b1080 { background: rgba(167,139,250,.12); color: #a78bfa; border: 1px solid rgba(167,139,250,.2); }
|
||||
.dr-badge.bcljav { background: rgba(74,222,128,.12); color: #4ade80; border: 1px solid rgba(74,222,128,.2); }
|
||||
.dr-badge.bfmt { background: rgba(251,146,60,.12); color: #fb923c; border: 1px solid rgba(251,146,60,.2); }
|
||||
.dr-badge.bmkv { background: rgba(251,146,60,.09); color: #fb923c; border: 1px solid rgba(251,146,60,.15); }
|
||||
|
||||
.dr-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.dr-tag { font-size: 9px; font-weight: 700; letter-spacing: .08em; padding: 2px 8px; border-radius: 10px; min-width: 58px; text-align: center; white-space: nowrap; flex-shrink: 0; }
|
||||
.dr-tag.keep { background: rgba(74,222,128,.12); color: #4ade80; border: 1px solid rgba(74,222,128,.2); }
|
||||
.dr-tag.del { background: rgba(248,113,113,.12); color: #f87171; border: 1px solid rgba(248,113,113,.2); }
|
||||
.dr-tag.cat { background: rgba(96,165,250,.12); color: #93c5fd; border: 1px solid rgba(96,165,250,.2); }
|
||||
.dr-path { font-family: Consolas, monospace; font-size: 11px; color: #9090b0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
|
||||
.dr-row.keep .dr-path { color: #c0c0e0; }
|
||||
.dr-keep-reason { color:#8ff0ae; font-size:10px; font-family:Consolas,monospace; margin:-2px 0 2px 68px; }
|
||||
.dr-risk-note { background:rgba(251,191,36,.09); border:1px solid rgba(251,191,36,.24); border-radius:5px; color:#ffe487; font-size:11px; line-height:1.42; margin:0 0 3px; padding:7px 9px; }
|
||||
.dr-risk-note strong { color:#fff0b0; }
|
||||
.dr-sz { font-size: 11px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
|
||||
.dr-sz.keep { color: #4ade80; }
|
||||
.dr-sz.del { color: #f87171; }
|
||||
.dr-sz.cat { color: #93c5fd; }
|
||||
|
||||
/* Interactive delete row states */
|
||||
.dr-row.del { cursor: default; }
|
||||
.dr-row.del:hover { background: rgba(248,113,113,.03); }
|
||||
.dr-row.del.confirmed { background: rgba(248,113,113,.08); }
|
||||
.dr-row.del.confirmed .dr-tag.del { background: rgba(248,113,113,.25); color: #fca5a5; border-color: rgba(248,113,113,.4); }
|
||||
.dr-row.del.confirmed .dr-path { text-decoration: line-through; text-decoration-color: #f87171; }
|
||||
.dr-row.del.done { opacity: 0.35; pointer-events: none; }
|
||||
.dr-row.del.done .dr-path { text-decoration: line-through; color: #555; }
|
||||
.dr-row.del.error .dr-tag.del { background: rgba(251,191,36,.12); color: #fbbf24; border-color: rgba(251,191,36,.25); }
|
||||
/* KEEP row — clickable to swap roles */
|
||||
.dr-row.keep { cursor: pointer; user-select: none; }
|
||||
.dr-row.keep:hover { background: rgba(248,113,113,.03); }
|
||||
|
||||
#dupe-review-execute { background: rgba(248,113,113,.15); border-color: rgba(248,113,113,.3); color: #fca5a5; }
|
||||
#dupe-review-execute:hover:not(:disabled) { background: rgba(248,113,113,.25); }
|
||||
#dupe-review-execute:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
.dr-empty { padding: 20px 16px; color: #3a3a6a; font-size: 13px; }
|
||||
.dr-skipped { padding: 8px 16px 12px; font-size: 11px; color: #bbbbdd; }
|
||||
.dr-skipped-item { font-family: Consolas, monospace; font-size: 10px; color: #9999bb; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* Keep Ranking Panel */
|
||||
.kr-panel { background: rgba(96,165,250,.04); border: 1px solid rgba(96,165,250,.12); border-radius: 8px; padding: 14px 16px; margin-top: 10px; }
|
||||
.kr-info { font-size: 11px; color: #8888aa; margin-bottom: 12px; line-height: 1.5; }
|
||||
.kr-info code { background: rgba(255,255,255,.07); padding: 1px 5px; border-radius: 3px; font-family: Consolas, monospace; color: #93c5fd; }
|
||||
.kr-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.kr-label { font-size: 12px; color: #b0b0c8; width: 140px; flex-shrink: 0; }
|
||||
.kr-input { width: 70px; background: rgba(255,255,255,.06); border: 1px solid #2a2a3a; border-radius: 5px; color: #c0c0e0; font-size: 12px; padding: 4px 8px; }
|
||||
.kr-input:focus { outline: none; border-color: rgba(96,165,250,.4); }
|
||||
.kr-unit { font-size: 11px; color: #666; }
|
||||
.kr-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #b0b0c8; cursor: pointer; }
|
||||
.kr-toggle input[type=checkbox] { accent-color: #60a5fa; }
|
||||
.kr-fmt-list { display: flex; flex-direction: column; gap: 4px; min-width: 130px; }
|
||||
.kr-fmt-item { display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,.04); border: 1px solid #2a2a3a; border-radius: 5px; padding: 5px 8px; cursor: grab; font-size: 12px; color: #c0c0e0; font-family: Consolas, monospace; user-select: none; }
|
||||
.kr-fmt-item.dragging { opacity: 0.4; }
|
||||
.kr-fmt-item.drag-over { border-color: rgba(96,165,250,.5); background: rgba(96,165,250,.08); }
|
||||
.kr-fmt-grip { color: #444; font-size: 10px; cursor: grab; }
|
||||
.kr-fmt-priority { font-size: 10px; color: #555; margin-left: auto; }
|
||||
.kr-save-row { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
|
||||
.kr-save-status { font-size: 11px; color: #8888aa; flex: 1; }
|
||||
.kr-save-status.ok { color: #6ee7b7; }
|
||||
.kr-save-status.err { color: #f87171; }
|
||||
|
||||
/* Library Issues modal */
|
||||
#library-issues-modal .modal { background: linear-gradient(135deg, #0f0f1a 0%, #0d1117 100%); border-color: #2a2a3a; width: 65vw; }
|
||||
#library-issues-modal .modal-head { background: linear-gradient(90deg, #12122a, #0d0d1a); border-bottom-color: #1e1e3a; }
|
||||
#library-issues-modal .modal-title { color: #e8e8ff; }
|
||||
#library-issues-modal .modal-subtitle { color: #8888aa; font-family: system-ui, sans-serif; }
|
||||
#library-issues-modal .modal-head button { color: #6060aa; border-color: #2a2a4a; }
|
||||
#library-issues-modal .modal-head button:hover { color: #b0b0d0; background: rgba(255,255,255,.05); }
|
||||
#library-issues-modal .modal-actions { border-top-color: #1a1a2a; background: rgba(0,0,0,.3); }
|
||||
#library-issues-modal .modal-actions button { background: rgba(255,255,255,.07); border-color: #3a3a5a; color: #c8c8e8; }
|
||||
#library-issues-modal .modal-actions button:hover { background: rgba(255,255,255,.13); }
|
||||
#library-issues-modal .modal-body { padding: 0; scrollbar-width: thin; scrollbar-color: #2a2a4a #0d0d1a; }
|
||||
#library-issues-modal .modal-body::-webkit-scrollbar { width: 6px; }
|
||||
#library-issues-modal .modal-body::-webkit-scrollbar-track { background: #0d0d1a; }
|
||||
#library-issues-modal .modal-body::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
|
||||
|
||||
/* Library issue rows */
|
||||
.li-section-head { padding: 8px 16px; font-size: 11px; font-weight: 600; color: #fbbf24; background: rgba(251,191,36,.06); border-bottom: 1px solid rgba(251,191,36,.12); }
|
||||
.li-row { display: grid; grid-template-columns: auto 1fr auto auto; gap: 8px; align-items: center; padding: 7px 16px; border-bottom: 1px solid rgba(255,255,255,.04); }
|
||||
.li-row:hover { background: rgba(255,255,255,.03); }
|
||||
.li-tag { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 10px; white-space: nowrap; }
|
||||
.li-tag.bracket { background: rgba(251,191,36,.12); color: #fbbf24; border: 1px solid rgba(251,191,36,.25); }
|
||||
.li-tag.nohyphen { background: rgba(167,139,250,.12); color: #c4b5fd; border: 1px solid rgba(167,139,250,.25); }
|
||||
.li-tag.done { background: rgba(74,222,128,.12); color: #4ade80; border: 1px solid rgba(74,222,128,.25); }
|
||||
.li-tag.conflict { background: rgba(248,113,113,.12); color: #f87171; border: 1px solid rgba(248,113,113,.25); }
|
||||
.li-names { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.li-old { font-family: Consolas, monospace; font-size: 11px; color: #8888aa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.li-arrow { font-size: 11px; color: #4ade80; }
|
||||
.li-new { font-family: Consolas, monospace; font-size: 11px; color: #c0c0e0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.li-sz { font-size: 11px; color: #6060aa; white-space: nowrap; }
|
||||
.li-rename-btn { font-size: 11px; padding: 3px 10px; border-radius: 5px; border: 1px solid #3a3a5a; background: rgba(255,255,255,.06); color: #c8c8e8; cursor: pointer; white-space: nowrap; }
|
||||
.li-rename-btn:hover:not(:disabled) { background: rgba(255,255,255,.12); }
|
||||
.li-rename-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.li-empty { padding: 24px 16px; color: #4ade80; font-size: 13px; }
|
||||
.li-stats { padding: 10px 16px; font-size: 12px; color: #8888aa; border-bottom: 1px solid rgba(255,255,255,.06); }
|
||||
.li-stats b { color: #c8c8e8; }
|
||||
|
||||
/* Dupe Review filter bar */
|
||||
.dr-filter-bar { position: sticky; top: 0; z-index: 5; display: flex; flex-wrap: wrap; align-items: center; gap: 5px; padding: 9px 14px; background: #0d0d1a; border-bottom: 1px solid #1e1e2a; }
|
||||
.dr-filter-label { font-size: 11px; color: #555; padding: 0 2px; white-space: nowrap; }
|
||||
.dr-filter-sep { width: 1px; height: 16px; background: #2a2a3a; margin: 0 4px; flex-shrink: 0; }
|
||||
.dr-chip { font-size: 11px; padding: 3px 11px; border-radius: 20px; border: 1px solid #2a2a3a; background: rgba(255,255,255,.04); color: #888; cursor: pointer; transition: all .15s; font-family: system-ui, sans-serif; }
|
||||
.dr-chip:hover { border-color: #3a3a5a; color: #aaa; }
|
||||
.dr-search { font-size: 11px; padding: 3px 10px; border-radius: 20px; border: 1px solid #2a2a3a; background: rgba(255,255,255,.04); color: #c0c0e0; outline: none; width: 160px; font-family: Consolas, monospace; }
|
||||
.dr-search::placeholder { color: #444; }
|
||||
.dr-search:focus { border-color: rgba(96,165,250,.4); background: rgba(96,165,250,.05); }
|
||||
.dr-chip.active { border-color: rgba(96,165,250,.4); background: rgba(96,165,250,.12); color: #93c5fd; }
|
||||
.dr-card.dr-hidden { display: none; }
|
||||
|
||||
/* Variant alerts */
|
||||
.dr-variant-section { margin: 16px 0 0; }
|
||||
.dr-variant-heading { padding: 8px 16px; font-size: 12px; font-weight: 600; color: #fbbf24; background: rgba(251,191,36,.06); border-top: 1px solid rgba(251,191,36,.2); border-bottom: 1px solid rgba(251,191,36,.12); }
|
||||
.dr-card.variant-alert { border-color: rgba(251,191,36,.25); }
|
||||
.dr-card.variant-alert .dr-card-head { background: rgba(251,191,36,.06); }
|
||||
.dr-variant-label { font-size: 10px; font-weight: 500; color: #fbbf24; background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.25); padding: 1px 7px; border-radius: 10px; }
|
||||
.dr-tag.variant { background: rgba(251,191,36,.12); color: #fbbf24; border: 1px solid rgba(251,191,36,.25); }
|
||||
.dr-tag.bare { background: rgba(167,139,250,.12); color: #c4b5fd; border: 1px solid rgba(167,139,250,.25); }
|
||||
.dr-row.variant .dr-path { color: #c0c0e0; }
|
||||
+806
@@ -0,0 +1,806 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>rclone-jav options</title>
|
||||
<link rel="stylesheet" href="options.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="layout">
|
||||
|
||||
<!-- ============================== SIDEBAR ============================== -->
|
||||
<div class="side">
|
||||
<div class="brand">
|
||||
<strong>rclone-jav</strong>
|
||||
<span>Extension settings</span>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="gtitle">Scanning</div>
|
||||
<div class="item active" data-pane="triggers"><span class="icon">▶</span>Scan Behavior</div>
|
||||
<div class="item" data-pane="overlays"><span class="icon">▣</span>Overlays</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="gtitle">Library</div>
|
||||
<div class="item" data-pane="profiles"><span class="icon">☰</span>Profiles</div>
|
||||
<div class="item" data-pane="search"><span class="icon">⌕</span>Cache & Scans</div>
|
||||
<div class="item" data-pane="maintenance"><span class="icon">≡</span>Library Review</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="gtitle">Matching</div>
|
||||
<div class="item" data-pane="adapters"><span class="icon">⌖</span>Site Extraction</div>
|
||||
<div class="item" data-pane="normalizers"><span class="icon">⇄</span>ID Rules</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="gtitle">System</div>
|
||||
<div class="item" data-pane="paths"><span class="icon">⌘</span>Setup</div>
|
||||
<div class="item" data-pane="diagnostics"><span class="icon">✓</span>Diagnostics</div>
|
||||
<div class="item" data-pane="debug"><span class="icon">⚙</span>Debug Tools</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="gtitle">Danger</div>
|
||||
<div class="item danger" data-pane="deletion"><span class="icon">×</span>Deletion</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================== MAIN ============================== -->
|
||||
<div class="main">
|
||||
|
||||
<!-- TRIGGERS -->
|
||||
<div class="pane active" id="pane-triggers">
|
||||
<div class="pane-head">
|
||||
<h1>Scan Behavior</h1>
|
||||
<div class="pdesc">Choose when rclone-jav checks the current page.</div>
|
||||
</div>
|
||||
<div id="trigger-summary" class="section-note"></div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Scope</div>
|
||||
<div class="setting-list">
|
||||
<label><input type="checkbox" id="autoEveryPage"> Auto-check every page load
|
||||
<span class="sublabel">Runs on every site. Badge shows "?" when no JAV ID detected.</span></label>
|
||||
|
||||
<label><input type="checkbox" id="autoKnownSites"> Auto-check on known JAV sites
|
||||
<span class="sublabel">Only fires on host patterns listed below.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Known site patterns</div>
|
||||
<div class="help">Comma- or newline-separated. Bare domain matches site + any subdomain (<code>clearjav.com</code> covers <code>www.clearjav.com</code>).</div>
|
||||
<textarea id="knownSitePatterns" placeholder="clearjav.com javdb.com" style="resize:none;overflow:hidden;field-sizing:content;min-height:60px;"></textarea>
|
||||
<div class="chip-row" style="margin-top:8px;">
|
||||
<button id="add-current-site" class="chip-btn" type="button">Add Current Site</button>
|
||||
<button id="add-clearjav-site" class="chip-btn" type="button">Add ClearJAV</button>
|
||||
<span id="known-site-status" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Navigation types</div>
|
||||
<div class="setting-list">
|
||||
<label><input type="checkbox" id="autoPageLoad"> Full page loads
|
||||
<span class="sublabel">Runs when the browser reports the page has finished loading.</span></label>
|
||||
<label><input type="checkbox" id="autoSpaNavigation"> SPA and history URL changes
|
||||
<span class="sublabel">Runs when a site changes URL without a full reload, including browser back/forward on some sites.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Manual controls</div>
|
||||
<div class="setting-list">
|
||||
<label><input type="checkbox" id="toolbarClick"> Toolbar icon popup
|
||||
<span class="sublabel">Click the rclone-jav icon to run a check on the active tab.</span></label>
|
||||
<label><input type="checkbox" id="contextMenu"> Right-click context menu</label>
|
||||
<label><input type="checkbox" id="keyboardShortcut"> Keyboard shortcut (Alt+J)
|
||||
<span class="sublabel">Rebind at <code>brave://extensions/shortcuts</code>.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OVERLAYS -->
|
||||
<div class="pane" id="pane-overlays">
|
||||
<div class="pane-head">
|
||||
<h1>Overlays</h1>
|
||||
<div class="pdesc">In-page toast notifications after auto-trigger scans.</div>
|
||||
</div>
|
||||
<div id="overlay-summary" class="section-note"></div>
|
||||
|
||||
<!-- Horizontal tabs -->
|
||||
<div class="overlay-tabs">
|
||||
<button class="otab active" data-otab="match">✓ Match</button>
|
||||
<button class="otab" data-otab="nomatch">✗ No Match</button>
|
||||
</div>
|
||||
|
||||
<!-- MATCH tab -->
|
||||
<div class="otab-panel active" id="otab-match">
|
||||
<label style="margin-bottom:10px;display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="showOverlay"> Show overlay on match
|
||||
<span class="sublabel" style="margin:0;">Hover to pause countdown. Auto-check only.</span>
|
||||
</label>
|
||||
|
||||
<div class="fieldset">
|
||||
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:#888;margin-bottom:4px;">Position</div>
|
||||
<div class="radio-group" style="margin:0;">
|
||||
<label><input type="radio" name="overlayPosition" value="top-left"> Top-Left</label>
|
||||
<label><input type="radio" name="overlayPosition" value="top-right" checked> Top-Right</label>
|
||||
<label><input type="radio" name="overlayPosition" value="bottom-left"> Bottom-Left</label>
|
||||
<label><input type="radio" name="overlayPosition" value="bottom-right"> Bottom-Right</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#888;margin-bottom:4px;">Duration (sec)</div>
|
||||
<input type="number" id="overlayDuration" value="5" min="1" max="60" step="1" style="width:80px;">
|
||||
</div>
|
||||
</div>
|
||||
<label style="margin-top:10px;display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="overlayGlow"> Subtle glow
|
||||
<input type="color" id="overlayGlowColor" value="#6ec1ff" style="width:36px;height:24px;padding:0;cursor:pointer;" title="Glow color">
|
||||
</label>
|
||||
<div id="glow-detail" style="margin-top:10px;padding:10px;background:#101010;border:1px solid #232323;border-radius:4px;display:grid;grid-template-columns:90px 1fr 50px;gap:8px;align-items:center;font-size:11px;">
|
||||
<span style="color:#888;">Blur (soft)</span>
|
||||
<input type="range" id="overlayGlowBlur" min="0" max="60" step="1" value="10" style="width:100%;">
|
||||
<span id="overlayGlowBlurVal" style="color:#6ec1ff;text-align:right;font-family:Consolas,monospace;">10 px</span>
|
||||
<span style="color:#888;">Spread (size)</span>
|
||||
<input type="range" id="overlayGlowSpread" min="0" max="40" step="1" value="0" style="width:100%;">
|
||||
<span id="overlayGlowSpreadVal" style="color:#6ec1ff;text-align:right;font-family:Consolas,monospace;">0 px</span>
|
||||
<span style="color:#888;">Opacity</span>
|
||||
<input type="range" id="overlayGlowOpacity" min="0.05" max="1" step="0.05" value="0.35" style="width:100%;">
|
||||
<span id="overlayGlowOpacityVal" style="color:#6ec1ff;text-align:right;font-family:Consolas,monospace;">0.35</span>
|
||||
</div>
|
||||
<div style="margin-top:8px;display:flex;justify-content:flex-end;">
|
||||
<button id="overlay-reset" type="button" style="font-size:11px;padding:4px 10px;">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px;border-top:1px solid #222;padding-top:12px;">
|
||||
<div style="font-size:11px;color:#888;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>Preview <span id="overlay-preview-pos" style="color:#6ec1ff;">— top-right</span></span>
|
||||
<button id="overlay-preview-replay" type="button" style="font-size:11px;padding:2px 8px;">Replay</button>
|
||||
</div>
|
||||
<div id="overlay-preview-stage" style="background:#080808;border:1px dashed #2a2a2a;border-radius:4px;padding:18px;display:flex;justify-content:flex-end;align-items:flex-start;min-height:130px;position:relative;">
|
||||
<div id="overlay-preview" style="width:280px;background:#1a1a1a;color:#ddd;border:1px solid #2a2a2a;border-radius:6px;font-size:12px;overflow:hidden;box-shadow:0 6px 20px rgba(0,0,0,.55);">
|
||||
<div style="padding:6px 10px;background:#1e3a1e;color:#afa;font-weight:600;display:flex;align-items:center;gap:6px;font-size:12px;"><span>✓ IPZZ-860 — 1 hit(s)</span></div>
|
||||
<div style="padding:6px;background:#0d0d0d;">
|
||||
<div style="background:#161616;border:1px solid #2a2a2a;border-radius:4px;padding:8px 10px;">
|
||||
<div style="color:#fff;font-weight:600;font-size:12px;">IPZZ-860 [4K].mkv</div>
|
||||
<div style="color:#aaa;font-size:11px;font-family:Consolas,monospace;margin:3px 0;"><span style="color:#555;font-weight:600;">Path:</span> cq:JAV/ClearJAV/...</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;font-size:10px;">
|
||||
<span style="background:#2a2a1a;color:#ffcc44;padding:1px 6px;border-radius:10px;font-weight:600;letter-spacing:.3px;">TARGET</span>
|
||||
<span style="color:#6ec1ff;font-weight:600;">5.93 GiB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:3px;background:#222;"><div id="overlay-preview-bar" style="height:100%;background:linear-gradient(90deg,#6ec1ff,#66dd66);transform-origin:left;transform:scaleX(1);"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NO MATCH tab -->
|
||||
<div class="otab-panel" id="otab-nomatch">
|
||||
<label style="margin-bottom:10px;display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="noMatchOverlay"> Show overlay on no match
|
||||
<span class="sublabel" style="margin:0;">Off by default. Auto-check only.</span>
|
||||
</label>
|
||||
|
||||
<div class="fieldset" id="no-match-fieldset">
|
||||
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:#888;margin-bottom:4px;">Position</div>
|
||||
<div class="radio-group" id="noMatchPositionGroup" style="margin:0;">
|
||||
<label><input type="radio" name="noMatchPosition" value="top-left"> Top-Left</label>
|
||||
<label><input type="radio" name="noMatchPosition" value="top-right" checked> Top-Right</label>
|
||||
<label><input type="radio" name="noMatchPosition" value="bottom-left"> Bottom-Left</label>
|
||||
<label><input type="radio" name="noMatchPosition" value="bottom-right"> Bottom-Right</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#888;margin-bottom:4px;">Duration (sec)</div>
|
||||
<input type="number" id="noMatchDuration" value="5" min="1" max="60" step="1" style="width:80px;">
|
||||
</div>
|
||||
</div>
|
||||
<label style="margin-top:10px;display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="noMatchGlow"> Subtle glow
|
||||
<input type="color" id="noMatchGlowColor" value="#ff6666" style="width:36px;height:24px;padding:0;cursor:pointer;" title="Glow color">
|
||||
</label>
|
||||
<div style="margin-top:10px;padding:10px;background:#101010;border:1px solid #232323;border-radius:4px;display:grid;grid-template-columns:90px 1fr 50px;gap:8px;align-items:center;font-size:11px;">
|
||||
<span style="color:#888;">Blur (soft)</span>
|
||||
<input type="range" id="noMatchGlowBlur" min="0" max="60" step="1" value="10" style="width:100%;">
|
||||
<span id="noMatchGlowBlurVal" style="color:#ff6666;text-align:right;font-family:Consolas,monospace;">10 px</span>
|
||||
<span style="color:#888;">Spread (size)</span>
|
||||
<input type="range" id="noMatchGlowSpread" min="0" max="40" step="1" value="0" style="width:100%;">
|
||||
<span id="noMatchGlowSpreadVal" style="color:#ff6666;text-align:right;font-family:Consolas,monospace;">0 px</span>
|
||||
<span style="color:#888;">Opacity</span>
|
||||
<input type="range" id="noMatchGlowOpacity" min="0.05" max="1" step="0.05" value="0.35" style="width:100%;">
|
||||
<span id="noMatchGlowOpacityVal" style="color:#ff6666;text-align:right;font-family:Consolas,monospace;">0.35</span>
|
||||
</div>
|
||||
<div style="margin-top:8px;display:flex;justify-content:flex-end;">
|
||||
<button id="no-match-reset" type="button" style="font-size:11px;padding:4px 10px;">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px;border-top:1px solid #222;padding-top:12px;">
|
||||
<div style="font-size:11px;color:#888;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>Preview <span id="no-match-preview-pos" style="color:#ff6666;">— top-right</span></span>
|
||||
<button id="no-match-preview-replay" type="button" style="font-size:11px;padding:2px 8px;">Replay</button>
|
||||
</div>
|
||||
<div id="no-match-preview-stage" style="background:#080808;border:1px dashed #2a2a2a;border-radius:4px;padding:18px;display:flex;justify-content:flex-end;align-items:flex-start;min-height:90px;position:relative;">
|
||||
<div id="no-match-preview" style="width:280px;background:#1a1a1a;color:#ddd;border:1px solid #2a2a2a;border-radius:6px;font-size:12px;overflow:hidden;box-shadow:0 6px 20px rgba(0,0,0,.55);">
|
||||
<div style="padding:6px 10px;background:#3a1e1e;color:#faa;font-weight:600;font-size:12px;">✗ START-489 — NOT IN LIBRARY</div>
|
||||
<div style="padding:8px 12px;background:#0d0d0d;color:#aaa;font-size:11px;font-family:Consolas,monospace;word-break:break-all;"><span style="color:#555;font-weight:600;">Scanned:</span> cq:JAV</div>
|
||||
<div style="height:3px;background:#222;"><div id="no-match-preview-bar" style="height:100%;background:linear-gradient(90deg,#ff6666,#ff8c42);transform-origin:left;transform:scaleX(1);"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SITE ADAPTERS -->
|
||||
<div class="pane" id="pane-adapters">
|
||||
<div class="pane-head">
|
||||
<h1>Site Extraction</h1>
|
||||
<div class="pdesc">For sites where the JAV ID isn't in the page title, point at a DOM element. Selector runs <code>document.querySelector</code> and extracts <code>textContent</code>.</div>
|
||||
</div>
|
||||
<div id="adapter-summary" class="section-note"></div>
|
||||
|
||||
<table class="adapters" id="adapters">
|
||||
<thead><tr><th>Host pattern</th><th>CSS selector(s)</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div class="fieldset" style="margin-top:12px;">
|
||||
<div class="ftitle">Built-in presets</div>
|
||||
<div style="font-family:Consolas,monospace;font-size:11px;color:#888;line-height:1.5;word-break:break-all;">
|
||||
<div><span style="color:#aaa;">clearjav.com</span> → <code>div.meta-chip > h3.meta-chip__value</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;margin-top:10px;align-items:center;">
|
||||
<button id="add-adapter">+ Add Row</button>
|
||||
<button id="pick-element">Pick Element</button>
|
||||
<button id="test-active-page">Test Active Page</button>
|
||||
<button id="validate-adapters" type="button">Validate Rows</button>
|
||||
<span id="picker-status" style="color:#888;font-size:11px;"></span>
|
||||
</div>
|
||||
|
||||
<div id="adapter-test-result" class="fieldset" style="display:none;margin-top:14px;">
|
||||
<div class="ftitle">Active page extraction bench</div>
|
||||
<div id="adapter-test-output" style="font-family:Consolas,monospace;font-size:11px;color:#aaa;line-height:1.5;word-break:break-all;"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ID RULES -->
|
||||
<div class="pane" id="pane-normalizers">
|
||||
<div class="pane-head">
|
||||
<h1>ID Rules</h1>
|
||||
<div class="pdesc">Normalize odd IDs and teach rc-jav how multipart filename suffixes should stay distinct.</div>
|
||||
</div>
|
||||
<div id="normalizer-summary" class="section-note"></div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Built-in ID rules</div>
|
||||
<div style="font-size:11px;color:#888;line-height:1.5;">
|
||||
<div><code>FC2-?PPV-?(\d{4,})</code> → <code>FC2-PPV-$1</code>
|
||||
<span style="color:#666;"> — handles <code>FC2PPV4903171</code>, <code>FC2-PPV4903171</code>, <code>FC2-PPV-4903171</code></span></div>
|
||||
<div><code>FC2-(\d{4,})</code> → <code>FC2-PPV-$1</code>
|
||||
<span style="color:#666;"> — handles bare site labels like <code>FC2-1841460</code></span></div>
|
||||
<div><code>_A</code> / <code>_B</code> multipart suffixes are distinct parts in rc-jav duplicate/cache ID detection.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset" style="margin-top:14px;">
|
||||
<div class="ftitle">Test ID extraction</div>
|
||||
<div class="help">Paste a page title, DOM text, URL fragment, or filename. Uses the current custom normalizer rows below plus built-in extraction rules.</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 120px;gap:8px;align-items:center;">
|
||||
<input type="text" id="norm-test-in" placeholder="paste a sample page title or filename">
|
||||
<button id="norm-test-run">Test Text</button>
|
||||
</div>
|
||||
<div id="norm-test-out" style="margin-top:8px;font-family:Consolas,monospace;font-size:12px;color:#aaa;"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Custom ID normalizers</div>
|
||||
<table class="adapters" id="normalizers">
|
||||
<thead><tr><th>Pattern (regex)</th><th>Replacement</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div style="display:flex;gap:8px;margin-top:10px;align-items:center;">
|
||||
<button id="add-normalizer">+ Add Row</button>
|
||||
<button id="validate-normalizers" type="button">Validate Regex</button>
|
||||
<span id="normalizer-status" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Part detectors</div>
|
||||
<div class="help">Built-in and custom Python regexes run against the filename stem. Capture group 1 must be the part number or a single part letter. Built-ins are shown here so you can see what is already covered before adding a custom detector.</div>
|
||||
<div class="muted" style="margin:0 0 7px;">Built in</div>
|
||||
<div id="builtin-part-detectors" class="part-detector-list" style="margin-bottom:12px;"></div>
|
||||
<div class="muted" style="margin:0 0 7px;">Custom</div>
|
||||
<div id="part-detectors" class="part-detector-list"></div>
|
||||
<div class="button-row" style="margin-top:9px;">
|
||||
<button id="add-part-detector" type="button">+ Add Detector</button>
|
||||
</div>
|
||||
<div class="help">Host searches and cache rebuilds receive these saved rules. Direct CLI use can store the same list as <code>part_patterns</code> in <code>config.json</code> or pass repeatable <code>--part-pattern</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CACHE & SCANS -->
|
||||
<div class="pane" id="pane-search">
|
||||
<div class="pane-head">
|
||||
<h1>Cache & Scans</h1>
|
||||
<div class="pdesc">Choose lookup mode, inspect cache state, and run cache rebuild jobs.</div>
|
||||
</div>
|
||||
<div id="search-summary" class="section-note"></div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Lookup mode</div>
|
||||
<div class="help">Quick mode bypasses cache and uses <code>rclone --include</code> directly — fastest for single-ID lookups. ~1–2s per call. Disable to use the cache (faster on warm cache, requires <code>--scan</code> to keep fresh).</div>
|
||||
<label><input type="checkbox" id="quickMode"> Use --quick mode (live rclone lookup, no cache)</label>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Cache status</div>
|
||||
<div class="help">Inspect the current <code>cache.json</code> remotes, scan times, file counts, and skipped filename samples.</div>
|
||||
<div class="cache-freshness">
|
||||
<label>Stale after
|
||||
<input type="number" id="cacheStaleHours" value="24" min="1" max="8760" step="1">
|
||||
hours
|
||||
</label>
|
||||
<span class="sublabel">Used for cache warnings in the extension. Freshness does not change search results.</span>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="cache-status-run" type="button">Check Cache</button>
|
||||
</div>
|
||||
<div id="cache-status-results" class="mono-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Cache rebuild</div>
|
||||
<div class="help">Refresh the cache from the configured scan roots. Incremental rebuilds only ask rclone for recently changed files.</div>
|
||||
<div class="button-row">
|
||||
<select id="cache-rebuild-mode">
|
||||
<option value="">Full Rebuild</option>
|
||||
<option value="24h">Update last 24h</option>
|
||||
<option value="7d">Update last 7d</option>
|
||||
</select>
|
||||
<button id="cache-rebuild-run" type="button">Rebuild Cache</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Scan job</div>
|
||||
<div class="help">Follow the current or last cache scan job and clear old job history after it is no longer useful.</div>
|
||||
<div class="button-row">
|
||||
<button id="scan-job-clear" type="button" style="margin-left:auto;">Clear Job</button>
|
||||
</div>
|
||||
<div id="scan-job-results" class="mono-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIBRARY REVIEW -->
|
||||
<div class="pane" id="pane-maintenance">
|
||||
<div class="pane-head">
|
||||
<h1>Library Review</h1>
|
||||
<div class="pdesc">Review duplicate groups, check IDs in bulk, and fix non-canonical filenames in your library.</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Duplicate review</div>
|
||||
<div class="help">Review cached duplicate groups before acting. Keep/delete suggestions follow rc-jav rules; catalog rows remain reference-only.</div>
|
||||
<div class="button-row">
|
||||
<button id="dupe-review-run" type="button">Review Cached Duplicates</button>
|
||||
</div>
|
||||
<div id="dupe-review-results" class="mono-output">Review opens in a focused window so large duplicate groups do not stretch this page.</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Keep ranking</div>
|
||||
<div class="help">Controls how rc-jav picks the KEEP file when duplicates are found. Changes take effect immediately — no rescan required.</div>
|
||||
<div class="kr-panel">
|
||||
<div class="kr-info">
|
||||
<strong style="color:#93c5fd">How it works:</strong> Video files in <code>VIP folders</code> win first. <code>.ts</code> files rank below other video containers. After that, files within <code>size tolerance</code> of the largest are treated as equal-size, then ranked by
|
||||
<code>format preference</code>. Remaining ties broken by resolution tag, then filename length.
|
||||
With tolerance = 0 (default), size always decides first.
|
||||
</div>
|
||||
<div class="kr-row" style="align-items:flex-start">
|
||||
<span class="kr-label" style="padding-top:6px">VIP folders</span>
|
||||
<div>
|
||||
<div id="kr-vip-list" class="kr-fmt-list"></div>
|
||||
<div class="button-row" style="margin-top:6px;">
|
||||
<input id="kr-vip-add" type="text" placeholder="ClearJAV or cq:JAV/DirectRips" style="width:220px;">
|
||||
<button id="kr-vip-add-btn" type="button">Add VIP Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="kr-unit" style="padding-top:6px">drag to reorder · top wins first · ClearJAV is the default direct-rip VIP folder</span>
|
||||
</div>
|
||||
<div class="kr-row">
|
||||
<span class="kr-label">Size tolerance</span>
|
||||
<input id="kr-tolerance" class="kr-input" type="number" min="0" max="9999" step="1" value="0">
|
||||
<span class="kr-unit">MiB — files within this range of the largest are considered equal-size</span>
|
||||
</div>
|
||||
<div class="kr-row" style="align-items:flex-start">
|
||||
<span class="kr-label" style="padding-top:6px">Format preference</span>
|
||||
<div id="kr-fmt-list" class="kr-fmt-list">
|
||||
<!-- populated by JS -->
|
||||
</div>
|
||||
<span class="kr-unit" style="padding-top:6px">drag to reorder · top = highest priority</span>
|
||||
</div>
|
||||
<div class="kr-row">
|
||||
<span class="kr-label">Tie-breaks</span>
|
||||
<label class="kr-toggle"><input type="checkbox" id="kr-res-tag" checked> Prefer file with resolution tag <code>[1080p]</code></label>
|
||||
</div>
|
||||
<div class="kr-row">
|
||||
<span class="kr-label"></span>
|
||||
<label class="kr-toggle"><input type="checkbox" id="kr-longer-name" checked> Prefer longer filename (more metadata)</label>
|
||||
</div>
|
||||
<div class="kr-save-row">
|
||||
<button id="kr-save" type="button">Save Ranking</button>
|
||||
<span id="kr-save-status" class="kr-save-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Library issues</div>
|
||||
<div class="help">Find files with non-canonical names: bracket-wrapped IDs like <code>[REAL-779].mp4</code> and no-hyphen IDs like <code>MVSD312.avi</code>. Rename suggestions are computed from cache — no network required.</div>
|
||||
<div class="button-row">
|
||||
<button id="library-issues-run" type="button">Review Library Issues</button>
|
||||
</div>
|
||||
<div id="library-issues-results" class="mono-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Bulk ID check</div>
|
||||
<div class="help">Paste IDs separated by lines, commas, or spaces. Uses the current LIVE/CACHE mode and active library profile.</div>
|
||||
<textarea id="bulk-id-input" placeholder="BLK-474 FC2-4865786 PRTD-[027-030]"></textarea>
|
||||
<div class="button-row" style="margin-top:8px;">
|
||||
<button id="bulk-id-run" type="button">Check IDs</button>
|
||||
<button id="bulk-id-clear" type="button">Clear</button>
|
||||
</div>
|
||||
<div id="bulk-id-results" class="mono-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SETUP -->
|
||||
<div class="pane" id="pane-paths">
|
||||
<div class="pane-head">
|
||||
<h1>Setup</h1>
|
||||
<div class="pdesc">Script location and settings backup. Native host registration + extension ID live in Diagnostics.</div>
|
||||
</div>
|
||||
<div id="paths-summary" class="section-note"></div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">rc-jav script path</div>
|
||||
<div class="help">Folder containing <code>rc-jav.py</code>, or full file path. Leave blank for the host default.</div>
|
||||
<div class="compact-grid">
|
||||
<input type="text" id="rcjavPath" placeholder="D:\DEV\Project\rclone-jav">
|
||||
<button id="clear-rcjav-path" type="button">Use Default</button>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top:8px;">
|
||||
<button id="check-rcjav-path" type="button">Check Path</button>
|
||||
<span id="path-check-output" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="backup-summary" class="section-note"></div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Backup before moving</div>
|
||||
<div class="help">Extension settings and native messaging permissions are tied to the extension ID. Export before moving, reloading, or reinstalling. See <code>docs/EXTENSION_ID.md</code> for the stable-ID workflow.</div>
|
||||
<div class="button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<button id="import-settings">Import Settings</button>
|
||||
<input type="file" id="import-file" accept=".json,application/json" style="display:none;">
|
||||
<span id="backup-status" style="color:#888;font-size:11px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROFILES -->
|
||||
<div class="pane" id="pane-profiles">
|
||||
<div class="pane-head">
|
||||
<h1>Library Profiles</h1>
|
||||
<div class="pdesc">Named source and target remote sets for switching libraries from the popup.</div>
|
||||
</div>
|
||||
<div id="profiles-summary" class="section-note"></div>
|
||||
|
||||
<div id="profiles-list" style="margin-bottom:12px;"></div>
|
||||
<div class="button-row">
|
||||
<button id="add-profile" type="button">+ Add Profile</button>
|
||||
<button id="load-remotes" type="button">Load Remotes</button>
|
||||
<span id="profiles-status" class="muted"></span>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIAGNOSTICS -->
|
||||
<div class="pane" id="pane-diagnostics">
|
||||
<div class="pane-head">
|
||||
<h1>Diagnostics</h1>
|
||||
<div class="pdesc">Check setup health, runtime dependencies, native host registration, and recent search behavior.</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Setup health</div>
|
||||
<div class="help">Quick read on current lookup mode, pause state, active profile, cache freshness, and native host registration.</div>
|
||||
<div class="button-row">
|
||||
<button id="setup-health-run" type="button">Check Setup Health</button>
|
||||
</div>
|
||||
<div id="setup-health-results" class="mono-output"></div>
|
||||
</div>
|
||||
|
||||
<div id="native-repair-card" class="fieldset" style="display:none;">
|
||||
<div class="ftitle" id="native-repair-title">Native host setup</div>
|
||||
<div id="native-repair-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Runtime</div>
|
||||
<div class="help">Verifies Python, rclone, rc-jav, config, cache, and WinCatalog files.</div>
|
||||
<div class="button-row">
|
||||
<button id="run-diag">Run Diagnostics</button>
|
||||
<button id="run-all-diag" type="button">Run All</button>
|
||||
</div>
|
||||
<div id="diag-results" style="margin-top:14px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Native host registration</div>
|
||||
<div class="help">Checks the manifest, extension ID permission, and Windows registry entries used by Brave/Chrome native messaging.</div>
|
||||
<div class="compact-grid" style="margin-bottom:10px;align-items:center;">
|
||||
<div style="font-family:Consolas,monospace;font-size:12px;color:#bbb;"><span style="color:#888;">Extension ID: </span><span id="diag-extension-id">—</span></div>
|
||||
<button id="diag-copy-extension-id" type="button" style="padding:4px 10px;font-size:11px;">Copy ID</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="host-status-run" type="button">Check Host Registration</button>
|
||||
<button id="host-repair-run" type="button">Repair Registration</button>
|
||||
<button id="host-verify-run" type="button">Verify Registration</button>
|
||||
</div>
|
||||
<div id="host-status-results" style="margin-top:14px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEBUG TOOLS -->
|
||||
<div class="pane" id="pane-debug">
|
||||
<div class="pane-head">
|
||||
<h1>Debug Tools</h1>
|
||||
<div class="pdesc">Standalone diagnostic surfaces moved out of the main Diagnostics pane. Search troubleshooting bench + recent search/page-check activity log. Not part of the maintenance workflow.</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Search troubleshooting</div>
|
||||
<div class="help">Paste an ID, page title, DOM text, URL fragment, or filename. The bench extracts an ID, then compares LIVE and CACHE lookup paths for that ID under the active library profile.</div>
|
||||
<textarea id="search-bench-input" placeholder="BLK-474 or paste a page title / DOM text / URL containing an ID"></textarea>
|
||||
<div class="button-row" style="margin-top:8px;">
|
||||
<button id="search-bench-run" type="button">Test Search</button>
|
||||
<button id="search-bench-clear" type="button">Clear</button>
|
||||
</div>
|
||||
<div id="search-bench-results" class="mono-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset">
|
||||
<div class="ftitle">Recent activity</div>
|
||||
<div class="help">Short local history of page checks and popup searches. Stores the extracted ID, outcome, page context, mode, and timing summary. Search-trigger events only — deletion actions are not recorded here.</div>
|
||||
<div class="button-row">
|
||||
<button id="activity-refresh" type="button">Refresh Activity</button>
|
||||
<button id="activity-clear" type="button">Clear Activity</button>
|
||||
</div>
|
||||
<div class="activity-filters" id="activity-filters" aria-label="Recent activity filters">
|
||||
<button class="activity-filter active" type="button" data-activity-filter="all">All</button>
|
||||
<button class="activity-filter" type="button" data-activity-filter="hit">Match</button>
|
||||
<button class="activity-filter" type="button" data-activity-filter="miss">No Match</button>
|
||||
<button class="activity-filter" type="button" data-activity-filter="no_id">No ID</button>
|
||||
<button class="activity-filter" type="button" data-activity-filter="other">Other</button>
|
||||
</div>
|
||||
<div id="activity-results" class="mono-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DELETION -->
|
||||
<div class="pane" id="pane-deletion">
|
||||
<div class="pane-head">
|
||||
<h1 style="color:#faa;">Deletion</h1>
|
||||
<div class="pdesc">When enabled, the popup adds a delete button for matched files. Every delete still requires typing the exact filename to confirm.</div>
|
||||
</div>
|
||||
<div id="deletion-summary" class="section-note danger"></div>
|
||||
|
||||
<div class="fieldset danger-zone">
|
||||
<div class="ftitle">Enable</div>
|
||||
<label><input type="checkbox" id="enableDelete"> Enable file deletion from popup</label>
|
||||
</div>
|
||||
|
||||
<div class="fieldset danger-zone">
|
||||
<div class="ftitle">Mode</div>
|
||||
<div class="radio-group">
|
||||
<label class="selected" id="deleteModeTrashLbl"><input type="radio" name="deleteMode" id="deleteModeTrash" value="trash" checked> Move To Trash</label>
|
||||
<label id="deleteModePermLbl"><input type="radio" name="deleteMode" id="deleteModePerm" value="permanent"> Permanent Delete</label>
|
||||
</div>
|
||||
<div class="help">Trash mode runs <code>rclone moveto</code> into the trash dir + <code>YYYY-MM-DD/</code> subdir. Reversible until cleared. Permanent runs <code>rclone deletefile</code> — cloud Drives may still keep a 30-day recycle bin.</div>
|
||||
</div>
|
||||
|
||||
<div class="fieldset danger-zone">
|
||||
<div class="ftitle">Trash directory</div>
|
||||
<input type="text" id="trashDir" placeholder="cq:personal-files/.rclone-jav-trash">
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary save-btn">SAVE</button>
|
||||
<span class="saved">Saved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="skipped-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="skipped-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="skipped-modal-title">Skipped IDs</div>
|
||||
<div class="modal-subtitle" id="skipped-modal-subtitle"></div>
|
||||
</div>
|
||||
<button id="skipped-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="skipped-modal-summary" class="skip-summary"></div>
|
||||
<div id="skipped-modal-list"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="skipped-modal-copy" type="button">Copy List</button>
|
||||
<button id="skipped-modal-done" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profile-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="profile-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="profile-modal-title">Library Profile</div>
|
||||
<div class="modal-subtitle">Named source and target remotes for popup library switching.</div>
|
||||
</div>
|
||||
<button id="profile-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-field">
|
||||
<label for="profile-modal-name">Profile name</label>
|
||||
<input type="text" id="profile-modal-name" placeholder="ClearJAV">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>Source remotes</label>
|
||||
<div id="profile-modal-source" class="prof-remote-group"></div>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>Target remotes</label>
|
||||
<div id="profile-modal-target" class="prof-remote-group"></div>
|
||||
</div>
|
||||
<div id="profile-modal-status" class="muted"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="profile-modal-cancel" type="button">Cancel</button>
|
||||
<button id="profile-modal-save" class="primary" type="button">Save Profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dupe-review-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="dupe-review-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="dupe-review-modal-title">Duplicate Review</div>
|
||||
<div class="modal-subtitle">Cached duplicate groups and rc-jav keep/delete suggestions.</div>
|
||||
</div>
|
||||
<button id="dupe-review-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div id="dupe-review-modal-body" class="modal-body"></div>
|
||||
<div class="modal-actions">
|
||||
<span id="dupe-review-confirm-status" style="font-size:12px;color:#8888aa;flex:1;"></span>
|
||||
<button id="dupe-review-execute" type="button" disabled>Execute Deletions (0)</button>
|
||||
<button id="dupe-review-export" type="button" disabled>Export JSON</button>
|
||||
<button id="dupe-review-modal-done" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="library-issues-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="library-issues-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="library-issues-modal-title">Library Issues</div>
|
||||
<div class="modal-subtitle">Non-canonical filenames detected in cache. Rename to fix.</div>
|
||||
</div>
|
||||
<button id="library-issues-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div id="library-issues-modal-body" class="modal-body"></div>
|
||||
<div class="modal-actions">
|
||||
<span id="library-issues-rename-status" style="font-size:12px;color:#8888aa;flex:1;"></span>
|
||||
<button id="library-issues-rename-all" type="button" disabled>Rename All</button>
|
||||
<button id="library-issues-modal-done" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="import-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="import-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="import-modal-title">Import Settings</div>
|
||||
<div class="modal-subtitle" id="import-modal-subtitle"></div>
|
||||
</div>
|
||||
<button id="import-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div id="import-modal-body" class="modal-body"></div>
|
||||
<div class="modal-actions">
|
||||
<button id="import-modal-cancel" type="button">Cancel</button>
|
||||
<button id="import-modal-confirm" class="primary" type="button">Import Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="delete-enable-modal" class="modal-backdrop" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-enable-modal-title">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<div class="modal-title" id="delete-enable-modal-title">Enable Deletion</div>
|
||||
<div class="modal-subtitle">This exposes delete controls in the popup.</div>
|
||||
</div>
|
||||
<button id="delete-enable-modal-close" type="button" title="Close">x</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-confirm">Matched files can be moved to trash or permanently deleted from the popup after this setting is saved. Popup deletion still requires typing the exact filename before rc-jav acts.</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="delete-enable-modal-cancel" type="button">Cancel</button>
|
||||
<button id="delete-enable-modal-confirm" class="primary" type="button">Enable Deletion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+3133
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
body { font-family: -apple-system, Segoe UI, sans-serif; font-size: 13px; width: 420px; margin: 0; padding: 8px; background: #1a1a1a; color: #ddd; }
|
||||
#header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
#search-bar { display: flex; gap: 4px; margin-bottom: 6px; }
|
||||
#search-bar input { flex: 1; background: #0d0d0d; color: #ddd; border: 1px solid #444; border-radius: 3px; padding: 4px 8px; font-size: 12px; font-family: Consolas, monospace; box-sizing: border-box; min-width: 0; }
|
||||
#search-bar input:focus { outline: none; border-color: #6ec1ff; }
|
||||
#search-bar button { padding: 3px 8px; font-size: 12px; }
|
||||
#header strong { font-size: 14px; }
|
||||
#header-controls { display: flex; align-items: center; gap: 6px; }
|
||||
#mode-toggle { display: flex; border: 1px solid #444; border-radius: 3px; overflow: hidden; background: #181818; }
|
||||
#mode-toggle button {
|
||||
border: 0;
|
||||
border-right: 1px solid #444;
|
||||
border-radius: 0;
|
||||
background: #222;
|
||||
color: #888;
|
||||
padding: 3px 7px;
|
||||
width: 48px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-align: center;
|
||||
}
|
||||
#mode-toggle button:last-child { border-right: 0; }
|
||||
#mode-toggle button:hover { background: #2a2a2a; color: #ddd; }
|
||||
#mode-toggle button.active {
|
||||
background: #1a2a3a;
|
||||
color: #6ec1ff;
|
||||
}
|
||||
#mode-toggle button.active[data-mode="cache"] {
|
||||
background: #1a3a1a;
|
||||
color: #afa;
|
||||
}
|
||||
#pause-scan {
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
#pause-scan.paused {
|
||||
background: #3a2f13;
|
||||
border-color: #66551f;
|
||||
color: #ffdd77;
|
||||
}
|
||||
button { background: #333; color: #ddd; border: 1px solid #555; border-radius: 3px; padding: 3px 8px; cursor: pointer; font-size: 12px; }
|
||||
button:hover { background: #444; }
|
||||
button.scanning { background: #3a1a1a; border-color: #722; color: #faa; }
|
||||
button.scanning:hover { background: #4a1a1a; }
|
||||
#status { padding: 8px 10px; border-radius: 3px; background: #222; margin-bottom: 6px; font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
||||
#status.hit { background: #1e3a1e; color: #afa; }
|
||||
#status.miss { background: #3a1e1e; color: #faa; }
|
||||
#status.err { background: #3a3a1e; color: #ffa; }
|
||||
#status.loading { background: #1a2a3a; color: #6ec1ff; }
|
||||
#status .spinner {
|
||||
width: 14px; height: 14px; border-radius: 50%;
|
||||
border: 2px solid rgba(110, 193, 255, 0.25);
|
||||
border-top-color: #6ec1ff;
|
||||
animation: spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.9; } }
|
||||
#output .skeleton {
|
||||
background: #161616;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
#output .skeleton .bar { height: 10px; background: #2a2a2a; border-radius: 2px; margin: 4px 0; }
|
||||
#output .skeleton .bar.short { width: 50%; }
|
||||
#output .skeleton .bar.long { width: 90%; }
|
||||
#output .skeleton .bar.tiny { width: 30%; height: 8px; }
|
||||
#output { background: #0d0d0d; padding: 6px; border-radius: 3px; margin: 0; color: #bbb; }
|
||||
#output:empty { display: none; }
|
||||
#output .hit { background: #161616; border: 1px solid #2a2a2a; border-radius: 4px; padding: 10px; margin-bottom: 6px; }
|
||||
#output > *:last-child, #output .hit:last-child { margin-bottom: 0; }
|
||||
#output .hit > *:first-child { margin-top: 0; }
|
||||
#output .hit > *:last-child { margin-bottom: 0; }
|
||||
#output .hit .file { color: #fff; font-weight: 600; font-size: 13px; word-break: break-all; margin: 0 0 4px; }
|
||||
#output .hit .path { color: #aaa; font-size: 13px; font-family: Consolas, monospace; word-break: break-all; margin: 0 0 6px; line-height: 1.45; }
|
||||
#output .hit .plabel { color: #555; font-weight: 600; }
|
||||
#output .hit .meta { display: flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1; }
|
||||
#output .hit .size { color: #6ec1ff; font-weight: 600; }
|
||||
#output .hit .src { background: #2a2a1a; color: #ffcc44; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
|
||||
#output .hit .src.source { background: #1a3a1a; color: #66dd66; }
|
||||
#output .hit .src.catalog { background: #1a2a3a; color: #66bbff; }
|
||||
#output .hit .reason { background: #202a32; color: #9dccff; border: 1px solid #314453; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; }
|
||||
#output .empty { color: #888; font-style: italic; padding: 6px; }
|
||||
#output .no-match-detail { font-style: normal; line-height: 1.45; background:#17170f; border:1px solid #3d3a1f; border-radius:4px; margin-bottom:6px; color:#bcb58a; }
|
||||
#output .no-match-detail strong { display:block; color:#ffe08a; font-size:11px; margin-bottom:2px; }
|
||||
#output .err { color: #faa; font-family: Consolas, monospace; white-space: pre-wrap; padding: 4px; }
|
||||
#output .setup-guide { background:#17170f; border:1px solid #4a4420; border-radius:4px; color:#ffdd77; padding:9px; line-height:1.45; margin-bottom:6px; }
|
||||
#output .setup-guide strong { display:block; color:#fff0a5; margin-bottom:3px; }
|
||||
#output .setup-guide .detail { color:#bcb58a; font-size:11px; }
|
||||
#output .setup-guide .actions { display:flex; gap:6px; margin-top:7px; }
|
||||
#output .setup-guide button { font-size:11px; padding:3px 7px; }
|
||||
#output .timings {
|
||||
color: #777;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 10px;
|
||||
padding: 4px 2px 0;
|
||||
}
|
||||
.timing-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
padding: 7px 6px;
|
||||
background: #111;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.timing-metric {
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.timing-label {
|
||||
color: #666;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.timing-value {
|
||||
color: #aaa;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#filter-bar { display: flex; gap: 4px; margin-bottom: 4px; flex-wrap: wrap; }
|
||||
.filter-chip { background: #1e1e1e; border: 1px solid #444; color: #aaa; border-radius: 10px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
|
||||
.filter-chip:hover { background: #2a2a2a; }
|
||||
.filter-chip.active { background: #1a2a3a; border-color: #6ec1ff; color: #6ec1ff; }
|
||||
#history-bar { display: flex; align-items: flex-start; gap: 4px; margin-bottom: 6px; }
|
||||
#history-chips { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; min-width: 0; }
|
||||
.history-chip { background: #1a1a2a; border: 1px solid #334; color: #88aaff; border-radius: 10px; padding: 2px 8px; font-size: 11px; font-family: Consolas, monospace; cursor: pointer; white-space: nowrap; }
|
||||
.history-chip:hover { background: #22223a; border-color: #557; }
|
||||
#history-clear { background: none; border: none; color: #555; font-size: 13px; cursor: pointer; padding: 2px 4px; flex-shrink: 0; }
|
||||
#history-clear:hover { color: #aaa; }
|
||||
#cache-banner { background: #2a2500; border: 1px solid #554400; color: #ffcc44; font-size: 11px; padding: 4px 8px; border-radius: 3px; margin-bottom: 6px; line-height: 1.45; }
|
||||
#cache-banner.no-cache { background: #1a1a2a; border-color: #334; color: #88aaff; }
|
||||
#actions { margin-top: 8px; display: flex; gap: 6px; align-items: center; }
|
||||
#actions #ping { margin-left: auto; }
|
||||
#actions #delete-btn { margin-left: auto; }
|
||||
|
||||
/* Modal row — same look as result cards */
|
||||
#modal-list .hit {
|
||||
background: #161616;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, border-color 0.1s;
|
||||
}
|
||||
#modal-list .hit:last-child { margin-bottom: 0; }
|
||||
#modal-list .hit:hover { background: #1e1e1e; border-color: #444; }
|
||||
#modal-list .hit.selected { background: #3a1e1e; border-color: #722; }
|
||||
#modal-list .hit .file { color: #fff; font-weight: 600; font-size: 13px; word-break: break-all; margin: 0 0 4px; }
|
||||
#modal-list .hit .path { color: #aaa; font-size: 13px; font-family: Consolas, monospace; word-break: break-all; margin: 0 0 6px; line-height: 1.45; }
|
||||
#modal-list .hit .plabel { color: #555; font-weight: 600; }
|
||||
#modal-list .hit .meta { display: flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1; }
|
||||
#modal-list .hit .size { color: #6ec1ff; font-weight: 600; }
|
||||
#modal-list .hit .src { background: #2a2a1a; color: #ffcc44; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
|
||||
#modal-list .hit .src.source { background: #1a3a1a; color: #66dd66; }
|
||||
#modal-list .hit .src.catalog { background: #1a2a3a; color: #66bbff; }
|
||||
#status:not(.loading) + #output:empty + #actions,
|
||||
#status + #output:empty + #actions { margin-top: 0; }
|
||||
|
||||
/* Undo modal entries */
|
||||
#undo-list .undo-entry {
|
||||
background: #161616;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
#undo-list .undo-entry:last-child { margin-bottom: 0; }
|
||||
#undo-list .undo-entry .file { color: #fff; font-weight: 600; font-size: 13px; word-break: break-all; margin: 0 0 4px; }
|
||||
#undo-list .undo-entry .path { color: #aaa; font-size: 11px; font-family: Consolas, monospace; word-break: break-all; margin: 0 0 3px; line-height: 1.4; }
|
||||
#undo-list .undo-entry .plabel { color: #555; font-weight: 600; }
|
||||
#undo-list .undo-meta { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }
|
||||
#undo-list .undo-age { color: #777; font-size: 10px; }
|
||||
#undo-list .undo-row-btn { background: #1a2a1a; border: 1px solid #3a5a3a; color: #66dd66; border-radius: 3px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
|
||||
#undo-list .undo-row-btn:hover { background: #243a24; }
|
||||
#undo-list .undo-row-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<strong>rclone-jav</strong>
|
||||
<select id="profile-select" title="Active library profile" style="display:none;background:#222;color:#ddd;border:1px solid #444;border-radius:3px;font-size:12px;padding:2px 4px;max-width:140px;"></select>
|
||||
<div id="header-controls">
|
||||
<div id="mode-toggle" title="Search mode">
|
||||
<button id="mode-live" type="button" data-mode="live" title="LIVE: query rclone directly">LIVE</button>
|
||||
<button id="mode-cache" type="button" data-mode="cache" title="CACHE: use cache.json">CACHE</button>
|
||||
</div>
|
||||
<button id="pause-scan" type="button" title="Pause scanning">⏸</button>
|
||||
<button id="open-options" title="Options">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-bar">
|
||||
<input id="search-input" type="text" placeholder="search ID (e.g. SSIS-001, PRTD-[1-10])" spellcheck="false" autocomplete="off">
|
||||
<button id="search-go" title="Search">🔍</button>
|
||||
<button id="search-clear" title="Clear / back to current tab">×</button>
|
||||
</div>
|
||||
<div id="history-bar" style="display:none;">
|
||||
<div id="history-chips"></div>
|
||||
<button id="history-clear" title="Clear search history">✕</button>
|
||||
</div>
|
||||
<div id="cache-banner" style="display:none;"></div>
|
||||
<div id="status">checking…</div>
|
||||
<div id="filter-bar" style="display:none;"></div>
|
||||
<div id="output"></div>
|
||||
<div id="actions">
|
||||
<button id="recheck">Re-Scan</button>
|
||||
<button id="ping">Ping Host</button>
|
||||
<button id="undo-btn" title="Undo recent trash delete" style="display:none;">↶ Undo</button>
|
||||
<button id="delete-btn" style="display:none;background:#511;border-color:#722;color:#faa;font-weight:600;letter-spacing:0.5px;">DELETE</button>
|
||||
</div>
|
||||
|
||||
<!-- Delete modal -->
|
||||
<div id="modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:100;overflow-y:auto;">
|
||||
<div id="modal" style="background:#1a1a1a;border:1px solid #722;border-radius:6px;padding:10px;margin:8px;color:#ddd;font-size:12px;">
|
||||
<h3 style="margin:0 0 8px;color:#faa;font-size:13px;">Choose file to delete</h3>
|
||||
<div id="modal-list" style="padding:6px;background:#0d0d0d;border-radius:3px;"></div>
|
||||
<div id="modal-confirm" style="display:none;margin-top:8px;">
|
||||
<div id="modal-target" style="background:#0d0d0d;padding:4px 6px;font-size:10px;font-family:Consolas,monospace;border:1px solid #444;border-radius:3px;margin-bottom:6px;word-break:break-all;"></div>
|
||||
<div style="margin-bottom:4px;">Type the exact filename to confirm:</div>
|
||||
<div id="confirm-id" style="color:#faa;font-family:Consolas,monospace;word-break:break-all;font-size:10px;background:#0d0d0d;padding:4px 6px;border:1px solid #444;border-radius:3px;margin-bottom:4px;"></div>
|
||||
<input id="modal-confirm-input" style="width:100%;background:#0d0d0d;color:#ddd;border:1px solid #722;padding:6px;font-family:Consolas,monospace;font-size:11px;box-sizing:border-box;">
|
||||
<div id="modal-mode" style="font-size:10px;color:#aaa;margin-top:4px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:6px;margin-top:8px;">
|
||||
<button id="modal-cancel">Cancel</button>
|
||||
<button id="modal-delete" disabled style="background:#511;border-color:#722;color:#faa;font-weight:600;letter-spacing:0.5px;">DELETE</button>
|
||||
</div>
|
||||
<div id="modal-status" style="font-size:10px;margin-top:4px;color:#aaa;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Undo modal -->
|
||||
<div id="undo-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:100;overflow-y:auto;">
|
||||
<div id="undo-modal" style="background:#1a1a1a;border:1px solid #555;border-radius:6px;padding:10px;margin:8px;color:#ddd;font-size:12px;">
|
||||
<h3 style="margin:0 0 8px;color:#aaa;font-size:13px;">Recent trash deletes</h3>
|
||||
<div id="undo-list" style="padding:6px;background:#0d0d0d;border-radius:3px;min-height:30px;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
|
||||
<button id="undo-cancel">Close</button>
|
||||
</div>
|
||||
<div id="undo-status" style="font-size:10px;margin-top:4px;color:#aaa;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,659 @@
|
||||
const $status = document.getElementById("status");
|
||||
const $output = document.getElementById("output");
|
||||
const $deleteBtn = document.getElementById("delete-btn");
|
||||
const $undoBtn = document.getElementById("undo-btn");
|
||||
const $cacheBanner = document.getElementById("cache-banner");
|
||||
const $filterBar = document.getElementById("filter-bar");
|
||||
const $modeLive = document.getElementById("mode-live");
|
||||
const $modeCache = document.getElementById("mode-cache");
|
||||
const $pauseScan = document.getElementById("pause-scan");
|
||||
|
||||
let lastResult = null;
|
||||
let settings = null;
|
||||
let activeFilter = "all"; // "all" | "Source" | "Target" | "Catalog"
|
||||
|
||||
function syncModeToggle() {
|
||||
const live = settings && settings.quickMode !== false;
|
||||
$modeLive.classList.toggle("active", live);
|
||||
$modeCache.classList.toggle("active", !live);
|
||||
$modeLive.setAttribute("aria-pressed", live ? "true" : "false");
|
||||
$modeCache.setAttribute("aria-pressed", !live ? "true" : "false");
|
||||
}
|
||||
|
||||
function syncPauseButton() {
|
||||
const paused = !!(settings && settings.scanPaused);
|
||||
$pauseScan.classList.toggle("paused", paused);
|
||||
$pauseScan.textContent = paused ? "▶" : "⏸";
|
||||
$pauseScan.title = paused ? "Resume scanning" : "Pause scanning";
|
||||
$pauseScan.setAttribute("aria-pressed", paused ? "true" : "false");
|
||||
}
|
||||
|
||||
function renderPausedState() {
|
||||
setStatus("Scanning paused", "err");
|
||||
$output.innerHTML = "";
|
||||
const div = document.createElement("div");
|
||||
div.className = "empty";
|
||||
div.textContent = "Press ▶ to resume scans.";
|
||||
$output.appendChild(div);
|
||||
$deleteBtn.style.display = "none";
|
||||
$undoBtn.style.display = "none";
|
||||
}
|
||||
|
||||
async function setScanPaused(paused) {
|
||||
const s = await chrome.runtime.sendMessage({ type: "get-settings" }) || {};
|
||||
settings = Object.assign({}, s, { scanPaused: paused });
|
||||
await chrome.storage.sync.set({ settings });
|
||||
chrome.runtime.sendMessage({ type: "settings-changed" });
|
||||
syncPauseButton();
|
||||
if (paused) renderPausedState();
|
||||
else if (manualMode && $searchInput.value.trim()) runManualSearch();
|
||||
else runCheck(true);
|
||||
}
|
||||
|
||||
async function setSearchMode(mode) {
|
||||
const quickMode = mode === "live";
|
||||
if (settings && settings.quickMode === quickMode) return;
|
||||
const s = await chrome.runtime.sendMessage({ type: "get-settings" }) || {};
|
||||
settings = Object.assign({}, s, { quickMode });
|
||||
await chrome.storage.sync.set({ settings });
|
||||
chrome.runtime.sendMessage({ type: "settings-changed" });
|
||||
syncModeToggle();
|
||||
if (manualMode && $searchInput.value.trim()) runManualSearch();
|
||||
else runCheck(true);
|
||||
}
|
||||
|
||||
function setStatus(text, cls = "") {
|
||||
$status.className = cls;
|
||||
if (cls === "loading") {
|
||||
$status.innerHTML = `<span class="spinner"></span><span>${text}</span>`;
|
||||
} else {
|
||||
$status.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function showSkeleton(rows = 2) {
|
||||
$output.innerHTML = "";
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "skeleton";
|
||||
div.innerHTML = `
|
||||
<div class="bar long"></div>
|
||||
<div class="bar short"></div>
|
||||
<div class="bar tiny"></div>
|
||||
`;
|
||||
$output.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function fmtAge(ts) {
|
||||
if (!ts) return "";
|
||||
const s = Math.round((Date.now() - ts) / 1000);
|
||||
if (s < 60) return `${s}s ago`;
|
||||
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
||||
return `${Math.round(s / 3600)}h ago`;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
|
||||
function fmtMs(n) {
|
||||
return Number.isFinite(n) ? `${n}ms` : "?";
|
||||
}
|
||||
|
||||
function renderTimings(r) {
|
||||
const t = r && r.timings;
|
||||
if (!t) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "timing-strip";
|
||||
const engineLabel = Number.isFinite(t.host_cached_ms) ? "HOST" : "RC-JAV";
|
||||
const engineMs = Number.isFinite(t.host_cached_ms) ? t.host_cached_ms : t.host_rcjav_ms;
|
||||
const matchMs = t.cache_match_ms ?? t.match_ms;
|
||||
const metrics = [
|
||||
["TOTAL", fmtMs(t.total_ms)],
|
||||
[engineLabel, fmtMs(engineMs)],
|
||||
["EXTRACT", fmtMs(t.extract_ms)],
|
||||
["MATCH", fmtMs(matchMs)],
|
||||
];
|
||||
div.innerHTML = metrics.map(([label, value]) => `
|
||||
<div class="timing-metric">
|
||||
<div class="timing-label">${escapeHtml(label)}</div>
|
||||
<div class="timing-value">${escapeHtml(value)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
$output.appendChild(div);
|
||||
}
|
||||
|
||||
function renderSearchMode(_r) {
|
||||
// Mode chip removed — the LIVE/CACHE toggle in the header already shows the mode.
|
||||
}
|
||||
|
||||
function buildHitCard(h) {
|
||||
const filename = h.path.split("/").pop();
|
||||
const idx = h.full_path.lastIndexOf("/");
|
||||
const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path;
|
||||
const srcCls = h.source === "Source" ? "src source"
|
||||
: h.source === "Catalog" ? "src catalog" : "src";
|
||||
const confidence = h.match_confidence ? ` · ${h.match_confidence}` : "";
|
||||
const reason = h.match_reason
|
||||
? `<span class="reason" title="Matched ${escapeHtml(h.matched_query || h.jav_id || "")}${escapeHtml(confidence)}">${escapeHtml(h.match_reason)}</span>`
|
||||
: "";
|
||||
const div = document.createElement("div");
|
||||
div.className = "hit";
|
||||
div.innerHTML = `
|
||||
<div class="file">${escapeHtml(filename)}</div>
|
||||
<div class="path"><span class="plabel">Path:</span> ${escapeHtml(dir)}</div>
|
||||
<div class="meta"><span class="${srcCls}">${escapeHtml(h.source.toUpperCase())}</span><span class="size">${escapeHtml(h.size_human)}</span>${reason}</div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderFilterBar(structured) {
|
||||
$filterBar.innerHTML = "";
|
||||
if (!structured || structured.length < 2) { $filterBar.style.display = "none"; return; }
|
||||
const sources = [...new Set(structured.map((h) => h.source))];
|
||||
if (sources.length < 2) { $filterBar.style.display = "none"; return; }
|
||||
$filterBar.style.display = "";
|
||||
const filters = ["all", ...sources];
|
||||
for (const f of filters) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "filter-chip" + (activeFilter === f ? " active" : "");
|
||||
chip.textContent = f === "all" ? "All" : f;
|
||||
chip.addEventListener("click", () => {
|
||||
activeFilter = f;
|
||||
renderHits(lastResult && lastResult.structured);
|
||||
for (const c of $filterBar.children) c.classList.toggle("active", c.textContent === (f === "all" ? "All" : f));
|
||||
});
|
||||
$filterBar.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
function renderHits(structured) {
|
||||
// Clear existing hit cards but keep mode/timing lines
|
||||
for (const el of [...$output.children]) {
|
||||
if (el.classList.contains("hit") || el.classList.contains("empty")) el.remove();
|
||||
}
|
||||
const hits = structured || [];
|
||||
const filtered = activeFilter === "all" ? hits : hits.filter((h) => h.source === activeFilter);
|
||||
if (filtered.length) {
|
||||
const anchor = $output.querySelector(".timing-strip") || $output.querySelector(".timings");
|
||||
for (const h of filtered) {
|
||||
const card = buildHitCard(h);
|
||||
if (anchor) $output.insertBefore(card, anchor);
|
||||
else $output.appendChild(card);
|
||||
}
|
||||
} else {
|
||||
const div = document.createElement("div");
|
||||
div.className = "empty";
|
||||
div.textContent = filtered.length === 0 && activeFilter !== "all"
|
||||
? `No ${activeFilter} results`
|
||||
: "no matches";
|
||||
const anchor = $output.querySelector(".timing-strip") || $output.querySelector(".timings");
|
||||
if (anchor) $output.insertBefore(div, anchor);
|
||||
else $output.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function isNativeRegistrationIssue(result) {
|
||||
return result && (result.error_kind === "forbidden" || result.error_kind === "not_found");
|
||||
}
|
||||
|
||||
function renderNativeSetupGuide(result) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "setup-guide";
|
||||
const forbidden = result.error_kind === "forbidden";
|
||||
div.innerHTML = `
|
||||
<strong>${forbidden ? "Register this extension ID on this PC" : "Native host registration is missing on this PC"}</strong>
|
||||
<div class="detail">${forbidden
|
||||
? "Brave found the host, but this copy of the extension is not allowed to launch it yet."
|
||||
: "Brave cannot find the rclone-jav native host yet."}</div>
|
||||
<div class="actions">
|
||||
<button type="button" data-open-native-setup>Open Registration Guide</button>
|
||||
${result.extension_id ? `<button type="button" data-copy-extension-id="${escapeHtml(result.extension_id)}">Copy Extension ID</button>` : ""}
|
||||
</div>
|
||||
`;
|
||||
div.querySelector("[data-open-native-setup]").addEventListener("click", async () => {
|
||||
await chrome.storage.local.set({
|
||||
optionsActivePane: "diagnostics",
|
||||
pendingNativeSetupIssue: {
|
||||
error: result.error || result.reason || "native host registration failed",
|
||||
error_kind: result.error_kind,
|
||||
extension_id: result.extension_id || "",
|
||||
},
|
||||
});
|
||||
chrome.runtime.openOptionsPage();
|
||||
});
|
||||
const copy = div.querySelector("[data-copy-extension-id]");
|
||||
if (copy) {
|
||||
copy.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(copy.dataset.copyExtensionId || "");
|
||||
copy.textContent = "Copied";
|
||||
setTimeout(() => { copy.textContent = "Copy Extension ID"; }, 1200);
|
||||
});
|
||||
}
|
||||
$output.appendChild(div);
|
||||
}
|
||||
|
||||
function renderResult(r) {
|
||||
lastResult = r;
|
||||
activeFilter = "all";
|
||||
$output.innerHTML = "";
|
||||
$filterBar.style.display = "none";
|
||||
if (!r) { setStatus("no response", "err"); return; }
|
||||
const tag = r.cached ? ` [session ${fmtAge(r.ts)}]` : "";
|
||||
if (!r.ok) {
|
||||
setStatus("✗ " + (r.reason || "error") + tag, "err");
|
||||
$output.innerHTML = "";
|
||||
if (r.no_match_title || r.no_match_detail) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "empty no-match-detail";
|
||||
div.innerHTML = `<strong>${escapeHtml(r.no_match_title || "No result")}</strong>${r.no_match_detail ? `<div>${escapeHtml(r.no_match_detail)}</div>` : ""}`;
|
||||
$output.appendChild(div);
|
||||
}
|
||||
if (r.stderr || r.error) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "err";
|
||||
div.textContent = r.stderr || r.error;
|
||||
$output.appendChild(div);
|
||||
}
|
||||
if (isNativeRegistrationIssue(r)) renderNativeSetupGuide(r);
|
||||
$deleteBtn.style.display = "none";
|
||||
$undoBtn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const idShown = r.id || (r.structured && r.structured[0] && r.structured[0].jav_id) || "?";
|
||||
if (r.hits > 0) setStatus(`✓ ${idShown} — ${r.hits} hit(s)${tag}`, "hit");
|
||||
else setStatus(`✗ ${idShown} — NOT FOUND${tag}`, "miss");
|
||||
|
||||
// Render mode + timings first (renderHits inserts before these)
|
||||
renderSearchMode(r);
|
||||
renderTimings(r);
|
||||
// Render filter bar then hit cards
|
||||
renderFilterBar(r.structured);
|
||||
if (r.structured && r.structured.length) {
|
||||
renderHits(r.structured);
|
||||
} else {
|
||||
const div = document.createElement("div");
|
||||
div.className = "empty no-match-detail";
|
||||
div.innerHTML = `<strong>${escapeHtml(r.no_match_title || "No matches")}</strong>${r.no_match_detail ? `<div>${escapeHtml(r.no_match_detail)}</div>` : ""}`;
|
||||
$output.insertBefore(div, $output.firstChild);
|
||||
}
|
||||
|
||||
const canDelete = settings && settings.enableDelete && r.structured && r.structured.length > 1;
|
||||
$deleteBtn.style.display = canDelete ? "" : "none";
|
||||
$deleteBtn.textContent = "DELETE";
|
||||
$undoBtn.style.display = (settings && settings.enableDelete && settings.deleteMode === "trash") ? "" : "none";
|
||||
}
|
||||
|
||||
function runCheck(force = false) {
|
||||
if (settings && settings.scanPaused) { renderPausedState(); return; }
|
||||
setStatus("Scanning…", "loading");
|
||||
showSkeleton(2);
|
||||
$deleteBtn.style.display = "none";
|
||||
$undoBtn.style.display = "none";
|
||||
chrome.runtime.sendMessage({ type: "check-tab", force }, (r) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
setStatus("error: " + chrome.runtime.lastError.message, "err");
|
||||
$output.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
renderResult(r);
|
||||
});
|
||||
}
|
||||
|
||||
// ----- delete modal -----
|
||||
|
||||
const $overlay = document.getElementById("modal-overlay");
|
||||
const $list = document.getElementById("modal-list");
|
||||
const $confirmWrap = document.getElementById("modal-confirm");
|
||||
const $target = document.getElementById("modal-target");
|
||||
const $confirmId = document.getElementById("confirm-id");
|
||||
const $confirmInput = document.getElementById("modal-confirm-input");
|
||||
const $mode = document.getElementById("modal-mode");
|
||||
const $modalDelete = document.getElementById("modal-delete");
|
||||
const $modalStatus = document.getElementById("modal-status");
|
||||
let chosenHit = null;
|
||||
let expectedId = "";
|
||||
|
||||
function openDeleteModal() {
|
||||
if (!lastResult || !lastResult.structured) return;
|
||||
$list.innerHTML = "";
|
||||
chosenHit = null;
|
||||
$confirmWrap.style.display = "none";
|
||||
$modalDelete.disabled = true;
|
||||
$modalStatus.textContent = "";
|
||||
$confirmInput.value = "";
|
||||
|
||||
for (const h of lastResult.structured) {
|
||||
const filename = h.path.split("/").pop();
|
||||
const idx = h.full_path.lastIndexOf("/");
|
||||
const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path;
|
||||
const srcCls = h.source === "Source" ? "src source"
|
||||
: h.source === "Catalog" ? "src catalog" : "src";
|
||||
const row = document.createElement("div");
|
||||
row.className = "hit";
|
||||
row.innerHTML = `
|
||||
<div class="file">${escapeHtml(filename)}</div>
|
||||
<div class="path"><span class="plabel">Path:</span> ${escapeHtml(dir)}</div>
|
||||
<div class="meta"><span class="${srcCls}">${escapeHtml(h.source.toUpperCase())}</span><span class="size">${escapeHtml(h.size_human)}</span></div>
|
||||
`;
|
||||
row.addEventListener("click", () => selectHit(h, row));
|
||||
$list.appendChild(row);
|
||||
}
|
||||
$overlay.style.display = "block";
|
||||
}
|
||||
|
||||
function selectHit(h, row) {
|
||||
chosenHit = h;
|
||||
for (const el of $list.children) el.classList.remove("selected");
|
||||
row.classList.add("selected");
|
||||
$confirmWrap.style.display = "";
|
||||
$target.textContent = h.full_path;
|
||||
// Require typing the full filename (basename incl. extension) — strongest unique signal.
|
||||
expectedId = h.path.split("/").pop();
|
||||
$confirmId.textContent = expectedId;
|
||||
const modeText = settings.deleteMode === "permanent"
|
||||
? "PERMANENTLY DELETE"
|
||||
: `move to trash: ${settings.trashDir}`;
|
||||
$mode.innerHTML = `
|
||||
<div><strong>Mode:</strong> ${escapeHtml(modeText)}</div>
|
||||
<div><strong>Source:</strong> ${escapeHtml(h.source || "?")}</div>
|
||||
<div><strong>Remote/prefix:</strong> ${escapeHtml(h.remote || "?")}</div>
|
||||
<div><strong>Host safety:</strong> path must be inside configured rc-jav source/target or trash prefixes.</div>
|
||||
`;
|
||||
$confirmInput.value = "";
|
||||
$confirmInput.focus();
|
||||
$modalDelete.disabled = true;
|
||||
}
|
||||
|
||||
$confirmInput.addEventListener("input", () => {
|
||||
// Case-insensitive, exact filename match.
|
||||
$modalDelete.disabled = $confirmInput.value.trim().toLowerCase() !== expectedId.toLowerCase();
|
||||
});
|
||||
|
||||
document.getElementById("modal-cancel").addEventListener("click", () => {
|
||||
$overlay.style.display = "none";
|
||||
});
|
||||
|
||||
$modalDelete.addEventListener("click", () => {
|
||||
if (!chosenHit) return;
|
||||
$modalDelete.disabled = true;
|
||||
$modalStatus.textContent = "deleting…";
|
||||
chrome.runtime.sendMessage({ type: "delete-file", path: chosenHit.full_path }, (r) => {
|
||||
if (!r) { $modalStatus.textContent = "no response"; return; }
|
||||
if (!r.ok) { $modalStatus.textContent = "error: " + (r.error || r.stderr || "unknown"); return; }
|
||||
$modalStatus.textContent = (settings.deleteMode === "permanent" ? "deleted" : "moved to: " + r.dst);
|
||||
// Refresh underlying tab result
|
||||
setTimeout(() => {
|
||||
$overlay.style.display = "none";
|
||||
runCheck(true);
|
||||
}, 800);
|
||||
});
|
||||
});
|
||||
|
||||
// ----- search history -----
|
||||
|
||||
const HISTORY_KEY = "searchHistory";
|
||||
const HISTORY_MAX = 20;
|
||||
const $historyBar = document.getElementById("history-bar");
|
||||
const $historyChips = document.getElementById("history-chips");
|
||||
|
||||
async function loadHistory() {
|
||||
const got = await chrome.storage.local.get(HISTORY_KEY);
|
||||
return Array.isArray(got[HISTORY_KEY]) ? got[HISTORY_KEY] : [];
|
||||
}
|
||||
|
||||
async function pushHistory(id) {
|
||||
const list = await loadHistory();
|
||||
const filtered = list.filter((x) => x.toLowerCase() !== id.toLowerCase());
|
||||
const updated = [id, ...filtered].slice(0, HISTORY_MAX);
|
||||
await chrome.storage.local.set({ [HISTORY_KEY]: updated });
|
||||
renderHistory(updated);
|
||||
}
|
||||
|
||||
function renderHistory(list) {
|
||||
$historyChips.innerHTML = "";
|
||||
if (!list || !list.length) { $historyBar.style.display = "none"; return; }
|
||||
$historyBar.style.display = "";
|
||||
for (const id of list) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "history-chip";
|
||||
chip.textContent = id;
|
||||
chip.title = `Search ${id}`;
|
||||
chip.addEventListener("click", () => {
|
||||
$searchInput.value = id;
|
||||
runManualSearch();
|
||||
});
|
||||
$historyChips.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("history-clear").addEventListener("click", async () => {
|
||||
await chrome.storage.local.set({ [HISTORY_KEY]: [] });
|
||||
renderHistory([]);
|
||||
});
|
||||
|
||||
// ----- manual search -----
|
||||
|
||||
const $searchInput = document.getElementById("search-input");
|
||||
const $searchGo = document.getElementById("search-go");
|
||||
const $searchClear = document.getElementById("search-clear");
|
||||
let manualMode = false; // true while popup is showing manual-search results
|
||||
|
||||
function runManualSearch() {
|
||||
const raw = $searchInput.value.trim();
|
||||
if (!raw) return;
|
||||
if (settings && settings.scanPaused) { renderPausedState(); return; }
|
||||
manualMode = true;
|
||||
setStatus(`Searching ${raw}…`, "loading");
|
||||
showSkeleton(2);
|
||||
$deleteBtn.style.display = "none";
|
||||
$undoBtn.style.display = "none";
|
||||
const t0 = performance.now();
|
||||
chrome.runtime.sendMessage({
|
||||
type: "manual-query",
|
||||
action: "search",
|
||||
id: raw,
|
||||
quick: !!(settings && settings.quickMode),
|
||||
}, (r) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
setStatus("error: " + chrome.runtime.lastError.message, "err");
|
||||
$output.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
// The host search response uses the same shape as check-tab, so reuse the
|
||||
// renderer. Synthesize a `timings.total_ms` so the timing chip still works.
|
||||
const result = Object.assign({}, r, {
|
||||
id: raw,
|
||||
timings: Object.assign({ total_ms: Math.round(performance.now() - t0) }, r && r.timings ? r.timings : {}),
|
||||
});
|
||||
renderResult(result);
|
||||
if (result && result.ok) pushHistory(raw);
|
||||
});
|
||||
}
|
||||
|
||||
$searchGo.addEventListener("click", runManualSearch);
|
||||
$searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") { e.preventDefault(); runManualSearch(); }
|
||||
});
|
||||
$searchClear.addEventListener("click", () => {
|
||||
$searchInput.value = "";
|
||||
manualMode = false;
|
||||
runCheck(false);
|
||||
$searchInput.focus();
|
||||
});
|
||||
|
||||
// ----- undo modal -----
|
||||
|
||||
const $undoOverlay = document.getElementById("undo-overlay");
|
||||
const $undoList = document.getElementById("undo-list");
|
||||
const $undoStatus = document.getElementById("undo-status");
|
||||
|
||||
function openUndoModal() {
|
||||
$undoList.innerHTML = `<div class="empty">Loading…</div>`;
|
||||
$undoStatus.textContent = "";
|
||||
$undoOverlay.style.display = "block";
|
||||
chrome.runtime.sendMessage({ type: "recent-deletes", limit: 20 }, (r) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
$undoList.innerHTML = `<div class="err">${escapeHtml(chrome.runtime.lastError.message)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (!r || !r.ok) {
|
||||
$undoList.innerHTML = `<div class="err">${escapeHtml((r && r.error) || "unknown error")}</div>`;
|
||||
return;
|
||||
}
|
||||
const entries = (r.entries || []);
|
||||
if (!entries.length) {
|
||||
$undoList.innerHTML = `<div class="empty">No recent trash deletes found.</div>`;
|
||||
return;
|
||||
}
|
||||
$undoList.innerHTML = "";
|
||||
for (const e of entries) {
|
||||
const origName = (e.path || "").split("/").pop();
|
||||
const age = fmtAge(e.ts ? new Date(e.ts).getTime() : null);
|
||||
const row = document.createElement("div");
|
||||
row.className = "undo-entry";
|
||||
row.innerHTML = `
|
||||
<div class="file">${escapeHtml(origName)}</div>
|
||||
<div class="path"><span class="plabel">Orig:</span> ${escapeHtml(e.path || "?")}</div>
|
||||
<div class="path"><span class="plabel">Trash:</span> ${escapeHtml(e.dst || "?")}</div>
|
||||
<div class="undo-meta"><span class="undo-age">${escapeHtml(age)}</span><button class="undo-row-btn">↶ Restore</button></div>
|
||||
`;
|
||||
row.querySelector(".undo-row-btn").addEventListener("click", () => doUndo(e, row));
|
||||
$undoList.appendChild(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doUndo(e, row) {
|
||||
const btn = row.querySelector(".undo-row-btn");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "…";
|
||||
$undoStatus.textContent = "";
|
||||
chrome.runtime.sendMessage({ type: "undo-delete", dst: e.dst, path: e.path }, (r) => {
|
||||
if (!r) { btn.disabled = false; btn.textContent = "↶ Restore"; $undoStatus.textContent = "no response"; return; }
|
||||
if (!r.ok) { btn.disabled = false; btn.textContent = "↶ Restore"; $undoStatus.textContent = "error: " + (r.error || r.stderr || "unknown"); return; }
|
||||
btn.textContent = "✓ restored";
|
||||
row.style.opacity = "0.5";
|
||||
$undoStatus.textContent = "Restored to: " + (e.path || "?");
|
||||
// Refresh the main result after a short pause
|
||||
setTimeout(() => {
|
||||
$undoOverlay.style.display = "none";
|
||||
runCheck(true);
|
||||
}, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("undo-cancel").addEventListener("click", () => {
|
||||
$undoOverlay.style.display = "none";
|
||||
});
|
||||
$undoBtn.addEventListener("click", openUndoModal);
|
||||
|
||||
// ----- wiring -----
|
||||
|
||||
document.getElementById("recheck").addEventListener("click", async () => {
|
||||
if (settings && settings.scanPaused) {
|
||||
await setScanPaused(false);
|
||||
return;
|
||||
}
|
||||
if (manualMode && $searchInput.value.trim()) runManualSearch();
|
||||
else runCheck(true);
|
||||
});
|
||||
document.getElementById("open-options").addEventListener("click", () => chrome.runtime.openOptionsPage());
|
||||
$modeLive.addEventListener("click", () => setSearchMode("live"));
|
||||
$modeCache.addEventListener("click", () => setSearchMode("cache"));
|
||||
$pauseScan.addEventListener("click", () => setScanPaused(!(settings && settings.scanPaused)));
|
||||
$deleteBtn.addEventListener("click", openDeleteModal);
|
||||
document.getElementById("ping").addEventListener("click", () => {
|
||||
setStatus("pinging host…");
|
||||
$output.textContent = "";
|
||||
chrome.runtime.sendMessage({ type: "ping-host" }, (r) => {
|
||||
if (!r || !r.ok) { setStatus("host unreachable", "err"); $output.textContent = r?.error || ""; return; }
|
||||
setStatus("host ok: " + (r.version || "unknown"), "hit");
|
||||
const rows = [
|
||||
["Host version", r.version || "?"],
|
||||
["rc-jav path", r.rc_jav || "?"],
|
||||
["Script exists", r.rc_jav_exists ? "✓ yes" : "✗ not found"],
|
||||
["Path source", r.rc_jav_overridden ? "options override" : "built-in default"],
|
||||
["Python", r.python || "?"],
|
||||
];
|
||||
const div = document.createElement("div");
|
||||
div.style.cssText = "font-size:12px;line-height:1.7;";
|
||||
div.innerHTML = rows.map(([k, v]) =>
|
||||
`<div><span style="color:#888;min-width:110px;display:inline-block;">${k}</span> <span style="color:#ddd;">${v}</span></div>`
|
||||
).join("");
|
||||
$output.appendChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
function loadCacheBanner() {
|
||||
chrome.runtime.sendMessage({ type: "cache-status" }, (r) => {
|
||||
if (chrome.runtime.lastError || !r || !r.ok) return; // silent fail — banner optional
|
||||
if (!r.cache_exists) {
|
||||
$cacheBanner.className = "no-cache";
|
||||
$cacheBanner.textContent = "⚠ No cache — searches use quick (live) mode only. Run --scan to build.";
|
||||
$cacheBanner.style.display = "";
|
||||
return;
|
||||
}
|
||||
const staleRemotes = (r.remotes || []).filter((x) => x.stale);
|
||||
if (!staleRemotes.length) { $cacheBanner.style.display = "none"; return; }
|
||||
const oldest = staleRemotes.reduce((a, b) => (b.age_hours || 0) > (a.age_hours || 0) ? b : a, staleRemotes[0]);
|
||||
const ageH = oldest.age_hours != null ? Math.round(oldest.age_hours) : "?";
|
||||
$cacheBanner.className = "";
|
||||
$cacheBanner.textContent = `⚠ Cache is ${ageH}h old (${staleRemotes.length} remote${staleRemotes.length > 1 ? "s" : ""} stale). Consider running --scan.`;
|
||||
$cacheBanner.style.display = "";
|
||||
});
|
||||
}
|
||||
|
||||
const $profileSelect = document.getElementById("profile-select");
|
||||
$profileSelect.addEventListener("change", async () => {
|
||||
const newProfile = $profileSelect.value;
|
||||
// Save activeProfile to storage so background.js picks it up on next search.
|
||||
const s = await chrome.runtime.sendMessage({ type: "get-settings" });
|
||||
await chrome.storage.sync.set({ settings: Object.assign({}, s, { activeProfile: newProfile }) });
|
||||
settings = Object.assign({}, s, { activeProfile: newProfile });
|
||||
// Re-run current search with new profile
|
||||
if (manualMode && $searchInput.value.trim()) runManualSearch();
|
||||
else runCheck(true);
|
||||
});
|
||||
|
||||
function renderProfileSelector(s) {
|
||||
const profiles = s.profiles || [];
|
||||
if (!profiles.length) { $profileSelect.style.display = "none"; return; }
|
||||
$profileSelect.innerHTML = "";
|
||||
const def = document.createElement("option");
|
||||
def.value = "";
|
||||
def.textContent = "Default";
|
||||
$profileSelect.appendChild(def);
|
||||
for (const p of profiles) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.name;
|
||||
opt.textContent = p.name;
|
||||
$profileSelect.appendChild(opt);
|
||||
}
|
||||
$profileSelect.value = s.activeProfile || "";
|
||||
$profileSelect.style.display = "";
|
||||
}
|
||||
|
||||
(async () => {
|
||||
settings = await chrome.runtime.sendMessage({ type: "get-settings" }) || {};
|
||||
syncModeToggle();
|
||||
syncPauseButton();
|
||||
renderProfileSelector(settings);
|
||||
loadCacheBanner();
|
||||
renderHistory(await loadHistory());
|
||||
if (settings.scanPaused) {
|
||||
renderPausedState();
|
||||
return;
|
||||
}
|
||||
if (settings.triggers?.toolbarClick !== false) {
|
||||
runCheck(false);
|
||||
} else {
|
||||
setStatus("toolbar auto-check disabled");
|
||||
$output.innerHTML = "";
|
||||
const div = document.createElement("div");
|
||||
div.className = "empty";
|
||||
div.textContent = "Use Re-Scan to check this page.";
|
||||
$output.appendChild(div);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sim Dupe preview harness — extracted from popup.js during the console
|
||||
// consolidation refactor. The Sim Dupe button was removed from the live popup
|
||||
// (see mockups/console-consolidation-claude.html, step 1 of the sequence).
|
||||
// This file preserves the fake multi-hit payload so layout/design work on
|
||||
// renderResult / renderHits can still simulate a 3-hit result without
|
||||
// running a real search.
|
||||
//
|
||||
// To re-enable temporarily for layout testing:
|
||||
// 1. Re-add the button to popup.html under #actions:
|
||||
// <button id="sim-dupe" title="Simulate a multi-hit result (debug)">Sim Dupe</button>
|
||||
// 2. Copy the SIM_DUPE_PAYLOAD constant + the click handler below into popup.js.
|
||||
// 3. After layout work is done, remove both again so the production popup
|
||||
// stays free of debug surfaces.
|
||||
//
|
||||
// Or, for pure visual review without rebuilding the extension:
|
||||
// open popup.html in a browser, paste this payload into renderResult() via
|
||||
// the devtools console.
|
||||
|
||||
const SIM_DUPE_PAYLOAD = {
|
||||
ok: true,
|
||||
id: "PRTD-027",
|
||||
hits: 3,
|
||||
found: true,
|
||||
cached: false,
|
||||
structured: [
|
||||
{
|
||||
source: "Source", remote: "cq:JAV", jav_id: "PRTD-027",
|
||||
path: "ClearJAV/ichika-matsumoto/PRTD-027 - Ichika Matsumoto.mkv",
|
||||
full_path: "cq:JAV/ClearJAV/ichika-matsumoto/PRTD-027 - Ichika Matsumoto.mkv",
|
||||
size: 7455357566, size_human: "6.94 GiB", mod_time: "",
|
||||
},
|
||||
{
|
||||
source: "Target", remote: "cq:JAV", jav_id: "PRTD-027",
|
||||
path: "K-P/P/PRTD/PRTD-027 [1080p].mp4",
|
||||
full_path: "cq:JAV/K-P/P/PRTD/PRTD-027 [1080p].mp4",
|
||||
size: 7463966365, size_human: "6.95 GiB", mod_time: "",
|
||||
},
|
||||
{
|
||||
source: "Target", remote: "cq:JAV", jav_id: "PRTD-027",
|
||||
path: "TMP/PRTD-027.mp4",
|
||||
full_path: "cq:JAV/TMP/PRTD-027.mp4",
|
||||
size: 5047288128, size_human: "4.70 GiB", mod_time: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Optional: paste this back next to the other action-button listeners in popup.js
|
||||
// document.getElementById("sim-dupe").addEventListener("click", () => {
|
||||
// renderResult(SIM_DUPE_PAYLOAD);
|
||||
// });
|
||||
Reference in New Issue
Block a user