Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c429215626
|
||
|
|
9b7a6edd14
|
||
|
|
bbab33f55f
|
||
|
|
432ed37f45
|
||
|
|
a56742b421
|
||
|
|
9fd66f6668
|
||
|
|
51caae3b7c
|
||
|
|
80a54eed06
|
||
|
|
f626c5aa33
|
||
|
|
5df0bbc551
|
||
|
|
e61d56b59a
|
||
|
|
0bc5ca0315
|
||
|
|
2a56834ff3
|
||
|
|
cddf197a17
|
||
|
|
904e657bb9
|
||
|
|
e652850dfa
|
||
|
|
bfe20a84dd
|
||
|
|
46a8a62110
|
||
|
|
de582ae157
|
||
|
|
eb64de6ea3
|
||
|
|
eab6d10a19
|
||
|
|
76f6bd1a1e
|
||
|
|
29a9a1d07f
|
||
|
|
4d37b4f570
|
||
|
|
9d41e6ceeb
|
||
|
|
6d10a569d9
|
||
|
|
5f63718d62
|
||
|
|
eaffab1f27
|
||
|
|
d01c01a216
|
||
|
|
0b6bc5d0a0
|
||
|
|
3f5983685f
|
||
|
|
41d89e0993
|
||
|
|
a424dea5ca
|
||
|
|
ab7ae99807
|
||
|
|
5d47c511be
|
||
|
|
7713ba5bad | ||
|
|
ce0b28de4f
|
||
|
|
a2b041e225
|
||
|
|
4f87aadfd7
|
||
|
|
8456111bf0
|
||
|
|
6efe92a036
|
||
|
|
0cb13905ff
|
||
|
|
f32d1b3f71
|
||
|
|
e6c56bcecb
|
||
|
|
a7a0aafd68
|
||
|
|
3cf1a4acd1
|
||
|
|
a9956831c4
|
||
|
|
25d3acf576
|
||
|
|
7b8b4324af
|
||
|
|
8b9e4bea1d
|
||
|
|
8c94cc2088
|
||
|
|
19d3af02a2
|
||
|
|
306e0e3ea0
|
||
|
|
1536c13c3e
|
||
|
|
6bd319c8cc
|
||
|
|
3aee8c8f9a
|
||
|
|
939ee08466
|
||
|
|
5a175c3cf8
|
||
|
|
805e5a82e5
|
||
|
|
df34b1fee9
|
||
|
|
0741c6e535
|
||
|
|
fad0c49e65
|
||
|
|
66075fb6f3
|
||
|
|
bf4025dcb4
|
||
|
|
76a7b933bb
|
||
|
|
1cd533fc5c
|
||
|
|
8c5bd68d39
|
||
|
|
9c257af446
|
||
|
|
64a9b85587
|
||
|
|
edd997037a
|
||
|
|
f85a1f9f29
|
||
|
|
97366b76b6
|
||
|
|
8269875bb1
|
||
|
|
e34ec17f33
|
||
|
|
8d3905b654
|
||
|
|
7fd8a931d8
|
||
|
|
17319c1e25
|
||
|
|
841c1a246e
|
||
|
|
ed0f63e8bc
|
||
|
|
53f66f1eeb
|
||
|
|
f106ab490a
|
||
|
|
5a38121e09
|
||
|
|
36ed922b5c
|
||
|
|
3275d1f322
|
||
|
|
f6d706f096
|
||
|
|
04292a8018
|
@@ -10,6 +10,6 @@ liberapay: # Replace with a single Liberapay username
|
|||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
polar: # Replace with a single Polar username
|
polar: # Replace with a single Polar username
|
||||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
buy_me_a_coffee: treeman183
|
||||||
thanks_dev: # Replace with a single thanks.dev username
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
run: npm install -g web-ext
|
run: npm install -g web-ext
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: web-ext lint
|
run: web-ext lint --source-dir extension
|
||||||
|
|
||||||
# Beta tag (v*-beta) → Sign as unlisted on AMO, attach signed XPI to GitHub Prerelease
|
# Beta tag (v*-beta) → Sign as unlisted on AMO, attach signed XPI to GitHub Prerelease
|
||||||
# Firefox blocks all unsigned XPIs, even for self-hosted installs — unlisted signing is required
|
# Firefox blocks all unsigned XPIs, even for self-hosted installs — unlisted signing is required
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
web-ext sign \
|
web-ext sign \
|
||||||
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
||||||
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
|
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
|
||||||
--source-dir . \
|
--source-dir extension \
|
||||||
--artifacts-dir web-ext-artifacts \
|
--artifacts-dir web-ext-artifacts \
|
||||||
--channel unlisted
|
--channel unlisted
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Beta ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
files: ${{ steps.xpi.outputs.file }}
|
files: ${{ steps.xpi.outputs.file }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
body: |
|
body: |
|
||||||
@@ -61,11 +61,13 @@ jobs:
|
|||||||
|
|
||||||
# Stable tag (v* without -beta) → Sign & submit to public AMO listing
|
# Stable tag (v* without -beta) → Sign & submit to public AMO listing
|
||||||
- name: Sign & Submit to AMO (stable)
|
- name: Sign & Submit to AMO (stable)
|
||||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-beta')
|
if:
|
||||||
|
startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name,
|
||||||
|
'-beta')
|
||||||
run: |
|
run: |
|
||||||
web-ext sign \
|
web-ext sign \
|
||||||
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
||||||
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
|
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
|
||||||
--source-dir . \
|
--source-dir extension \
|
||||||
--artifacts-dir web-ext-artifacts \
|
--artifacts-dir web-ext-artifacts \
|
||||||
--channel listed
|
--channel listed
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"kiroAgent.configureMCP": "Disabled"
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Available for Firefox
|
# Available for Firefox
|
||||||
[](https://addons.mozilla.org//firefox/addon/speeder/)
|
|
||||||
|
|
||||||
|
[](https://addons.mozilla.org/firefox/addon/speeder/)
|
||||||
|
|
||||||
# The science of accelerated playback
|
## The science of accelerated playback
|
||||||
|
|
||||||
**TL;DR: faster playback translates to better engagement and retention.**
|
**TL;DR: faster playback translates to better engagement and retention.**
|
||||||
|
|
||||||
@@ -33,9 +33,11 @@ last point to listen to it a few more times.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/videospeed/) Extension*
|
## Using the extension
|
||||||
|
|
||||||
\*\* Once the extension is installed simply navigate to any page that offers
|
[](https://addons.mozilla.org/firefox/addon/speeder/)
|
||||||
|
|
||||||
|
Once the extension is installed simply navigate to any page that offers
|
||||||
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
|
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
|
||||||
see a speed indicator in top left corner. Hover over the indicator to reveal the
|
see a speed indicator in top left corner. Hover over the indicator to reveal the
|
||||||
controls to accelerate, slowdown, and quickly rewind or advance the video. Or,
|
controls to accelerate, slowdown, and quickly rewind or advance the video. Or,
|
||||||
@@ -56,7 +58,7 @@ shortcuts with different values, which will allow you to quickly toggle between
|
|||||||
your most commonly used speeds. To add a new shortcut, open extension settings
|
your most commonly used speeds. To add a new shortcut, open extension settings
|
||||||
and click "Add New".
|
and click "Add New".
|
||||||
|
|
||||||

|
<img width="1760" height="1330" alt="image" src="https://github.com/user-attachments/assets/32e814dd-93ea-4943-8ec9-3eca735447ac" />
|
||||||
|
|
||||||
Some sites may assign other functionality to one of the assigned shortcut keys —
|
Some sites may assign other functionality to one of the assigned shortcut keys —
|
||||||
these collisions are inevitable, unfortunately. As a workaround, the extension
|
these collisions are inevitable, unfortunately. As a workaround, the extension
|
||||||
@@ -65,21 +67,35 @@ listens both for lower and upper case values (i.e. you can use
|
|||||||
key. This is not a perfect solution, as some sites may listen to both, but works
|
key. This is not a perfect solution, as some sites may listen to both, but works
|
||||||
most of the time.
|
most of the time.
|
||||||
|
|
||||||
### FAQ
|
## Development
|
||||||
|
|
||||||
**The video controls are not showing up?** This extension is only compatible
|
The unpacked extension root is `extension/`. Load that directory in
|
||||||
|
`about:debugging`, and run extension tooling against it, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm test
|
||||||
|
npx --yes web-ext lint --source-dir extension
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### The video controls are not showing up?
|
||||||
|
|
||||||
|
This extension is only compatible
|
||||||
with HTML5 video. If you don't see the controls showing up, chances are you are
|
with HTML5 video. If you don't see the controls showing up, chances are you are
|
||||||
viewing a Flash video. If you want to confirm, try right-clicking on the video
|
viewing a Flash video. If you want to confirm, try right-clicking on the video
|
||||||
and inspect the menu: if it mentions flash, then that's the issue. That said,
|
and inspect the menu: if it mentions flash, then that's the issue. That said,
|
||||||
most sites will fallback to HTML5 if they detect that Flash is not available.
|
most sites will fallback to HTML5 if they detect that Flash is not available.
|
||||||
You can try manually disabling Flash from the browser.
|
You can try manually disabling Flash from the browser.
|
||||||
|
|
||||||
**What is this fork all about?** This is a fork of
|
### What is this fork all about?
|
||||||
[CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed)
|
|
||||||
|
This is a fork of
|
||||||
|
[CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed)
|
||||||
which is a fork of [Igrigorik's Video Speed Controller extension for Chromium](https://github.com/igrigorik/videospeed).
|
which is a fork of [Igrigorik's Video Speed Controller extension for Chromium](https://github.com/igrigorik/videospeed).
|
||||||
|
|
||||||
The goal of this fork is fix bugs in the upstream code as well as add new features.
|
The goal of this fork is fix bugs in the upstream code as well as add new features.
|
||||||
|
|
||||||
### License
|
## License
|
||||||
|
|
||||||
(GPLv3) - Copyright (c) 2025 Josh Patra
|
(GPLv3) - Copyright (c) 2025 Josh Patra
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
import glob
|
|
||||||
import fnmatch
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
SCRIPT_NAME = os.path.basename(__file__)
|
|
||||||
TARGET_FILE = "manifest.json"
|
|
||||||
DEFAULT_EXCLUDE_FILES = {".DS_Store"}
|
|
||||||
DEFAULT_EXCLUDE_DIRS = {"__pycache__", "temp"}
|
|
||||||
DEFAULT_EXCLUDE_PATTERNS = {"._*", "*.pyc"}
|
|
||||||
|
|
||||||
|
|
||||||
def should_exclude(rel_path, exclude_files, exclude_dirs):
|
|
||||||
rel_path = os.path.normpath(rel_path)
|
|
||||||
path_parts = rel_path.split(os.sep)
|
|
||||||
file_name = path_parts[-1]
|
|
||||||
|
|
||||||
if file_name in DEFAULT_EXCLUDE_FILES or rel_path in DEFAULT_EXCLUDE_FILES:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if any(part in DEFAULT_EXCLUDE_DIRS for part in path_parts):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if file_name in exclude_files or rel_path in exclude_files:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if any(part in exclude_dirs for part in path_parts):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if any(fnmatch.fnmatch(file_name, pattern) for pattern in DEFAULT_EXCLUDE_PATTERNS):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def zip_folder(output_name, folder, exclude_files, exclude_dirs):
|
|
||||||
with zipfile.ZipFile(output_name, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
||||||
for root, dirs, files in os.walk(folder):
|
|
||||||
dirs[:] = [
|
|
||||||
d
|
|
||||||
for d in dirs
|
|
||||||
if not should_exclude(
|
|
||||||
os.path.relpath(os.path.join(root, d), folder),
|
|
||||||
exclude_files,
|
|
||||||
exclude_dirs,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
for file in files:
|
|
||||||
rel_path = os.path.relpath(os.path.join(root, file), folder)
|
|
||||||
if should_exclude(rel_path, exclude_files, exclude_dirs):
|
|
||||||
continue
|
|
||||||
zipf.write(os.path.join(root, file), arcname=rel_path)
|
|
||||||
|
|
||||||
|
|
||||||
def update_version_line(file_path, new_version):
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
updated = False
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
match = re.match(r'\s*"version":\s*"([^"]+)"', line)
|
|
||||||
if match:
|
|
||||||
old_version = match.group(1)
|
|
||||||
lines[i] = re.sub(
|
|
||||||
r'"version":\s*".+?"', f'"version": "{new_version}"', line
|
|
||||||
)
|
|
||||||
updated = True
|
|
||||||
print(
|
|
||||||
f"🛠️ Changed version in {file_path} from {old_version} ➜ {new_version}"
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
else:
|
|
||||||
print(f"⚠️ No version line found in {file_path}.")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Step 0: Remove all existing .xpi files upfront
|
|
||||||
xpi_files = glob.glob("*.xpi")
|
|
||||||
for f in xpi_files:
|
|
||||||
try:
|
|
||||||
os.remove(f)
|
|
||||||
print(f"🗑️ Removed existing archive: {f}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Failed to remove {f}: {e}")
|
|
||||||
|
|
||||||
# Read current version from manifest.json
|
|
||||||
current_dir = os.getcwd()
|
|
||||||
manifest_path = os.path.join(current_dir, TARGET_FILE)
|
|
||||||
current_version = "unknown"
|
|
||||||
|
|
||||||
if os.path.exists(manifest_path):
|
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
match = re.match(r'\s*"version":\s*"([^"]+)"', line)
|
|
||||||
if match:
|
|
||||||
current_version = match.group(1)
|
|
||||||
break
|
|
||||||
|
|
||||||
print(f"📦 Current version: {current_version}")
|
|
||||||
base_version = input("Enter the new base version (e.g., 2.0.1): ").strip()
|
|
||||||
if not base_version:
|
|
||||||
print("❌ No version entered. Exiting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
github_version = f"{base_version}.0"
|
|
||||||
|
|
||||||
# Step 1: Update manifest.json on disk to base_version (for Firefox)
|
|
||||||
if os.path.exists(manifest_path):
|
|
||||||
update_version_line(manifest_path, base_version)
|
|
||||||
else:
|
|
||||||
print(f"❌ {TARGET_FILE} not found. Aborting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Step 2: Create videospeed-firefox.xpi (exclude script, .git, AND videospeed-firefox.xpi itself)
|
|
||||||
exclude_files = [SCRIPT_NAME, "videospeed-firefox.xpi"]
|
|
||||||
exclude_dirs = [".git"]
|
|
||||||
zip_folder("videospeed-firefox.xpi", current_dir, exclude_files, exclude_dirs)
|
|
||||||
print("✅ Created videospeed-firefox.xpi")
|
|
||||||
|
|
||||||
# Step 3: Re-scan for .xpi files after Firefox archive creation, exclude them for GitHub zip
|
|
||||||
current_xpi_files = set(glob.glob("*.xpi"))
|
|
||||||
exclude_temp_files = current_xpi_files.union({SCRIPT_NAME})
|
|
||||||
exclude_temp_dirs = set(exclude_dirs)
|
|
||||||
|
|
||||||
# Step 4: Create videospeed-github.xpi from temp folder with version bumped to .0
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
for item in os.listdir(current_dir):
|
|
||||||
if should_exclude(item, exclude_temp_files, exclude_temp_dirs):
|
|
||||||
continue
|
|
||||||
src = os.path.join(current_dir, item)
|
|
||||||
dst = os.path.join(temp_dir, item)
|
|
||||||
if os.path.isdir(src):
|
|
||||||
shutil.copytree(src, dst)
|
|
||||||
else:
|
|
||||||
shutil.copy2(src, dst)
|
|
||||||
|
|
||||||
temp_manifest = os.path.join(temp_dir, TARGET_FILE)
|
|
||||||
if os.path.exists(temp_manifest):
|
|
||||||
update_version_line(temp_manifest, github_version)
|
|
||||||
else:
|
|
||||||
print(f"⚠️ {TARGET_FILE} not found in temp folder.")
|
|
||||||
|
|
||||||
zip_folder(
|
|
||||||
"videospeed-github.xpi", temp_dir, exclude_files=[], exclude_dirs=[]
|
|
||||||
)
|
|
||||||
print("✅ Created videospeed-github.xpi")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg width="241" height="194" viewBox="0 0 241 194" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="kofiSymbolMask" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="0" width="242" height="194">
|
||||||
|
<path d="M240.469 0.958984H-0.00585938V193.918H240.469V0.958984Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#kofiSymbolMask)">
|
||||||
|
<path d="M96.1344 193.911C61.1312 193.911 32.6597 178.256 15.9721 149.829C1.19788 124.912 -0.00585938 97.9229 -0.00585938 67.7662C-0.00585938 49.8876 5.37293 34.3215 15.5413 22.7466C24.8861 12.1157 38.1271 5.22907 52.8317 3.35378C70.2858 1.14271 91.9848 0.958984 114.545 0.958984C151.259 0.958984 161.63 1.4088 176.075 2.85328C195.29 4.76026 211.458 11.932 222.824 23.5955C234.368 35.4428 240.469 51.2624 240.469 69.3627V72.9994C240.469 103.885 219.821 129.733 191.046 136.759C188.898 141.827 186.237 146.871 183.089 151.837L183.006 151.964C172.869 167.632 149.042 193.918 103.401 193.918H96.1281L96.1344 193.911Z" fill="white"/>
|
||||||
|
<path d="M174.568 17.9772C160.927 16.6151 151.38 16.1589 114.552 16.1589C90.908 16.1589 70.9008 16.387 54.7644 18.4334C33.3949 21.164 15.2058 37.5285 15.2058 67.7674C15.2058 98.0066 16.796 121.422 29.0741 142.107C42.9425 165.751 66.1302 178.707 96.1412 178.707H103.414C140.242 178.707 160.25 159.156 170.253 143.698C174.574 136.874 177.754 130.058 179.801 123.234C205.947 120.96 225.27 99.3624 225.27 72.9941V69.3577C225.27 40.9432 206.631 21.164 174.574 17.9772H174.568Z" fill="white"/>
|
||||||
|
<path d="M15.1975 67.7674C15.1975 37.5285 33.3866 21.164 54.7559 18.4334C70.8987 16.387 90.906 16.1589 114.544 16.1589C151.372 16.1589 160.919 16.6151 174.559 17.9772C206.617 21.1576 225.255 40.937 225.255 69.3577V72.9941C225.255 99.3687 205.932 120.966 179.786 123.234C177.74 130.058 174.559 136.874 170.238 143.698C160.235 159.156 140.228 178.707 103.4 178.707H96.1264C66.1155 178.707 42.9277 165.751 29.0595 142.107C16.7814 121.422 15.1912 98.4563 15.1912 67.7674" fill="#202020"/>
|
||||||
|
<path d="M32.2469 67.9899C32.2469 97.3168 34.0654 116.184 43.6127 133.689C54.5225 153.924 74.3018 161.653 96.8117 161.653H103.857C133.411 161.653 147.736 147.329 155.693 134.829C159.558 128.462 162.966 121.417 164.784 112.547L166.147 106.864H174.332C192.521 106.864 208.208 92.09 208.208 73.2166V69.8082C208.208 48.6669 195.024 37.5228 172.058 34.7987C159.102 33.6646 151.372 33.2084 114.538 33.2084C89.7602 33.2084 72.0272 33.4364 58.6152 35.4828C39.7483 38.2134 32.2407 48.8951 32.2407 67.9899" fill="white"/>
|
||||||
|
<path d="M166.158 83.6801C166.158 86.4107 168.204 88.4572 171.841 88.4572C183.435 88.4572 189.802 81.8619 189.802 70.9523C189.802 60.0427 183.435 53.2195 171.841 53.2195C168.204 53.2195 166.158 55.2657 166.158 57.9963V83.6866V83.6801Z" fill="#202020"/>
|
||||||
|
<path d="M54.5321 82.3198C54.5321 95.732 62.0332 107.326 71.5807 116.424C77.9478 122.562 87.9515 128.93 94.7685 133.022C96.8147 134.157 98.8611 134.841 101.136 134.841C103.866 134.841 106.134 134.157 107.959 133.022C114.782 128.93 124.779 122.562 130.919 116.424C140.694 107.332 148.195 95.7383 148.195 82.3198C148.195 67.7673 137.286 54.8115 121.599 54.8115C112.28 54.8115 105.912 59.5882 101.136 66.1772C96.8147 59.582 90.2259 54.8115 80.9001 54.8115C64.9855 54.8115 54.5256 67.7673 54.5256 82.3198" fill="#FF5A16"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,5 +1,5 @@
|
|||||||
chrome.runtime.onMessage.addListener(function (request) {
|
chrome.runtime.onMessage.addListener(function (request) {
|
||||||
if (request.action === "openOptions") {
|
if (request.action === "openOptions") {
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
|
chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* Runs via chrome.tabs.executeScript(allFrames) in the same isolated world as inject.js */
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
if (typeof getPrimaryVideoElement !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var v = getPrimaryVideoElement();
|
||||||
|
if (!v) return null;
|
||||||
|
return {
|
||||||
|
speed: v.playbackRate,
|
||||||
|
preferred: !v.paused
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -6,16 +6,12 @@
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
z-index: 2147483646 !important;
|
/* Keep the interactive controller above player-owned click/pause panes. */
|
||||||
|
z-index: 2147483647 !important;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Use minimal z-index for non-YouTube sites to avoid overlapping modals */
|
|
||||||
.vsc-controller.vsc-non-youtube {
|
|
||||||
z-index: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vsc-nosource {
|
.vsc-nosource {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
|
||||||
|
|
||||||
var isUserSeek = false; // Track if seek was user-initiated
|
var isUserSeek = false; // Track if seek was user-initiated
|
||||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||||
|
var speederShared =
|
||||||
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
|
var controllerUtils = speederShared.controllerUtils || {};
|
||||||
|
var keyBindingUtils = speederShared.keyBindings || {};
|
||||||
|
var siteRuleUtils = speederShared.siteRules || {};
|
||||||
|
|
||||||
|
function getPrimaryVideoElement() {
|
||||||
|
if (!tc.mediaElements || tc.mediaElements.length === 0) return null;
|
||||||
|
for (var i = 0; i < tc.mediaElements.length; i++) {
|
||||||
|
var el = tc.mediaElements[i];
|
||||||
|
if (el && !el.paused) return el;
|
||||||
|
}
|
||||||
|
return tc.mediaElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
var tc = {
|
var tc = {
|
||||||
settings: {
|
settings: {
|
||||||
lastSpeed: 1.0,
|
lastSpeed: 1.0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
speeds: {},
|
speeds: {},
|
||||||
displayKeyCode: 86,
|
|
||||||
rememberSpeed: false,
|
rememberSpeed: false,
|
||||||
forceLastSavedSpeed: false,
|
forceLastSavedSpeed: false,
|
||||||
audioBoolean: false,
|
audioBoolean: false,
|
||||||
@@ -27,9 +38,11 @@ var tc = {
|
|||||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||||
defaultLogLevel: 3,
|
defaultLogLevel: 3,
|
||||||
logLevel: 3,
|
logLevel: 3,
|
||||||
enableSubtitleNudge: true, // Enabled by default, but only activates on YouTube
|
enableSubtitleNudge: false,
|
||||||
|
subtitleNudgeEnabledByDefault: true,
|
||||||
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
|
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
|
||||||
subtitleNudgeAmount: 0.001
|
subtitleNudgeAmount: 0.001,
|
||||||
|
customButtonIcons: {}
|
||||||
},
|
},
|
||||||
mediaElements: [],
|
mediaElements: [],
|
||||||
isNudging: false,
|
isNudging: false,
|
||||||
@@ -46,23 +59,28 @@ var YT_NATIVE_MIN = 0.25;
|
|||||||
var YT_NATIVE_MAX = 2.0;
|
var YT_NATIVE_MAX = 2.0;
|
||||||
var YT_NATIVE_STEP = 0.05;
|
var YT_NATIVE_STEP = 0.05;
|
||||||
var vscObservedRoots = new WeakSet();
|
var vscObservedRoots = new WeakSet();
|
||||||
|
var vscConnectedScannedRoots = new WeakSet();
|
||||||
|
var vscInitializedDocuments = new Set();
|
||||||
var requestIdle =
|
var requestIdle =
|
||||||
typeof window.requestIdleCallback === "function"
|
typeof window.requestIdleCallback === "function"
|
||||||
? window.requestIdleCallback.bind(window)
|
? window.requestIdleCallback.bind(window)
|
||||||
: function (callback, options) {
|
: function(callback, options) {
|
||||||
return setTimeout(callback, (options && options.timeout) || 1);
|
return setTimeout(callback, (options && options.timeout) || 1);
|
||||||
};
|
};
|
||||||
var controllerLocations = [
|
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||||
"top-left",
|
? controllerUtils.controllerLocations.slice()
|
||||||
"top-center",
|
: [
|
||||||
"top-right",
|
"top-left",
|
||||||
"middle-right",
|
"top-center",
|
||||||
"bottom-right",
|
"top-right",
|
||||||
"bottom-center",
|
"middle-right",
|
||||||
"bottom-left",
|
"bottom-right",
|
||||||
"middle-left"
|
"bottom-center",
|
||||||
];
|
"bottom-left",
|
||||||
var defaultControllerLocation = controllerLocations[0];
|
"middle-left"
|
||||||
|
];
|
||||||
|
var defaultControllerLocation =
|
||||||
|
controllerUtils.defaultControllerLocation || controllerLocations[0];
|
||||||
var controllerLocationStyles = {
|
var controllerLocationStyles = {
|
||||||
"top-left": {
|
"top-left": {
|
||||||
top: "10px",
|
top: "10px",
|
||||||
@@ -106,75 +124,29 @@ var controllerLocationStyles = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* `label` fallback only when ui-icons has no path for the action. */
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { label: "\u00AB", className: "rw" },
|
rewind: { label: "", className: "rw" },
|
||||||
slower: { label: "\u2212", className: "" },
|
slower: { label: "", className: "" },
|
||||||
faster: { label: "+", className: "" },
|
faster: { label: "", className: "" },
|
||||||
advance: { label: "\u00BB", className: "rw" },
|
advance: { label: "", className: "rw" },
|
||||||
display: { label: "\u00D7", className: "hideButton" },
|
display: { label: "", className: "hideButton" },
|
||||||
reset: { label: "\u21BA", className: "" },
|
reset: { label: "\u21BB", className: "" },
|
||||||
fast: { label: "\u2605", className: "" },
|
fast: { label: "", className: "" },
|
||||||
settings: { label: "\u2699", className: "" },
|
nudge: { label: "", className: "" },
|
||||||
pause: { label: "\u23EF", className: "" },
|
pause: { label: "", className: "" },
|
||||||
muted: { label: "M", className: "" },
|
muted: { label: "", className: "" },
|
||||||
mark: { label: "\u2691", className: "" },
|
louder: { label: "", className: "" },
|
||||||
jump: { label: "\u21E5", className: "" }
|
softer: { label: "", className: "" },
|
||||||
|
mark: { label: "", className: "" },
|
||||||
|
jump: { label: "", className: "" },
|
||||||
|
settings: { label: "", className: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var keyCodeToEventKey = {
|
function createDefaultBinding(action, code, value) {
|
||||||
32: " ",
|
|
||||||
37: "ArrowLeft",
|
|
||||||
38: "ArrowUp",
|
|
||||||
39: "ArrowRight",
|
|
||||||
40: "ArrowDown",
|
|
||||||
96: "0",
|
|
||||||
97: "1",
|
|
||||||
98: "2",
|
|
||||||
99: "3",
|
|
||||||
100: "4",
|
|
||||||
101: "5",
|
|
||||||
102: "6",
|
|
||||||
103: "7",
|
|
||||||
104: "8",
|
|
||||||
105: "9",
|
|
||||||
106: "*",
|
|
||||||
107: "+",
|
|
||||||
109: "-",
|
|
||||||
110: ".",
|
|
||||||
111: "/",
|
|
||||||
112: "F1",
|
|
||||||
113: "F2",
|
|
||||||
114: "F3",
|
|
||||||
115: "F4",
|
|
||||||
116: "F5",
|
|
||||||
117: "F6",
|
|
||||||
118: "F7",
|
|
||||||
119: "F8",
|
|
||||||
120: "F9",
|
|
||||||
121: "F10",
|
|
||||||
122: "F11",
|
|
||||||
123: "F12",
|
|
||||||
186: ";",
|
|
||||||
188: "<",
|
|
||||||
189: "-",
|
|
||||||
187: "+",
|
|
||||||
190: ">",
|
|
||||||
191: "/",
|
|
||||||
192: "~",
|
|
||||||
219: "[",
|
|
||||||
220: "\\",
|
|
||||||
221: "]",
|
|
||||||
222: "'",
|
|
||||||
59: ";",
|
|
||||||
61: "+",
|
|
||||||
173: "-"
|
|
||||||
};
|
|
||||||
|
|
||||||
function createDefaultBinding(action, key, keyCode, value) {
|
|
||||||
return {
|
return {
|
||||||
action: action,
|
action: action,
|
||||||
key: key,
|
code: code,
|
||||||
keyCode: keyCode,
|
|
||||||
value: value,
|
value: value,
|
||||||
force: false,
|
force: false,
|
||||||
predefined: true
|
predefined: true
|
||||||
@@ -185,89 +157,73 @@ function defaultKeyBindings(storage) {
|
|||||||
return [
|
return [
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"slower",
|
"slower",
|
||||||
"S",
|
"KeyS",
|
||||||
Number(storage.slowerKeyCode) || 83,
|
|
||||||
Number(storage.speedStep) || 0.1
|
Number(storage.speedStep) || 0.1
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"faster",
|
"faster",
|
||||||
"D",
|
"KeyD",
|
||||||
Number(storage.fasterKeyCode) || 68,
|
|
||||||
Number(storage.speedStep) || 0.1
|
Number(storage.speedStep) || 0.1
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"rewind",
|
"rewind",
|
||||||
"Z",
|
"KeyZ",
|
||||||
Number(storage.rewindKeyCode) || 90,
|
|
||||||
Number(storage.rewindTime) || 10
|
Number(storage.rewindTime) || 10
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"advance",
|
"advance",
|
||||||
"X",
|
"KeyX",
|
||||||
Number(storage.advanceKeyCode) || 88,
|
|
||||||
Number(storage.advanceTime) || 10
|
Number(storage.advanceTime) || 10
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"reset",
|
"reset",
|
||||||
"R",
|
"KeyR",
|
||||||
Number(storage.resetKeyCode) || 82,
|
|
||||||
1.0
|
1.0
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"fast",
|
"fast",
|
||||||
"G",
|
"KeyG",
|
||||||
Number(storage.fastKeyCode) || 71,
|
|
||||||
Number(storage.fastSpeed) || 1.8
|
Number(storage.fastSpeed) || 1.8
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"move",
|
"move",
|
||||||
"P",
|
"KeyP",
|
||||||
80,
|
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"toggleSubtitleNudge",
|
"toggleSubtitleNudge",
|
||||||
"N",
|
"KeyN",
|
||||||
78,
|
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDefaultKeyBinding(action, key, keyCode, value) {
|
function ensureDefaultKeyBinding(action, code, value) {
|
||||||
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
|
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.settings.keyBindings.push(
|
tc.settings.keyBindings.push(
|
||||||
createDefaultBinding(action, key, keyCode, value)
|
createDefaultBinding(action, code, value)
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLegacyKeyCode(binding) {
|
function getLegacyKeyCode(binding) {
|
||||||
if (!binding) return null;
|
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
|
||||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
|
||||||
return binding.key;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeControllerLocation(location) {
|
function normalizeControllerLocation(location) {
|
||||||
if (controllerLocations.includes(location)) return location;
|
return controllerUtils.normalizeControllerLocation(
|
||||||
return defaultControllerLocation;
|
location,
|
||||||
|
defaultControllerLocation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var CONTROLLER_MARGIN_MAX_PX = 200;
|
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||||
|
|
||||||
function normalizeControllerMarginPx(value, fallback) {
|
function normalizeControllerMarginPx(value, fallback) {
|
||||||
var n = Number(value);
|
return controllerUtils.clampControllerMarginPx(value, fallback);
|
||||||
if (!Number.isFinite(n)) return fallback;
|
|
||||||
return Math.min(
|
|
||||||
CONTROLLER_MARGIN_MAX_PX,
|
|
||||||
Math.max(0, Math.round(n))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyControllerMargins(controller) {
|
function applyControllerMargins(controller) {
|
||||||
@@ -306,9 +262,7 @@ function applyControllerMargins(controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNextControllerLocation(location) {
|
function getNextControllerLocation(location) {
|
||||||
var normalizedLocation = normalizeControllerLocation(location);
|
return controllerUtils.getNextControllerLocation(location);
|
||||||
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
|
||||||
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getControllerElement(videoOrController) {
|
function getControllerElement(videoOrController) {
|
||||||
@@ -433,12 +387,13 @@ function captureSiteRuleBase() {
|
|||||||
controllerMarginTop: tc.settings.controllerMarginTop,
|
controllerMarginTop: tc.settings.controllerMarginTop,
|
||||||
controllerMarginBottom: tc.settings.controllerMarginBottom,
|
controllerMarginBottom: tc.settings.controllerMarginBottom,
|
||||||
enableSubtitleNudge: tc.settings.enableSubtitleNudge,
|
enableSubtitleNudge: tc.settings.enableSubtitleNudge,
|
||||||
|
subtitleNudgeEnabledByDefault: tc.settings.subtitleNudgeEnabledByDefault,
|
||||||
subtitleNudgeInterval: tc.settings.subtitleNudgeInterval,
|
subtitleNudgeInterval: tc.settings.subtitleNudgeInterval,
|
||||||
controllerButtons: Array.isArray(tc.settings.controllerButtons)
|
controllerButtons: Array.isArray(tc.settings.controllerButtons)
|
||||||
? tc.settings.controllerButtons.slice()
|
? tc.settings.controllerButtons.slice()
|
||||||
: tc.settings.controllerButtons,
|
: tc.settings.controllerButtons,
|
||||||
keyBindings: Array.isArray(tc.settings.keyBindings)
|
keyBindings: Array.isArray(tc.settings.keyBindings)
|
||||||
? tc.settings.keyBindings.map(function (binding) {
|
? tc.settings.keyBindings.map(function(binding) {
|
||||||
return Object.assign({}, binding);
|
return Object.assign({}, binding);
|
||||||
})
|
})
|
||||||
: tc.settings.keyBindings
|
: tc.settings.keyBindings
|
||||||
@@ -459,12 +414,13 @@ function resetSettingsFromSiteRuleBase() {
|
|||||||
tc.settings.controllerMarginTop = base.controllerMarginTop;
|
tc.settings.controllerMarginTop = base.controllerMarginTop;
|
||||||
tc.settings.controllerMarginBottom = base.controllerMarginBottom;
|
tc.settings.controllerMarginBottom = base.controllerMarginBottom;
|
||||||
tc.settings.enableSubtitleNudge = base.enableSubtitleNudge;
|
tc.settings.enableSubtitleNudge = base.enableSubtitleNudge;
|
||||||
|
tc.settings.subtitleNudgeEnabledByDefault = base.subtitleNudgeEnabledByDefault;
|
||||||
tc.settings.subtitleNudgeInterval = base.subtitleNudgeInterval;
|
tc.settings.subtitleNudgeInterval = base.subtitleNudgeInterval;
|
||||||
tc.settings.controllerButtons = Array.isArray(base.controllerButtons)
|
tc.settings.controllerButtons = Array.isArray(base.controllerButtons)
|
||||||
? base.controllerButtons.slice()
|
? base.controllerButtons.slice()
|
||||||
: base.controllerButtons;
|
: base.controllerButtons;
|
||||||
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
||||||
? base.keyBindings.map(function (binding) {
|
? base.keyBindings.map(function(binding) {
|
||||||
return Object.assign({}, binding);
|
return Object.assign({}, binding);
|
||||||
})
|
})
|
||||||
: base.keyBindings;
|
: base.keyBindings;
|
||||||
@@ -528,44 +484,41 @@ function cycleControllerLocation(video) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBindingKey(key) {
|
function normalizeBindingKey(key) {
|
||||||
if (typeof key !== "string" || key.length === 0) return null;
|
return keyBindingUtils.normalizeBindingKey(key);
|
||||||
if (key === "Spacebar") return " ";
|
|
||||||
if (key === "Esc") return "Escape";
|
|
||||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyKeyCodeToBinding(keyCode) {
|
function legacyBindingKeyToCode(key) {
|
||||||
if (!Number.isInteger(keyCode)) return null;
|
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||||
var key = keyCodeToEventKey[keyCode];
|
|
||||||
if (!key && keyCode >= 48 && keyCode <= 57) {
|
|
||||||
key = String.fromCharCode(keyCode);
|
|
||||||
}
|
|
||||||
if (!key && keyCode >= 65 && keyCode <= 90) {
|
|
||||||
key = String.fromCharCode(keyCode);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
key: normalizeBindingKey(key),
|
|
||||||
keyCode: keyCode,
|
|
||||||
code: null,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
||||||
if (!binding) return fallbackBinding;
|
}
|
||||||
|
|
||||||
|
function inferBindingCode(binding, fallbackCode) {
|
||||||
|
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStoredBinding(binding, fallbackCode) {
|
||||||
|
if (!binding) {
|
||||||
|
if (!fallbackCode) return null;
|
||||||
|
return {
|
||||||
|
code: fallbackCode,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: "false",
|
||||||
|
predefined: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding.disabled === true ||
|
binding.disabled === true ||
|
||||||
(binding.key === null &&
|
(binding.code === null &&
|
||||||
binding.keyCode === null &&
|
binding.key === null &&
|
||||||
binding.code === null)
|
binding.keyCode === null)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
action: binding.action,
|
action: binding.action,
|
||||||
key: null,
|
|
||||||
keyCode: null,
|
|
||||||
code: null,
|
code: null,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
value: Number(binding.value),
|
value: Number(binding.value),
|
||||||
@@ -574,46 +527,20 @@ function normalizeStoredBinding(binding, fallbackKeyCode) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
||||||
|
if (!normalizedCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var normalized = {
|
var normalized = {
|
||||||
action: binding.action,
|
action: binding.action,
|
||||||
key: null,
|
code: normalizedCode,
|
||||||
keyCode: null,
|
|
||||||
code:
|
|
||||||
typeof binding.code === "string" && binding.code.length > 0
|
|
||||||
? binding.code
|
|
||||||
: null,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
value: Number(binding.value),
|
value: Number(binding.value),
|
||||||
force: String(binding.force) === "true" ? "true" : "false",
|
force: String(binding.force) === "true" ? "true" : "false",
|
||||||
predefined: Boolean(binding.predefined)
|
predefined: Boolean(binding.predefined)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof binding.key === "string") {
|
|
||||||
normalized.key = normalizeBindingKey(binding.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
|
||||||
if (Number.isInteger(legacyKeyCode)) {
|
|
||||||
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
|
|
||||||
if (legacyBinding) {
|
|
||||||
normalized.key = normalized.key || legacyBinding.key;
|
|
||||||
normalized.keyCode = legacyKeyCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isInteger(binding.keyCode)) {
|
|
||||||
normalized.keyCode = binding.keyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalized.key && fallbackBinding) {
|
|
||||||
normalized.key = fallbackBinding.key;
|
|
||||||
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,19 +657,34 @@ function isSubtitleNudgeSupported(video) {
|
|||||||
return Boolean(video);
|
return Boolean(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSubtitleNudgeAvailableForVideo(video) {
|
||||||
|
return isSubtitleNudgeSupported(video) && Boolean(tc.settings.enableSubtitleNudge);
|
||||||
|
}
|
||||||
|
|
||||||
function isSubtitleNudgeEnabledForVideo(video) {
|
function isSubtitleNudgeEnabledForVideo(video) {
|
||||||
if (!video || !video.vsc) return tc.settings.enableSubtitleNudge;
|
if (!isSubtitleNudgeAvailableForVideo(video)) return false;
|
||||||
|
|
||||||
|
if (!video || !video.vsc) {
|
||||||
|
return Boolean(tc.settings.subtitleNudgeEnabledByDefault);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
|
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
|
||||||
return video.vsc.subtitleNudgeEnabledOverride;
|
return video.vsc.subtitleNudgeEnabledOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tc.settings.enableSubtitleNudge;
|
return Boolean(tc.settings.subtitleNudgeEnabledByDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
||||||
if (!video || !video.vsc) return false;
|
if (!video || !video.vsc) return false;
|
||||||
|
|
||||||
|
if (!isSubtitleNudgeAvailableForVideo(video)) {
|
||||||
|
video.vsc.subtitleNudgeEnabledOverride = null;
|
||||||
|
video.vsc.stopSubtitleNudge();
|
||||||
|
updateSubtitleNudgeIndicator(video);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var normalizedEnabled = Boolean(enabled);
|
var normalizedEnabled = Boolean(enabled);
|
||||||
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
|
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
|
||||||
|
|
||||||
@@ -759,7 +701,7 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
|||||||
if (flashEl) {
|
if (flashEl) {
|
||||||
flashEl.classList.add("visible");
|
flashEl.classList.add("visible");
|
||||||
clearTimeout(flashEl._flashTimer);
|
clearTimeout(flashEl._flashTimer);
|
||||||
flashEl._flashTimer = setTimeout(function () {
|
flashEl._flashTimer = setTimeout(function() {
|
||||||
flashEl.classList.remove("visible");
|
flashEl.classList.remove("visible");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
@@ -767,27 +709,68 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
|||||||
return normalizedEnabled;
|
return normalizedEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSubtitleNudgeIndicatorContent(target, isEnabled) {
|
||||||
|
if (!target) return;
|
||||||
|
var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff";
|
||||||
|
var custom =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[action] &&
|
||||||
|
tc.settings.customButtonIcons[action].svg;
|
||||||
|
vscClearElement(target);
|
||||||
|
if (custom) {
|
||||||
|
var customWrap = vscCreateSvgWrap(
|
||||||
|
target.ownerDocument || document,
|
||||||
|
custom,
|
||||||
|
"vsc-btn-icon"
|
||||||
|
);
|
||||||
|
if (customWrap) {
|
||||||
|
target.appendChild(customWrap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof vscIconSvgString !== "function") {
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var svg = vscIconSvgString(action, 14);
|
||||||
|
if (!svg) {
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var wrap = vscCreateSvgWrap(target.ownerDocument || document, svg, "vsc-btn-icon");
|
||||||
|
if (wrap) {
|
||||||
|
target.appendChild(wrap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
}
|
||||||
|
|
||||||
function updateSubtitleNudgeIndicator(video) {
|
function updateSubtitleNudgeIndicator(video) {
|
||||||
if (!video || !video.vsc) return;
|
if (!video || !video.vsc) return;
|
||||||
|
|
||||||
|
var isAvailable = isSubtitleNudgeAvailableForVideo(video);
|
||||||
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
||||||
var label = isEnabled ? "✓" : "×";
|
var title = !isAvailable
|
||||||
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
|
? "Subtitle nudge unavailable on this site"
|
||||||
|
: isEnabled
|
||||||
|
? "Subtitle nudge enabled"
|
||||||
|
: "Subtitle nudge disabled";
|
||||||
|
|
||||||
var indicator = video.vsc.subtitleNudgeIndicator;
|
var indicator = video.vsc.subtitleNudgeIndicator;
|
||||||
if (indicator) {
|
if (indicator) {
|
||||||
indicator.textContent = label;
|
renderSubtitleNudgeIndicatorContent(indicator, isEnabled);
|
||||||
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
||||||
indicator.dataset.supported = "true";
|
indicator.dataset.supported = isAvailable ? "true" : "false";
|
||||||
indicator.title = title;
|
indicator.title = title;
|
||||||
indicator.setAttribute("aria-label", title);
|
indicator.setAttribute("aria-label", title);
|
||||||
}
|
}
|
||||||
|
|
||||||
var flashEl = video.vsc.nudgeFlashIndicator;
|
var flashEl = video.vsc.nudgeFlashIndicator;
|
||||||
if (flashEl) {
|
if (flashEl) {
|
||||||
flashEl.textContent = label;
|
renderSubtitleNudgeIndicatorContent(flashEl, isEnabled);
|
||||||
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
||||||
flashEl.dataset.supported = "true";
|
flashEl.dataset.supported = isAvailable ? "true" : "false";
|
||||||
|
flashEl.setAttribute("aria-label", title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +780,7 @@ function schedulePersistLastSpeed(speed) {
|
|||||||
tc.pendingLastSpeedValue = speed;
|
tc.pendingLastSpeedValue = speed;
|
||||||
if (tc.pendingLastSpeedSave !== null) return;
|
if (tc.pendingLastSpeedSave !== null) return;
|
||||||
|
|
||||||
tc.pendingLastSpeedSave = setTimeout(function () {
|
tc.pendingLastSpeedSave = setTimeout(function() {
|
||||||
var speedToPersist = tc.pendingLastSpeedValue;
|
var speedToPersist = tc.pendingLastSpeedValue;
|
||||||
tc.pendingLastSpeedSave = null;
|
tc.pendingLastSpeedSave = null;
|
||||||
|
|
||||||
@@ -805,7 +788,8 @@ function schedulePersistLastSpeed(speed) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function () { });
|
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function() {
|
||||||
|
});
|
||||||
tc.persistedLastSpeed = speedToPersist;
|
tc.persistedLastSpeed = speedToPersist;
|
||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
@@ -864,6 +848,10 @@ function applySourceTransitionPolicy(video, forceUpdate) {
|
|||||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||||
setSpeed(video, desiredSpeed, false, false);
|
setSpeed(video, desiredSpeed, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same-tab SPA (e.g. YouTube watch → Shorts): URL can change while remember-speed
|
||||||
|
// already ran on src mutation — re-apply margins / location / opacity for new rules.
|
||||||
|
reapplySiteRulesAndControllerGeometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendSpeedRestoreWindow(video, duration) {
|
function extendSpeedRestoreWindow(video, duration) {
|
||||||
@@ -883,7 +871,7 @@ function scheduleSpeedRestore(video, desiredSpeed, reason) {
|
|||||||
clearTimeout(video.vsc.restoreSpeedTimer);
|
clearTimeout(video.vsc.restoreSpeedTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
video.vsc.restoreSpeedTimer = setTimeout(function () {
|
video.vsc.restoreSpeedTimer = setTimeout(function() {
|
||||||
if (!video.vsc) return;
|
if (!video.vsc) return;
|
||||||
|
|
||||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||||
@@ -930,19 +918,13 @@ function takePendingRateChange(video, currentSpeed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesKeyBinding(binding, event) {
|
function matchesKeyBinding(binding, event) {
|
||||||
if (!binding || binding.disabled) return false;
|
return Boolean(
|
||||||
|
binding &&
|
||||||
var normalizedEventKey = normalizeBindingKey(event.key);
|
binding.disabled !== true &&
|
||||||
if (binding.key && normalizedEventKey) {
|
typeof binding.code === "string" &&
|
||||||
return binding.key === normalizedEventKey;
|
binding.code.length > 0 &&
|
||||||
}
|
binding.code === event.code
|
||||||
|
);
|
||||||
if (binding.code && event.code) {
|
|
||||||
return binding.code === event.code;
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
|
||||||
return Number.isInteger(legacyKeyCode) && legacyKeyCode === event.keyCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaSelector() {
|
function mediaSelector() {
|
||||||
@@ -972,7 +954,7 @@ function hasUsableMediaSource(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.querySelectorAll) {
|
if (node.querySelectorAll) {
|
||||||
return Array.from(node.querySelectorAll("source[src]")).some(function (
|
return Array.from(node.querySelectorAll("source[src]")).some(function(
|
||||||
source
|
source
|
||||||
) {
|
) {
|
||||||
var src = source.getAttribute("src");
|
var src = source.getAttribute("src");
|
||||||
@@ -992,6 +974,14 @@ function ensureController(node, parent) {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// href selects site rules; re-run on every new/usable media so margins/opacity match current URL.
|
||||||
|
applySiteRuleOverrides();
|
||||||
|
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
refreshAllControllerGeometry();
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
|
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
|
||||||
4
|
4
|
||||||
@@ -1051,6 +1041,22 @@ function scanNodeForMedia(node, parent, added) {
|
|||||||
if (node.shadowRoot) {
|
if (node.shadowRoot) {
|
||||||
observeRoot(node.shadowRoot);
|
observeRoot(node.shadowRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep-scan descendant elements for shadow roots we haven't observed yet.
|
||||||
|
// This catches custom elements (like archive.org's <play-av>) whose shadow
|
||||||
|
// roots were created before our attachShadow patch was installed.
|
||||||
|
if (added && typeof node.querySelectorAll === "function") {
|
||||||
|
try {
|
||||||
|
var allElements = node.querySelectorAll("*");
|
||||||
|
for (var j = 0; j < allElements.length; j++) {
|
||||||
|
if (allElements[j].shadowRoot && !vscObservedRoots.has(allElements[j].shadowRoot)) {
|
||||||
|
observeRoot(allElements[j].shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// querySelectorAll may throw on detached or unusual nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScanNodeForRoot(root) {
|
function getScanNodeForRoot(root) {
|
||||||
@@ -1064,6 +1070,8 @@ function getScanNodeForRoot(root) {
|
|||||||
function rootMayContainMedia(root) {
|
function rootMayContainMedia(root) {
|
||||||
if (!root) return false;
|
if (!root) return false;
|
||||||
if (root.nodeType === Node.DOCUMENT_NODE) return true;
|
if (root.nodeType === Node.DOCUMENT_NODE) return true;
|
||||||
|
// Always scan shadow roots so we can find nested shadow roots or media.
|
||||||
|
if (root.host || (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot)) return true;
|
||||||
if (typeof root.querySelector !== "function") return true;
|
if (typeof root.querySelector !== "function") return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1073,6 +1081,7 @@ function rootMayContainMedia(root) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function scanRootForMedia(root) {
|
function scanRootForMedia(root) {
|
||||||
var scanRoot = getScanNodeForRoot(root);
|
var scanRoot = getScanNodeForRoot(root);
|
||||||
if (!scanRoot) return;
|
if (!scanRoot) return;
|
||||||
@@ -1083,13 +1092,30 @@ function scanRootForMedia(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function observeRoot(root) {
|
function observeRoot(root) {
|
||||||
if (!root || vscObservedRoots.has(root)) return;
|
if (!root) return;
|
||||||
vscObservedRoots.add(root);
|
|
||||||
setupListener(root);
|
var isConnected = false;
|
||||||
attachMutationObserver(root);
|
try {
|
||||||
attachMediaDetectionListeners(root);
|
isConnected = root.nodeType === Node.DOCUMENT_NODE ||
|
||||||
if (rootMayContainMedia(root)) {
|
root.isConnected ||
|
||||||
scanRootForMedia(root);
|
(root.host && (root.host.isConnected || (root.host.ownerDocument && root.host.ownerDocument.contains(root.host)))) ||
|
||||||
|
(root.ownerDocument && root.ownerDocument.contains(root));
|
||||||
|
} catch (e) {
|
||||||
|
isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vscObservedRoots.has(root)) {
|
||||||
|
vscObservedRoots.add(root);
|
||||||
|
setupListener(root);
|
||||||
|
attachMutationObserver(root);
|
||||||
|
attachMediaDetectionListeners(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected && !vscConnectedScannedRoots.has(root)) {
|
||||||
|
vscConnectedScannedRoots.add(root);
|
||||||
|
if (rootMayContainMedia(root)) {
|
||||||
|
scanRootForMedia(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,7 +1129,7 @@ function patchAttachShadow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var originalAttachShadow = Element.prototype.attachShadow;
|
var originalAttachShadow = Element.prototype.attachShadow;
|
||||||
Element.prototype.attachShadow = function () {
|
Element.prototype.attachShadow = function() {
|
||||||
var shadowRoot = originalAttachShadow.apply(this, arguments);
|
var shadowRoot = originalAttachShadow.apply(this, arguments);
|
||||||
try {
|
try {
|
||||||
if (shadowRoot) {
|
if (shadowRoot) {
|
||||||
@@ -1134,7 +1160,13 @@ function log(message, level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.sync.get(tc.settings, function (storage) {
|
// Patch attachShadow immediately — before any async operations — so we
|
||||||
|
// catch shadow roots created while chrome.storage.sync.get is pending.
|
||||||
|
// Sites like archive.org create Lit/LitElement shadow DOMs during page load;
|
||||||
|
// waiting for the storage callback would miss them entirely.
|
||||||
|
patchAttachShadow();
|
||||||
|
|
||||||
|
chrome.storage.sync.get(tc.settings, function(storage) {
|
||||||
var storedBindings = Array.isArray(storage.keyBindings)
|
var storedBindings = Array.isArray(storage.keyBindings)
|
||||||
? storage.keyBindings
|
? storage.keyBindings
|
||||||
: [];
|
: [];
|
||||||
@@ -1149,7 +1181,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
chrome.storage.sync.set({
|
chrome.storage.sync.set({
|
||||||
keyBindings: tc.settings.keyBindings,
|
keyBindings: tc.settings.keyBindings,
|
||||||
version: tc.settings.version,
|
version: tc.settings.version,
|
||||||
displayKeyCode: tc.settings.displayKeyCode,
|
|
||||||
rememberSpeed: tc.settings.rememberSpeed,
|
rememberSpeed: tc.settings.rememberSpeed,
|
||||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
||||||
audioBoolean: tc.settings.audioBoolean,
|
audioBoolean: tc.settings.audioBoolean,
|
||||||
@@ -1169,7 +1200,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
tc.settings.lastSpeed = 1.0;
|
tc.settings.lastSpeed = 1.0;
|
||||||
}
|
}
|
||||||
tc.persistedLastSpeed = tc.settings.lastSpeed;
|
tc.persistedLastSpeed = tc.settings.lastSpeed;
|
||||||
tc.settings.displayKeyCode = Number(storage.displayKeyCode);
|
|
||||||
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
|
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
|
||||||
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
|
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
|
||||||
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
|
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
|
||||||
@@ -1210,29 +1240,14 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
? storage.controllerButtons
|
? storage.controllerButtons
|
||||||
: tc.settings.controllerButtons;
|
: tc.settings.controllerButtons;
|
||||||
|
|
||||||
// Migrate legacy blacklist if present
|
|
||||||
if (storage.blacklist && typeof storage.blacklist === "string" && tc.settings.siteRules.length === 0) {
|
|
||||||
var lines = storage.blacklist.split("\n");
|
|
||||||
lines.forEach((line) => {
|
|
||||||
var pattern = line.replace(regStrip, "");
|
|
||||||
if (pattern.length > 0) {
|
|
||||||
tc.settings.siteRules.push({
|
|
||||||
pattern: pattern,
|
|
||||||
disableExtension: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (tc.settings.siteRules.length > 0) {
|
|
||||||
chrome.storage.sync.set({ siteRules: tc.settings.siteRules });
|
|
||||||
chrome.storage.sync.remove(["blacklist"]);
|
|
||||||
log("Migrated legacy blacklist to site rules", 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.settings.enableSubtitleNudge =
|
tc.settings.enableSubtitleNudge =
|
||||||
typeof storage.enableSubtitleNudge !== "undefined"
|
typeof storage.enableSubtitleNudge !== "undefined"
|
||||||
? Boolean(storage.enableSubtitleNudge)
|
? Boolean(storage.enableSubtitleNudge)
|
||||||
: tc.settings.enableSubtitleNudge;
|
: tc.settings.enableSubtitleNudge;
|
||||||
|
tc.settings.subtitleNudgeEnabledByDefault =
|
||||||
|
typeof storage.subtitleNudgeEnabledByDefault !== "undefined"
|
||||||
|
? Boolean(storage.subtitleNudgeEnabledByDefault)
|
||||||
|
: tc.settings.subtitleNudgeEnabledByDefault;
|
||||||
tc.settings.subtitleNudgeInterval = Math.min(
|
tc.settings.subtitleNudgeInterval = Math.min(
|
||||||
1000,
|
1000,
|
||||||
Math.max(10, Number(storage.subtitleNudgeInterval) || 50)
|
Math.max(10, Number(storage.subtitleNudgeInterval) || 50)
|
||||||
@@ -1243,67 +1258,142 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding(
|
ensureDefaultKeyBinding(
|
||||||
"display",
|
"display",
|
||||||
"V",
|
"KeyV",
|
||||||
Number(storage.displayKeyCode) || 86,
|
|
||||||
0
|
0
|
||||||
) || addedDefaultBinding;
|
) || addedDefaultBinding;
|
||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding("move", "P", 80, 0) || addedDefaultBinding;
|
ensureDefaultKeyBinding("move", "KeyP", 0) || addedDefaultBinding;
|
||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding("toggleSubtitleNudge", "N", 78, 0) ||
|
ensureDefaultKeyBinding("toggleSubtitleNudge", "KeyN", 0) ||
|
||||||
addedDefaultBinding;
|
addedDefaultBinding;
|
||||||
|
|
||||||
if (addedDefaultBinding) {
|
if (addedDefaultBinding) {
|
||||||
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
||||||
}
|
}
|
||||||
captureSiteRuleBase();
|
captureSiteRuleBase();
|
||||||
patchAttachShadow();
|
// patchAttachShadow() is now called at top-level before this callback
|
||||||
// Add a listener for messages from the popup.
|
// Add a listener for messages from the popup.
|
||||||
// We use a global flag to ensure the listener is only attached once.
|
// We use a global flag to ensure the listener is only attached once.
|
||||||
if (!window.vscMessageListener) {
|
if (!window.vscMessageListener) {
|
||||||
chrome.runtime.onMessage.addListener(
|
chrome.runtime.onMessage.addListener(
|
||||||
function (request, sender, sendResponse) {
|
function(request, sender, sendResponse) {
|
||||||
if (request.action === "rescan_page") {
|
if (request.action === "rescan_page") {
|
||||||
log("Re-scan command received from popup.", 4);
|
log("Re-scan command received from popup.", 4);
|
||||||
initializeWhenReady(document, true);
|
initializeWhenReady(document, true);
|
||||||
sendResponse({ status: "complete" });
|
sendResponse({ status: "complete" });
|
||||||
} else if (request.action === "get_speed") {
|
return false;
|
||||||
var speed = 1.0;
|
}
|
||||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
if (request.action === "get_speed") {
|
||||||
for (var i = 0; i < tc.mediaElements.length; i++) {
|
// Do not sendResponse in frames with no media — only one response is
|
||||||
if (tc.mediaElements[i] && !tc.mediaElements[i].paused) {
|
// accepted tab-wide, and the top frame often wins before an iframe.
|
||||||
speed = tc.mediaElements[i].playbackRate;
|
var videoGs = getPrimaryVideoElement();
|
||||||
break;
|
if (!videoGs) return false;
|
||||||
}
|
sendResponse({
|
||||||
}
|
speed: videoGs.playbackRate
|
||||||
if (speed === 1.0 && tc.mediaElements[0]) {
|
});
|
||||||
speed = tc.mediaElements[0].playbackRate;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if (request.action === "get_page_context") {
|
||||||
sendResponse({ speed: speed });
|
|
||||||
} else if (request.action === "get_page_context") {
|
|
||||||
sendResponse({ url: location.href });
|
sendResponse({ url: location.href });
|
||||||
} else if (request.action === "run_action") {
|
return false;
|
||||||
|
}
|
||||||
|
if (request.action === "set_force_last_saved_speed") {
|
||||||
|
tc.settings.forceLastSavedSpeed = Boolean(request.enabled);
|
||||||
|
if (isValidSpeed(Number(request.speed))) {
|
||||||
|
tc.settings.lastSpeed = Number(request.speed);
|
||||||
|
}
|
||||||
|
var forceVideo = getPrimaryVideoElement();
|
||||||
|
if (!forceVideo) return false;
|
||||||
|
if (tc.settings.forceLastSavedSpeed) {
|
||||||
|
tc.mediaElements.forEach(function(video) {
|
||||||
|
if (!video || !video.vsc) return;
|
||||||
|
setSpeed(video, tc.settings.lastSpeed, false, true);
|
||||||
|
extendSpeedRestoreWindow(video);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sendResponse({
|
||||||
|
enabled: tc.settings.forceLastSavedSpeed,
|
||||||
|
speed: forceVideo.playbackRate
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (request.action === "run_action") {
|
||||||
var value = request.value;
|
var value = request.value;
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
value = getKeyBindings(request.actionName, "value");
|
value = getKeyBindings(request.actionName, "value");
|
||||||
}
|
}
|
||||||
runAction(request.actionName, value);
|
runAction(request.actionName, value);
|
||||||
var newSpeed = 1.0;
|
var videoAfter = getPrimaryVideoElement();
|
||||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
if (!videoAfter) return false;
|
||||||
newSpeed = tc.mediaElements[0].playbackRate;
|
sendResponse({
|
||||||
}
|
speed: videoAfter.playbackRate
|
||||||
sendResponse({ speed: newSpeed });
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the flag to prevent adding the listener again.
|
// Set the flag to prevent adding the listener again.
|
||||||
window.vscMessageListener = true;
|
window.vscMessageListener = true;
|
||||||
}
|
}
|
||||||
initializeWhenReady(document);
|
chrome.storage.local.get(["customButtonIcons"], function(loc) {
|
||||||
|
tc.settings.customButtonIcons =
|
||||||
|
loc &&
|
||||||
|
loc.customButtonIcons &&
|
||||||
|
typeof loc.customButtonIcons === "object"
|
||||||
|
? loc.customButtonIcons
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (!window.vscCustomIconListener) {
|
||||||
|
window.vscCustomIconListener = true;
|
||||||
|
chrome.storage.onChanged.addListener(function(changes, area) {
|
||||||
|
if (area !== "local" || !changes.customButtonIcons) return;
|
||||||
|
var nv = changes.customButtonIcons.newValue;
|
||||||
|
tc.settings.customButtonIcons =
|
||||||
|
nv && typeof nv === "object" ? nv : {};
|
||||||
|
if (tc.mediaElements && tc.mediaElements.length) {
|
||||||
|
tc.mediaElements.forEach(function(video) {
|
||||||
|
if (!video.vsc || !video.vsc.div) return;
|
||||||
|
var doc = video.ownerDocument;
|
||||||
|
var shadow = video.vsc.div.shadowRoot;
|
||||||
|
if (!shadow) return;
|
||||||
|
shadow.querySelectorAll("button[data-action]").forEach(function(btn) {
|
||||||
|
var act = btn.dataset.action;
|
||||||
|
if (!act) return;
|
||||||
|
var svg =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[act] &&
|
||||||
|
tc.settings.customButtonIcons[act].svg;
|
||||||
|
vscClearElement(btn);
|
||||||
|
if (svg) {
|
||||||
|
var cw = vscCreateSvgWrap(doc, svg, "vsc-btn-icon");
|
||||||
|
if (cw) {
|
||||||
|
btn.appendChild(cw);
|
||||||
|
} else {
|
||||||
|
var cdf = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf && cdf.label) || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconWrap === "function") {
|
||||||
|
var wrap = vscIconWrap(doc, act, 14);
|
||||||
|
if (wrap) {
|
||||||
|
btn.appendChild(wrap);
|
||||||
|
} else {
|
||||||
|
var cdf = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf && cdf.label) || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var cdf2 = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf2 && cdf2.label) || "?";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSubtitleNudgeIndicator(video);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initializeWhenReady(document);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getKeyBindings(action, what = "value") {
|
function getKeyBindings(action, what = "value") {
|
||||||
@@ -1313,6 +1403,7 @@ function getKeyBindings(action, what = "value") {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setKeyBindings(action, value) {
|
function setKeyBindings(action, value) {
|
||||||
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
|
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
|
||||||
value;
|
value;
|
||||||
@@ -1321,15 +1412,172 @@ function setKeyBindings(action, value) {
|
|||||||
function createControllerButton(doc, action, label, className) {
|
function createControllerButton(doc, action, label, className) {
|
||||||
var button = doc.createElement("button");
|
var button = doc.createElement("button");
|
||||||
button.dataset.action = action;
|
button.dataset.action = action;
|
||||||
button.textContent = label;
|
var custom =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[action] &&
|
||||||
|
tc.settings.customButtonIcons[action].svg;
|
||||||
|
if (custom) {
|
||||||
|
var customWrap = vscCreateSvgWrap(doc, custom, "vsc-btn-icon");
|
||||||
|
if (customWrap) {
|
||||||
|
button.appendChild(customWrap);
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconWrap === "function") {
|
||||||
|
var wrap = vscIconWrap(doc, action, 14);
|
||||||
|
if (wrap) {
|
||||||
|
button.appendChild(wrap);
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
if (className) {
|
if (className) {
|
||||||
button.className = className;
|
button.className = className;
|
||||||
}
|
}
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getControllerMount(video) {
|
||||||
|
if (!video || !video.parentElement) return null;
|
||||||
|
|
||||||
|
var videoRect = video.getBoundingClientRect();
|
||||||
|
var mount = video.parentElement;
|
||||||
|
var candidate = mount;
|
||||||
|
var depth = 0;
|
||||||
|
|
||||||
|
// Player click-catchers are often siblings of the video's immediate parent.
|
||||||
|
// Climb through tightly-sized wrappers so our host shares their stacking
|
||||||
|
// context, but stop before broad page-layout containers.
|
||||||
|
while (candidate && candidate.parentElement && depth < 5) {
|
||||||
|
var next = candidate.parentElement;
|
||||||
|
var nextRect = next.getBoundingClientRect();
|
||||||
|
var widthLimit = Math.max(videoRect.width * 1.35, videoRect.width + 80);
|
||||||
|
var heightLimit = Math.max(videoRect.height * 1.35, videoRect.height + 80);
|
||||||
|
var containsVideo =
|
||||||
|
nextRect.left <= videoRect.left + 1 &&
|
||||||
|
nextRect.top <= videoRect.top + 1 &&
|
||||||
|
nextRect.right >= videoRect.right - 1 &&
|
||||||
|
nextRect.bottom >= videoRect.bottom - 1;
|
||||||
|
|
||||||
|
if (
|
||||||
|
videoRect.width <= 0 ||
|
||||||
|
videoRect.height <= 0 ||
|
||||||
|
!containsVideo ||
|
||||||
|
nextRect.width > widthLimit ||
|
||||||
|
nextRect.height > heightLimit
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount = next;
|
||||||
|
candidate = next;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionControllerHost(wrapper, video, mount) {
|
||||||
|
if (!wrapper || !video || !mount || !wrapper.isConnected) return;
|
||||||
|
|
||||||
|
var videoRect = video.getBoundingClientRect();
|
||||||
|
var mountRect = mount.getBoundingClientRect();
|
||||||
|
if (videoRect.width <= 0 || videoRect.height <= 0) return;
|
||||||
|
|
||||||
|
// Convert viewport pixels back into the mount's CSS pixel space. This keeps
|
||||||
|
// the overlay aligned even when a player or ancestor is scaled.
|
||||||
|
var mountWidth = mount.offsetWidth || mountRect.width || 1;
|
||||||
|
var mountHeight = mount.offsetHeight || mountRect.height || 1;
|
||||||
|
var scaleX = mountRect.width > 0 ? mountRect.width / mountWidth : 1;
|
||||||
|
var scaleY = mountRect.height > 0 ? mountRect.height / mountHeight : 1;
|
||||||
|
var left =
|
||||||
|
(videoRect.left - mountRect.left) / scaleX -
|
||||||
|
(mount.clientLeft || 0) +
|
||||||
|
(mount.scrollLeft || 0);
|
||||||
|
var top =
|
||||||
|
(videoRect.top - mountRect.top) / scaleY -
|
||||||
|
(mount.clientTop || 0) +
|
||||||
|
(mount.scrollTop || 0);
|
||||||
|
|
||||||
|
wrapper.style.setProperty("left", left + "px", "important");
|
||||||
|
wrapper.style.setProperty("top", top + "px", "important");
|
||||||
|
wrapper.style.setProperty(
|
||||||
|
"width",
|
||||||
|
videoRect.width / scaleX + "px",
|
||||||
|
"important"
|
||||||
|
);
|
||||||
|
wrapper.style.setProperty(
|
||||||
|
"height",
|
||||||
|
videoRect.height / scaleY + "px",
|
||||||
|
"important"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupControllerHostTracking(videoController, wrapper, mount) {
|
||||||
|
if (!videoController || !wrapper || !mount) return;
|
||||||
|
|
||||||
|
var doc = videoController.video.ownerDocument;
|
||||||
|
var win = doc.defaultView || window;
|
||||||
|
var frameId = null;
|
||||||
|
var update = function() {
|
||||||
|
frameId = null;
|
||||||
|
positionControllerHost(wrapper, videoController.video, mount);
|
||||||
|
};
|
||||||
|
var schedule = function() {
|
||||||
|
if (frameId !== null) return;
|
||||||
|
frameId = win.requestAnimationFrame(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (win.getComputedStyle(mount).position === "static") {
|
||||||
|
mount.dataset.vscPositionOwner = "true";
|
||||||
|
mount.dataset.vscOriginalPosition = mount.style.getPropertyValue("position");
|
||||||
|
mount.dataset.vscOriginalPositionPriority =
|
||||||
|
mount.style.getPropertyPriority("position");
|
||||||
|
mount.style.setProperty("position", "relative");
|
||||||
|
}
|
||||||
|
|
||||||
|
var resizeObserver = null;
|
||||||
|
if (typeof win.ResizeObserver === "function") {
|
||||||
|
resizeObserver = new win.ResizeObserver(schedule);
|
||||||
|
resizeObserver.observe(videoController.video);
|
||||||
|
resizeObserver.observe(mount);
|
||||||
|
}
|
||||||
|
|
||||||
|
win.addEventListener("resize", schedule, { passive: true });
|
||||||
|
doc.addEventListener("fullscreenchange", schedule, { passive: true });
|
||||||
|
mount.addEventListener("scroll", schedule, { passive: true });
|
||||||
|
update();
|
||||||
|
|
||||||
|
videoController.controllerHostCleanup = function() {
|
||||||
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
|
if (frameId !== null) win.cancelAnimationFrame(frameId);
|
||||||
|
win.removeEventListener("resize", schedule);
|
||||||
|
doc.removeEventListener("fullscreenchange", schedule);
|
||||||
|
mount.removeEventListener("scroll", schedule);
|
||||||
|
if (
|
||||||
|
mount.dataset.vscPositionOwner === "true" &&
|
||||||
|
!mount.querySelector(".vsc-controller")
|
||||||
|
) {
|
||||||
|
if (mount.dataset.vscOriginalPosition) {
|
||||||
|
mount.style.setProperty(
|
||||||
|
"position",
|
||||||
|
mount.dataset.vscOriginalPosition,
|
||||||
|
mount.dataset.vscOriginalPositionPriority || ""
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mount.style.removeProperty("position");
|
||||||
|
}
|
||||||
|
delete mount.dataset.vscPositionOwner;
|
||||||
|
delete mount.dataset.vscOriginalPosition;
|
||||||
|
delete mount.dataset.vscOriginalPositionPriority;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function defineVideoController() {
|
function defineVideoController() {
|
||||||
tc.videoController = function (target, parent) {
|
tc.videoController = function(target, parent) {
|
||||||
if (target.vsc) return target.vsc;
|
if (target.vsc) return target.vsc;
|
||||||
tc.mediaElements.push(target);
|
tc.mediaElements.push(target);
|
||||||
target.vsc = this;
|
target.vsc = this;
|
||||||
@@ -1343,11 +1591,13 @@ function defineVideoController() {
|
|||||||
this.suppressedRateChangeCount = 0;
|
this.suppressedRateChangeCount = 0;
|
||||||
this.suppressedRateChangeUntil = 0;
|
this.suppressedRateChangeUntil = 0;
|
||||||
this.visibilityResumeHandler = null;
|
this.visibilityResumeHandler = null;
|
||||||
|
this.resetToggleArmed = false;
|
||||||
|
this.resetButtonEl = null;
|
||||||
this.controllerLocation = normalizeControllerLocation(
|
this.controllerLocation = normalizeControllerLocation(
|
||||||
tc.settings.controllerLocation
|
tc.settings.controllerLocation
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || 'none'}`, 4);
|
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || "none"}`, 4);
|
||||||
|
|
||||||
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
|
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
|
||||||
this.targetSpeed = storedSpeed;
|
this.targetSpeed = storedSpeed;
|
||||||
@@ -1367,9 +1617,9 @@ function defineVideoController() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains('vsc-hidden')}`, 4);
|
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains("vsc-hidden")}`, 4);
|
||||||
|
|
||||||
var mediaEventAction = function (event) {
|
var mediaEventAction = function(event) {
|
||||||
if (
|
if (
|
||||||
event.type === "loadedmetadata" ||
|
event.type === "loadedmetadata" ||
|
||||||
event.type === "loadeddata" ||
|
event.type === "loadeddata" ||
|
||||||
@@ -1487,7 +1737,7 @@ function defineVideoController() {
|
|||||||
this.startSubtitleNudge();
|
this.startSubtitleNudge();
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.remove = function () {
|
tc.videoController.prototype.remove = function() {
|
||||||
this.stopSubtitleNudge();
|
this.stopSubtitleNudge();
|
||||||
if (this.youTubeAutoHideObserver) {
|
if (this.youTubeAutoHideObserver) {
|
||||||
this.youTubeAutoHideObserver.disconnect();
|
this.youTubeAutoHideObserver.disconnect();
|
||||||
@@ -1502,6 +1752,10 @@ function defineVideoController() {
|
|||||||
this.genericAutoHideCleanup = null;
|
this.genericAutoHideCleanup = null;
|
||||||
}
|
}
|
||||||
if (this.div) this.div.remove();
|
if (this.div) this.div.remove();
|
||||||
|
if (this.controllerHostCleanup) {
|
||||||
|
this.controllerHostCleanup();
|
||||||
|
this.controllerHostCleanup = null;
|
||||||
|
}
|
||||||
if (this.restoreSpeedTimer) clearTimeout(this.restoreSpeedTimer);
|
if (this.restoreSpeedTimer) clearTimeout(this.restoreSpeedTimer);
|
||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
this.video.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
||||||
@@ -1519,7 +1773,7 @@ function defineVideoController() {
|
|||||||
if (idx != -1) tc.mediaElements.splice(idx, 1);
|
if (idx != -1) tc.mediaElements.splice(idx, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.startSubtitleNudge = function () {
|
tc.videoController.prototype.startSubtitleNudge = function() {
|
||||||
if (
|
if (
|
||||||
!isSubtitleNudgeSupported(this.video) ||
|
!isSubtitleNudgeSupported(this.video) ||
|
||||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||||
@@ -1592,7 +1846,7 @@ function defineVideoController() {
|
|||||||
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
|
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.stopSubtitleNudge = function () {
|
tc.videoController.prototype.stopSubtitleNudge = function() {
|
||||||
if (this.nudgeAnimationId !== null) {
|
if (this.nudgeAnimationId !== null) {
|
||||||
clearTimeout(this.nudgeAnimationId);
|
clearTimeout(this.nudgeAnimationId);
|
||||||
this.nudgeAnimationId = null;
|
this.nudgeAnimationId = null;
|
||||||
@@ -1610,7 +1864,7 @@ function defineVideoController() {
|
|||||||
// doesn't lose the user's intended speed if the site hijacks it.
|
// doesn't lose the user's intended speed if the site hijacks it.
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.performImmediateNudge = function () {
|
tc.videoController.prototype.performImmediateNudge = function() {
|
||||||
if (
|
if (
|
||||||
!isSubtitleNudgeSupported(this.video) ||
|
!isSubtitleNudgeSupported(this.video) ||
|
||||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||||
@@ -1640,7 +1894,7 @@ function defineVideoController() {
|
|||||||
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
|
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.setupYouTubeAutoHide = function (wrapper) {
|
tc.videoController.prototype.setupYouTubeAutoHide = function(wrapper) {
|
||||||
if (!wrapper || !isOnYouTube()) return;
|
if (!wrapper || !isOnYouTube()) return;
|
||||||
|
|
||||||
const video = this.video;
|
const video = this.video;
|
||||||
@@ -1656,7 +1910,7 @@ function defineVideoController() {
|
|||||||
// The vsc-hidden class (from V key) takes precedence via CSS specificity
|
// The vsc-hidden class (from V key) takes precedence via CSS specificity
|
||||||
if (ytPlayer.classList.contains("ytp-autohide")) {
|
if (ytPlayer.classList.contains("ytp-autohide")) {
|
||||||
wrapper.classList.add("ytp-autohide");
|
wrapper.classList.add("ytp-autohide");
|
||||||
|
|
||||||
// Immediately end any temporary "vsc-show" state to hide with YouTube
|
// Immediately end any temporary "vsc-show" state to hide with YouTube
|
||||||
// UNLESS it was forced by a shortcut (vsc-forced-show)
|
// UNLESS it was forced by a shortcut (vsc-forced-show)
|
||||||
if (!wrapper.classList.contains("vsc-forced-show")) {
|
if (!wrapper.classList.contains("vsc-forced-show")) {
|
||||||
@@ -1666,7 +1920,7 @@ function defineVideoController() {
|
|||||||
wrapper.showTimeOut = undefined;
|
wrapper.showTimeOut = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("YouTube controls hidden, hiding controller", 5);
|
log("YouTube controls hidden, hiding controller", 5);
|
||||||
} else {
|
} else {
|
||||||
wrapper.classList.remove("ytp-autohide");
|
wrapper.classList.remove("ytp-autohide");
|
||||||
@@ -1716,7 +1970,7 @@ function defineVideoController() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.setupGenericAutoHide = function (wrapper) {
|
tc.videoController.prototype.setupGenericAutoHide = function(wrapper) {
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
|
||||||
const video = this.video;
|
const video = this.video;
|
||||||
@@ -1775,7 +2029,7 @@ function defineVideoController() {
|
|||||||
log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4);
|
log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.initializeControls = function () {
|
tc.videoController.prototype.initializeControls = function() {
|
||||||
const doc = this.video.ownerDocument;
|
const doc = this.video.ownerDocument;
|
||||||
const speed = this.video.playbackRate.toFixed(2);
|
const speed = this.video.playbackRate.toFixed(2);
|
||||||
var wrapper = doc.createElement("div");
|
var wrapper = doc.createElement("div");
|
||||||
@@ -1783,12 +2037,14 @@ function defineVideoController() {
|
|||||||
if (!this.video.src && !this.video.currentSrc)
|
if (!this.video.src && !this.video.currentSrc)
|
||||||
wrapper.classList.add("vsc-nosource");
|
wrapper.classList.add("vsc-nosource");
|
||||||
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
||||||
// Use lower z-index for non-YouTube sites to avoid overlapping modals
|
// z-index is handled by the base .vsc-controller CSS rule (2147483646).
|
||||||
if (!isOnYouTube()) wrapper.classList.add("vsc-non-youtube");
|
// The controller lives inside the video container, so high z-index only
|
||||||
|
// makes it topmost within the local stacking context — it won't overlay
|
||||||
|
// page-level modals or dialogs.
|
||||||
var shadow = wrapper.attachShadow({ mode: "open" });
|
var shadow = wrapper.attachShadow({ mode: "open" });
|
||||||
var shadowStylesheet = doc.createElement("link");
|
var shadowStylesheet = doc.createElement("link");
|
||||||
shadowStylesheet.rel = "stylesheet";
|
shadowStylesheet.rel = "stylesheet";
|
||||||
shadowStylesheet.href = chrome.runtime.getURL("shadow.css");
|
shadowStylesheet.href = chrome.runtime.getURL("content/shadow.css");
|
||||||
shadow.appendChild(shadowStylesheet);
|
shadow.appendChild(shadowStylesheet);
|
||||||
|
|
||||||
var controller = doc.createElement("div");
|
var controller = doc.createElement("div");
|
||||||
@@ -1813,7 +2069,7 @@ function defineVideoController() {
|
|||||||
|
|
||||||
var subtitleNudgeIndicator = null;
|
var subtitleNudgeIndicator = null;
|
||||||
|
|
||||||
buttonConfig.forEach(function (btnId) {
|
buttonConfig.forEach(function(btnId) {
|
||||||
if (btnId === "nudge") {
|
if (btnId === "nudge") {
|
||||||
subtitleNudgeIndicator = doc.createElement("span");
|
subtitleNudgeIndicator = doc.createElement("span");
|
||||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||||
@@ -1836,24 +2092,36 @@ function defineVideoController() {
|
|||||||
nudgeFlashIndicator.setAttribute("aria-hidden", "true");
|
nudgeFlashIndicator.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
controller.appendChild(dragHandle);
|
controller.appendChild(dragHandle);
|
||||||
controller.appendChild(nudgeFlashIndicator);
|
|
||||||
controller.appendChild(controls);
|
controller.appendChild(controls);
|
||||||
|
/* Flash sits after #controls so it never inserts space between speed and buttons. */
|
||||||
|
controller.appendChild(nudgeFlashIndicator);
|
||||||
shadow.appendChild(controller);
|
shadow.appendChild(controller);
|
||||||
|
|
||||||
this.speedIndicator = dragHandle;
|
this.speedIndicator = dragHandle;
|
||||||
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
||||||
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
||||||
|
this.resetButtonEl =
|
||||||
|
shadow.querySelector("button[data-action=\"reset\"]") || null;
|
||||||
|
this.resetToggleArmed = false;
|
||||||
if (subtitleNudgeIndicator) {
|
if (subtitleNudgeIndicator) {
|
||||||
updateSubtitleNudgeIndicator(this.video);
|
updateSubtitleNudgeIndicator(this.video);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blurAfterPointerTap(target, e) {
|
||||||
|
if (!target || typeof target.blur !== "function") return;
|
||||||
|
var pt = e.pointerType;
|
||||||
|
if (pt === "mouse" || pt === "touch" || (!pt && e.detail > 0)) {
|
||||||
|
requestAnimationFrame(function() {
|
||||||
|
target.blur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dragHandle.addEventListener(
|
dragHandle.addEventListener(
|
||||||
"mousedown",
|
"mousedown",
|
||||||
(e) => {
|
(e) => {
|
||||||
runAction(
|
var dragAction = dragHandle.dataset.action;
|
||||||
e.target.dataset["action"],
|
runAction(dragAction, getKeyBindings(dragAction, "value"), e);
|
||||||
getKeyBindings(e.target.dataset["action"], "value"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1862,11 +2130,9 @@ function defineVideoController() {
|
|||||||
button.addEventListener(
|
button.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(e) => {
|
(e) => {
|
||||||
runAction(
|
var action = button.dataset.action;
|
||||||
e.target.dataset["action"],
|
runAction(action, getKeyBindings(action), e);
|
||||||
getKeyBindings(e.target.dataset["action"]),
|
blurAfterPointerTap(button, e);
|
||||||
e
|
|
||||||
);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1881,6 +2147,7 @@ function defineVideoController() {
|
|||||||
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
||||||
setSubtitleNudgeEnabledForVideo(video, newState);
|
setSubtitleNudgeEnabledForVideo(video, newState);
|
||||||
}
|
}
|
||||||
|
blurAfterPointerTap(subtitleNudgeIndicator, e);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1897,126 +2164,84 @@ function defineVideoController() {
|
|||||||
this.setupGenericAutoHide(wrapper);
|
this.setupGenericAutoHide(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fragment = doc.createDocumentFragment();
|
|
||||||
fragment.appendChild(wrapper);
|
|
||||||
const parentEl = this.parent || this.video.parentElement;
|
const parentEl = this.parent || this.video.parentElement;
|
||||||
|
var mountEl = getControllerMount(this.video) || parentEl;
|
||||||
|
|
||||||
log(`Inserting controller: parentEl=${!!parentEl}, parentNode=${!!parentEl?.parentNode}, hostname=${location.hostname}`, 4);
|
log(`Inserting controller: parentEl=${!!parentEl}, parentNode=${!!parentEl?.parentNode}, hostname=${location.hostname}`, 4);
|
||||||
|
|
||||||
if (!parentEl || !parentEl.parentNode) {
|
if (!parentEl || !parentEl.parentNode) {
|
||||||
log("No suitable parent found, appending to body", 4);
|
log("No suitable parent found, appending to body", 4);
|
||||||
doc.body.appendChild(fragment);
|
doc.body.appendChild(wrapper);
|
||||||
|
setupControllerHostTracking(this, wrapper, doc.body);
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case location.hostname == "www.amazon.com":
|
case location.hostname === "www.amazon.com":
|
||||||
case location.hostname == "www.reddit.com":
|
case location.hostname === "www.reddit.com":
|
||||||
case /hbogo\./.test(location.hostname):
|
case /hbogo\./.test(location.hostname):
|
||||||
log("Using parentElement.parentElement insertion", 5);
|
mountEl = parentEl.parentElement || mountEl;
|
||||||
parentEl.parentElement.insertBefore(fragment, parentEl);
|
|
||||||
break;
|
break;
|
||||||
case location.hostname == "www.facebook.com":
|
case location.hostname === "www.facebook.com":
|
||||||
log("Using Facebook-specific insertion", 5);
|
var facebookMount = parentEl;
|
||||||
let p =
|
for (var facebookDepth = 0; facebookDepth < 7; facebookDepth += 1) {
|
||||||
parentEl.parentElement.parentElement.parentElement.parentElement
|
if (!facebookMount.parentElement) break;
|
||||||
.parentElement.parentElement.parentElement;
|
facebookMount = facebookMount.parentElement;
|
||||||
if (p && p.firstChild) p.insertBefore(fragment, p.firstChild);
|
|
||||||
else parentEl.insertBefore(fragment, parentEl.firstChild);
|
|
||||||
break;
|
|
||||||
case location.hostname == "tv.apple.com":
|
|
||||||
log("Using Apple TV-specific insertion", 5);
|
|
||||||
const r = parentEl.getRootNode();
|
|
||||||
const s = r && r.querySelector ? r.querySelector(".scrim") : null;
|
|
||||||
if (s) s.prepend(fragment);
|
|
||||||
else parentEl.insertBefore(fragment, parentEl.firstChild);
|
|
||||||
break;
|
|
||||||
case location.hostname == "www.youtube.com":
|
|
||||||
case location.hostname == "m.youtube.com":
|
|
||||||
case location.hostname == "music.youtube.com":
|
|
||||||
// YouTube's player DOM has .html5-video-container (video's parent) as a
|
|
||||||
// low layer with overlay siblings (.ytp-player-content, etc.) on top that
|
|
||||||
// intercept mouse events. Insert into .html5-video-player (the player
|
|
||||||
// root) so the controller sits above all overlay layers.
|
|
||||||
log("Using YouTube-specific insertion", 5);
|
|
||||||
var ytPlayer = parentEl.closest(".html5-video-player");
|
|
||||||
if (ytPlayer) {
|
|
||||||
ytPlayer.insertBefore(fragment, ytPlayer.firstChild);
|
|
||||||
} else {
|
|
||||||
parentEl.insertBefore(fragment, parentEl.firstChild);
|
|
||||||
}
|
}
|
||||||
|
mountEl = facebookMount || mountEl;
|
||||||
|
break;
|
||||||
|
case location.hostname === "tv.apple.com":
|
||||||
|
var appleRoot = parentEl.getRootNode();
|
||||||
|
var appleScrim =
|
||||||
|
appleRoot && appleRoot.querySelector
|
||||||
|
? appleRoot.querySelector(".scrim")
|
||||||
|
: null;
|
||||||
|
mountEl = appleScrim || mountEl;
|
||||||
|
break;
|
||||||
|
case location.hostname === "www.youtube.com":
|
||||||
|
case location.hostname === "m.youtube.com":
|
||||||
|
case location.hostname === "music.youtube.com":
|
||||||
|
mountEl = parentEl.closest(".html5-video-player") || mountEl;
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
log("Using default insertion method", 5);
|
|
||||||
parentEl.insertBefore(fragment, parentEl.firstChild);
|
|
||||||
}
|
}
|
||||||
|
mountEl.insertBefore(wrapper, mountEl.firstChild);
|
||||||
|
setupControllerHostTracking(this, wrapper, mountEl);
|
||||||
log("Controller successfully inserted into DOM", 4);
|
log("Controller successfully inserted into DOM", 4);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Error inserting controller: ${error.message}`, 2);
|
log(`Error inserting controller: ${error.message}`, 2);
|
||||||
// Fallback to body insertion
|
// Fallback to body insertion
|
||||||
doc.body.appendChild(fragment);
|
doc.body.appendChild(wrapper);
|
||||||
|
setupControllerHostTracking(this, wrapper, doc.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeStringRegExp(str) {
|
|
||||||
const m = /[|\\{}()[\]^$+*?.]/g;
|
|
||||||
return str.replace(m, "\\$&");
|
|
||||||
}
|
|
||||||
function applySiteRuleOverrides() {
|
function applySiteRuleOverrides() {
|
||||||
resetSettingsFromSiteRuleBase();
|
resetSettingsFromSiteRuleBase();
|
||||||
|
tc.activeSiteRule = null;
|
||||||
|
|
||||||
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
|
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentUrl = location.href;
|
var currentUrl = location.href;
|
||||||
var matchedRule = null;
|
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
|
||||||
|
|
||||||
for (var i = 0; i < tc.settings.siteRules.length; i++) {
|
if (!matchedRule) {
|
||||||
var rule = tc.settings.siteRules[i];
|
return false;
|
||||||
var pattern = rule.pattern;
|
|
||||||
if (!pattern || pattern.length === 0) continue;
|
|
||||||
|
|
||||||
var regex;
|
|
||||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
var lastSlash = pattern.lastIndexOf("/");
|
|
||||||
regex = new RegExp(
|
|
||||||
pattern.substring(1, lastSlash),
|
|
||||||
pattern.substring(lastSlash + 1)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
log(`Invalid site rule regex: ${pattern}. ${e.message}`, 2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(escapeStringRegExp(pattern));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regex && regex.test(currentUrl)) {
|
|
||||||
matchedRule = rule;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matchedRule) return false;
|
|
||||||
|
|
||||||
tc.activeSiteRule = matchedRule;
|
tc.activeSiteRule = matchedRule;
|
||||||
log(`Matched site rule: ${matchedRule.pattern}`, 4);
|
log(`Matched site rule: ${matchedRule.pattern}`, 4);
|
||||||
|
|
||||||
// Check if extension should be enabled/disabled on this site
|
// Check if extension should be enabled/disabled on this site
|
||||||
if (matchedRule.enabled === false) {
|
if (siteRuleUtils.isSiteRuleDisabled(matchedRule)) {
|
||||||
log(`Extension disabled for site: ${currentUrl}`, 4);
|
log(`Extension disabled for site: ${currentUrl}`, 4);
|
||||||
return true;
|
return true;
|
||||||
} else if (matchedRule.disableExtension === true) {
|
|
||||||
// Handle old format
|
|
||||||
log(`Extension disabled (legacy) for site: ${currentUrl}`, 4);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override general settings with site-specific overrides
|
// Override general settings with site-specific overrides
|
||||||
@@ -2032,6 +2257,7 @@ function applySiteRuleOverrides() {
|
|||||||
"controllerMarginTop",
|
"controllerMarginTop",
|
||||||
"controllerMarginBottom",
|
"controllerMarginBottom",
|
||||||
"enableSubtitleNudge",
|
"enableSubtitleNudge",
|
||||||
|
"subtitleNudgeEnabledByDefault",
|
||||||
"subtitleNudgeInterval"
|
"subtitleNudgeInterval"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2045,7 +2271,7 @@ function applySiteRuleOverrides() {
|
|||||||
[
|
[
|
||||||
"controllerMarginTop",
|
"controllerMarginTop",
|
||||||
"controllerMarginBottom"
|
"controllerMarginBottom"
|
||||||
].forEach(function (key) {
|
].forEach(function(key) {
|
||||||
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2074,6 +2300,27 @@ function applySiteRuleOverrides() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply current tc.settings controller layout/opacity to every attached controller (after site rules). */
|
||||||
|
function refreshAllControllerGeometry() {
|
||||||
|
tc.mediaElements.forEach(function(video) {
|
||||||
|
if (!video || !video.vsc) return;
|
||||||
|
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
|
||||||
|
var controllerEl = getControllerElement(video.vsc);
|
||||||
|
if (controllerEl) {
|
||||||
|
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||||
|
function reapplySiteRulesAndControllerGeometry() {
|
||||||
|
applySiteRuleOverrides();
|
||||||
|
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAllControllerGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPreserveDesiredSpeed(video, speed) {
|
function shouldPreserveDesiredSpeed(video, speed) {
|
||||||
if (!video || !video.vsc) return false;
|
if (!video || !video.vsc) return false;
|
||||||
var desiredSpeed = getDesiredSpeed(video);
|
var desiredSpeed = getDesiredSpeed(video);
|
||||||
@@ -2091,8 +2338,12 @@ function shouldPreserveDesiredSpeed(video, speed) {
|
|||||||
function setupListener(root) {
|
function setupListener(root) {
|
||||||
root = root || document;
|
root = root || document;
|
||||||
if (root.vscRateListenerAttached) return;
|
if (root.vscRateListenerAttached) return;
|
||||||
function updateSpeedFromEvent(video) {
|
|
||||||
|
function updateSpeedFromEvent(video, skipResetDisarm) {
|
||||||
if (!video.vsc || !video.vsc.speedIndicator) return;
|
if (!video.vsc || !video.vsc.speedIndicator) return;
|
||||||
|
if (!skipResetDisarm) {
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
|
}
|
||||||
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
||||||
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
||||||
video.vsc.targetSpeed = speed;
|
video.vsc.targetSpeed = speed;
|
||||||
@@ -2108,9 +2359,10 @@ function setupListener(root) {
|
|||||||
else video.vsc.startSubtitleNudge();
|
else video.vsc.startSubtitleNudge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.addEventListener(
|
root.addEventListener(
|
||||||
"ratechange",
|
"ratechange",
|
||||||
function (event) {
|
function(event) {
|
||||||
if (tc.isNudging) return;
|
if (tc.isNudging) return;
|
||||||
var video = event.target;
|
var video = event.target;
|
||||||
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
||||||
@@ -2119,7 +2371,7 @@ function setupListener(root) {
|
|||||||
if (tc.settings.forceLastSavedSpeed) {
|
if (tc.settings.forceLastSavedSpeed) {
|
||||||
if (event.detail && event.detail.origin === "videoSpeed") {
|
if (event.detail && event.detail.origin === "videoSpeed") {
|
||||||
video.playbackRate = event.detail.speed;
|
video.playbackRate = event.detail.speed;
|
||||||
updateSpeedFromEvent(video);
|
updateSpeedFromEvent(video, true);
|
||||||
} else {
|
} else {
|
||||||
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
|
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
|
||||||
}
|
}
|
||||||
@@ -2130,7 +2382,7 @@ function setupListener(root) {
|
|||||||
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
||||||
|
|
||||||
if (pendingRateChange) {
|
if (pendingRateChange) {
|
||||||
updateSpeedFromEvent(video);
|
updateSpeedFromEvent(video, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2139,6 +2391,7 @@ function setupListener(root) {
|
|||||||
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
|
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
||||||
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
|
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
|
||||||
return;
|
return;
|
||||||
@@ -2152,7 +2405,6 @@ function setupListener(root) {
|
|||||||
root.vscRateListenerAttached = true;
|
root.vscRateListenerAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var vscInitializedDocuments = new Set();
|
|
||||||
function clearPendingInitialization(doc) {
|
function clearPendingInitialization(doc) {
|
||||||
if (!doc || !doc.vscPendingInitializeHandler) return;
|
if (!doc || !doc.vscPendingInitializeHandler) return;
|
||||||
|
|
||||||
@@ -2189,7 +2441,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
|||||||
|
|
||||||
if (doc.vscPendingInitializeHandler) return;
|
if (doc.vscPendingInitializeHandler) return;
|
||||||
|
|
||||||
var pendingInitializeHandler = function () {
|
var pendingInitializeHandler = function() {
|
||||||
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2204,6 +2456,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
|||||||
setTimeout(pendingInitializeHandler, 0);
|
setTimeout(pendingInitializeHandler, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inIframe() {
|
function inIframe() {
|
||||||
try {
|
try {
|
||||||
return window.self !== window.top;
|
return window.self !== window.top;
|
||||||
@@ -2216,13 +2469,14 @@ function attachKeydownListeners(doc) {
|
|||||||
var docs = [doc];
|
var docs = [doc];
|
||||||
try {
|
try {
|
||||||
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
|
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
docs.forEach(function (keyDoc) {
|
docs.forEach(function(keyDoc) {
|
||||||
if (keyDoc.vscKeydownListenerAttached) return;
|
if (keyDoc.vscKeydownListenerAttached) return;
|
||||||
keyDoc.addEventListener(
|
keyDoc.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
function (event) {
|
function(event) {
|
||||||
if (
|
if (
|
||||||
!event.getModifierState ||
|
!event.getModifierState ||
|
||||||
event.getModifierState("Alt") ||
|
event.getModifierState("Alt") ||
|
||||||
@@ -2245,7 +2499,7 @@ function attachKeydownListeners(doc) {
|
|||||||
|
|
||||||
if (!tc.mediaElements.length) return;
|
if (!tc.mediaElements.length) return;
|
||||||
|
|
||||||
var item = tc.settings.keyBindings.find(function (binding) {
|
var item = tc.settings.keyBindings.find(function(binding) {
|
||||||
return matchesKeyBinding(binding, event);
|
return matchesKeyBinding(binding, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2269,24 +2523,24 @@ function attachMutationObserver(root) {
|
|||||||
|
|
||||||
var pendingMutations = [];
|
var pendingMutations = [];
|
||||||
var mutationProcessingScheduled = false;
|
var mutationProcessingScheduled = false;
|
||||||
var observer = new MutationObserver(function (mutations) {
|
var observer = new MutationObserver(function(mutations) {
|
||||||
pendingMutations.push(...mutations);
|
pendingMutations.push(...mutations);
|
||||||
if (mutationProcessingScheduled) return;
|
if (mutationProcessingScheduled) return;
|
||||||
|
|
||||||
mutationProcessingScheduled = true;
|
mutationProcessingScheduled = true;
|
||||||
requestIdle(
|
requestIdle(
|
||||||
function () {
|
function() {
|
||||||
var mutationsToProcess = pendingMutations.splice(0);
|
var mutationsToProcess = pendingMutations.splice(0);
|
||||||
mutationProcessingScheduled = false;
|
mutationProcessingScheduled = false;
|
||||||
|
|
||||||
mutationsToProcess.forEach(function (mutation) {
|
mutationsToProcess.forEach(function(mutation) {
|
||||||
if (mutation.type === "childList") {
|
if (mutation.type === "childList") {
|
||||||
mutation.addedNodes.forEach(function (node) {
|
mutation.addedNodes.forEach(function(node) {
|
||||||
// Skip text nodes, comments, etc. — only elements can contain media
|
// Skip text nodes, comments, etc. — only elements can contain media
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||||
scanNodeForMedia(node, node.parentNode || mutation.target, true);
|
scanNodeForMedia(node, node.parentNode || mutation.target, true);
|
||||||
});
|
});
|
||||||
mutation.removedNodes.forEach(function (node) {
|
mutation.removedNodes.forEach(function(node) {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||||
scanNodeForMedia(node, node.parentNode || mutation.target, false);
|
scanNodeForMedia(node, node.parentNode || mutation.target, false);
|
||||||
});
|
});
|
||||||
@@ -2335,7 +2589,7 @@ function attachMutationObserver(root) {
|
|||||||
function attachMediaDetectionListeners(root) {
|
function attachMediaDetectionListeners(root) {
|
||||||
if (root.vscMediaEventListenersAttached) return;
|
if (root.vscMediaEventListenersAttached) return;
|
||||||
|
|
||||||
var handleDetectedMedia = function (event) {
|
var handleDetectedMedia = function(event) {
|
||||||
var target = event.target;
|
var target = event.target;
|
||||||
if (!isMediaElement(target)) return;
|
if (!isMediaElement(target)) return;
|
||||||
ensureController(target, target.parentElement || target.parentNode);
|
ensureController(target, target.parentElement || target.parentNode);
|
||||||
@@ -2348,21 +2602,22 @@ function attachMediaDetectionListeners(root) {
|
|||||||
"canplay",
|
"canplay",
|
||||||
"playing",
|
"playing",
|
||||||
"play"
|
"play"
|
||||||
].forEach(function (eventName) {
|
].forEach(function(eventName) {
|
||||||
root.addEventListener(eventName, handleDetectedMedia, true);
|
root.addEventListener(eventName, handleDetectedMedia, true);
|
||||||
});
|
});
|
||||||
root.vscMediaEventListenersAttached = true;
|
root.vscMediaEventListenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachIframeListeners(doc) {
|
function attachIframeListeners(doc) {
|
||||||
Array.from(doc.getElementsByTagName("iframe")).forEach(function (frame) {
|
Array.from(doc.getElementsByTagName("iframe")).forEach(function(frame) {
|
||||||
if (!frame.vscLoadListenerAttached) {
|
if (!frame.vscLoadListenerAttached) {
|
||||||
frame.addEventListener("load", function () {
|
frame.addEventListener("load", function() {
|
||||||
try {
|
try {
|
||||||
if (frame.contentDocument) {
|
if (frame.contentDocument) {
|
||||||
initializeWhenReady(frame.contentDocument, true);
|
initializeWhenReady(frame.contentDocument, true);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
frame.vscLoadListenerAttached = true;
|
frame.vscLoadListenerAttached = true;
|
||||||
}
|
}
|
||||||
@@ -2371,24 +2626,25 @@ function attachIframeListeners(doc) {
|
|||||||
if (frame.contentDocument) {
|
if (frame.contentDocument) {
|
||||||
initializeWhenReady(frame.contentDocument);
|
initializeWhenReady(frame.contentDocument);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachNavigationListeners() {
|
function attachNavigationListeners() {
|
||||||
if (window.vscNavigationListenersAttached) return;
|
if (window.vscNavigationListenersAttached) return;
|
||||||
|
|
||||||
var scheduleRescan = function () {
|
var scheduleRescan = function() {
|
||||||
clearTimeout(window.vscNavigationRescanTimer);
|
clearTimeout(window.vscNavigationRescanTimer);
|
||||||
window.vscNavigationRescanTimer = setTimeout(function () {
|
window.vscNavigationRescanTimer = setTimeout(function() {
|
||||||
initializeWhenReady(document, true);
|
initializeWhenReady(document, true);
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
["pushState", "replaceState"].forEach(function (method) {
|
["pushState", "replaceState"].forEach(function(method) {
|
||||||
if (typeof history[method] !== "function") return;
|
if (typeof history[method] !== "function") return;
|
||||||
var original = history[method];
|
var original = history[method];
|
||||||
history[method] = function () {
|
history[method] = function() {
|
||||||
var result = original.apply(this, arguments);
|
var result = original.apply(this, arguments);
|
||||||
scheduleRescan();
|
scheduleRescan();
|
||||||
return result;
|
return result;
|
||||||
@@ -2397,14 +2653,20 @@ function attachNavigationListeners() {
|
|||||||
|
|
||||||
window.addEventListener("popstate", scheduleRescan);
|
window.addEventListener("popstate", scheduleRescan);
|
||||||
window.addEventListener("hashchange", scheduleRescan);
|
window.addEventListener("hashchange", scheduleRescan);
|
||||||
|
/* YouTube often navigates without a history API call the extension can see first */
|
||||||
|
if (typeof document !== "undefined" && isOnYouTube()) {
|
||||||
|
document.addEventListener("yt-navigate-finish", scheduleRescan);
|
||||||
|
}
|
||||||
window.vscNavigationListenersAttached = true;
|
window.vscNavigationListenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeNow(doc, forceReinit = false) {
|
function initializeNow(doc, forceReinit = false) {
|
||||||
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
|
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
|
||||||
|
|
||||||
var siteDisabled = applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
if (!tc.settings.enabled || siteDisabled) return;
|
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!doc.body.classList.contains("vsc-initialized")) {
|
if (!doc.body.classList.contains("vsc-initialized")) {
|
||||||
doc.body.classList.add("vsc-initialized");
|
doc.body.classList.add("vsc-initialized");
|
||||||
@@ -2414,22 +2676,39 @@ function initializeNow(doc, forceReinit = false) {
|
|||||||
attachNavigationListeners();
|
attachNavigationListeners();
|
||||||
observeRoot(doc);
|
observeRoot(doc);
|
||||||
|
|
||||||
|
// Delayed rescan to catch custom elements whose shadow roots were created
|
||||||
|
// before our attachShadow patch was installed (e.g. archive.org's <play-av>
|
||||||
|
// Lit component, or any site that lazily creates shadow DOM video players).
|
||||||
|
if (!doc.vscDelayedShadowScanDone) {
|
||||||
|
doc.vscDelayedShadowScanDone = true;
|
||||||
|
setTimeout(function() {
|
||||||
|
if (!doc.body) return;
|
||||||
|
try {
|
||||||
|
var els = doc.body.querySelectorAll("*");
|
||||||
|
for (var i = 0; i < els.length; i++) {
|
||||||
|
if (els[i].shadowRoot && !vscObservedRoots.has(els[i].shadowRoot)) {
|
||||||
|
observeRoot(els[i].shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
if (forceReinit) {
|
if (forceReinit) {
|
||||||
log("Force re-initialization requested", 4);
|
log("Force re-initialization requested", 4);
|
||||||
tc.mediaElements.forEach(function (video) {
|
refreshAllControllerGeometry();
|
||||||
if (!video || !video.vsc) return;
|
|
||||||
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
|
|
||||||
var controllerEl = getControllerElement(video.vsc);
|
|
||||||
if (controllerEl) {
|
|
||||||
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vscInitializedDocuments.add(doc);
|
vscInitializedDocuments.add(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
function setSpeed(
|
||||||
|
video,
|
||||||
|
speed,
|
||||||
|
isInitialCall = false,
|
||||||
|
isUserKeyPress = false,
|
||||||
|
fromResetSpeedToggle = false
|
||||||
|
) {
|
||||||
const numericSpeed = Number(speed);
|
const numericSpeed = Number(speed);
|
||||||
|
|
||||||
if (!isValidSpeed(numericSpeed)) {
|
if (!isValidSpeed(numericSpeed)) {
|
||||||
@@ -2442,6 +2721,10 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
|||||||
|
|
||||||
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
|
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
|
||||||
|
|
||||||
|
if (isUserKeyPress && !fromResetSpeedToggle) {
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
|
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
|
||||||
4
|
4
|
||||||
@@ -2528,7 +2811,7 @@ function runAction(action, value, e) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaTagsToProcess.forEach(function (v) {
|
mediaTagsToProcess.forEach(function(v) {
|
||||||
if (!v.vsc) return; // Don't process videos without a controller
|
if (!v.vsc) return; // Don't process videos without a controller
|
||||||
var controller = v.vsc.div;
|
var controller = v.vsc.div;
|
||||||
const userDrivenActionsThatShowController = [
|
const userDrivenActionsThatShowController = [
|
||||||
@@ -2536,6 +2819,8 @@ function runAction(action, value, e) {
|
|||||||
"advance",
|
"advance",
|
||||||
"faster",
|
"faster",
|
||||||
"slower",
|
"slower",
|
||||||
|
"louder",
|
||||||
|
"softer",
|
||||||
"reset",
|
"reset",
|
||||||
"fast",
|
"fast",
|
||||||
"move",
|
"move",
|
||||||
@@ -2544,10 +2829,18 @@ function runAction(action, value, e) {
|
|||||||
"mark",
|
"mark",
|
||||||
"jump",
|
"jump",
|
||||||
"drag",
|
"drag",
|
||||||
|
"nudge",
|
||||||
"toggleSubtitleNudge",
|
"toggleSubtitleNudge",
|
||||||
"display"
|
"display"
|
||||||
];
|
];
|
||||||
if (userDrivenActionsThatShowController.includes(action) && action !== "display") {
|
var subtitleNudgeActionBlocked =
|
||||||
|
(action === "toggleSubtitleNudge" || action === "nudge") &&
|
||||||
|
!isSubtitleNudgeAvailableForVideo(v);
|
||||||
|
if (
|
||||||
|
userDrivenActionsThatShowController.includes(action) &&
|
||||||
|
action !== "display" &&
|
||||||
|
!subtitleNudgeActionBlocked
|
||||||
|
) {
|
||||||
showController(controller, 2000, true);
|
showController(controller, 2000, true);
|
||||||
}
|
}
|
||||||
if (v.classList.contains("vsc-cancelled")) return;
|
if (v.classList.contains("vsc-cancelled")) return;
|
||||||
@@ -2650,6 +2943,12 @@ function runAction(action, value, e) {
|
|||||||
case "muted":
|
case "muted":
|
||||||
muted(v);
|
muted(v);
|
||||||
break;
|
break;
|
||||||
|
case "louder":
|
||||||
|
volumeUp(v, Number.isFinite(numValue) ? numValue : 0.1);
|
||||||
|
break;
|
||||||
|
case "softer":
|
||||||
|
volumeDown(v, Number.isFinite(numValue) ? numValue : 0.1);
|
||||||
|
break;
|
||||||
case "mark":
|
case "mark":
|
||||||
setMark(v);
|
setMark(v);
|
||||||
break;
|
break;
|
||||||
@@ -2659,6 +2958,12 @@ function runAction(action, value, e) {
|
|||||||
case "toggleSubtitleNudge":
|
case "toggleSubtitleNudge":
|
||||||
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
|
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
|
||||||
break;
|
break;
|
||||||
|
case "nudge":
|
||||||
|
setSubtitleNudgeEnabledForVideo(
|
||||||
|
v,
|
||||||
|
!isSubtitleNudgeEnabledForVideo(v)
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
log("runAction End", 5);
|
log("runAction End", 5);
|
||||||
@@ -2697,27 +3002,73 @@ function resetSpeed(v, target, isFastKey = false) {
|
|||||||
Math.abs(lastToggle - 1.0) < 0.01
|
Math.abs(lastToggle - 1.0) < 0.01
|
||||||
? getKeyBindings("fast") || 1.8
|
? getKeyBindings("fast") || 1.8
|
||||||
: lastToggle;
|
: lastToggle;
|
||||||
setSpeed(v, speedToRestore, false, true);
|
setSpeed(v, speedToRestore, false, true, true);
|
||||||
} else {
|
} else {
|
||||||
// Not at 1.0, save current as toggle speed and go to 1.0
|
// Not at 1.0, save current as toggle speed and go to 1.0
|
||||||
lastToggleSpeed[videoId] = currentSpeed;
|
lastToggleSpeed[videoId] = currentSpeed;
|
||||||
setSpeed(v, resetSpeedValue, false, true);
|
v.vsc.resetToggleArmed = true;
|
||||||
|
setSpeed(v, resetSpeedValue, false, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function muted(v) {
|
function muted(v) {
|
||||||
v.muted = !v.muted;
|
var nextMuted = !v.muted;
|
||||||
|
v.muted = nextMuted;
|
||||||
|
if (!isOnYouTube()) return;
|
||||||
|
var ytApi = getYouTubePlayerApi(v);
|
||||||
|
if (!ytApi) return;
|
||||||
|
if (nextMuted && typeof ytApi.mute === "function") ytApi.mute();
|
||||||
|
if (!nextMuted && typeof ytApi.unMute === "function") ytApi.unMute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getYouTubePlayerApi(video) {
|
||||||
|
if (!isOnYouTube()) return null;
|
||||||
|
var playerEl =
|
||||||
|
(video && video.closest ? video.closest(".html5-video-player") : null) ||
|
||||||
|
document.getElementById("movie_player") ||
|
||||||
|
document.querySelector(".html5-video-player");
|
||||||
|
if (!playerEl) return null;
|
||||||
|
return playerEl.wrappedJSObject || playerEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncYouTubePlayerVolume(video, volume) {
|
||||||
|
var ytApi = getYouTubePlayerApi(video);
|
||||||
|
if (!ytApi || typeof ytApi.setVolume !== "function") return;
|
||||||
|
ytApi.setVolume(Math.round(volume * 100));
|
||||||
|
if (volume > 0 && typeof ytApi.unMute === "function") {
|
||||||
|
ytApi.unMute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVideoVolume(video, targetVolume) {
|
||||||
|
var nextVolume = Math.max(0, Math.min(1, Number(targetVolume.toFixed(2))));
|
||||||
|
video.volume = nextVolume;
|
||||||
|
if (nextVolume > 0 && video.muted) {
|
||||||
|
video.muted = false;
|
||||||
|
}
|
||||||
|
syncYouTubePlayerVolume(video, nextVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeUp(v, value) {
|
||||||
|
setVideoVolume(v, v.volume + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeDown(v, value) {
|
||||||
|
setVideoVolume(v, v.volume - value);
|
||||||
|
}
|
||||||
|
|
||||||
function setMark(v) {
|
function setMark(v) {
|
||||||
v.vsc.mark = v.currentTime;
|
v.vsc.mark = v.currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpToMark(v) {
|
function jumpToMark(v) {
|
||||||
if (v.vsc && typeof v.vsc.mark === "number") {
|
if (v.vsc && typeof v.vsc.mark === "number") {
|
||||||
extendSpeedRestoreWindow(v);
|
extendSpeedRestoreWindow(v);
|
||||||
v.currentTime = v.vsc.mark;
|
v.currentTime = v.vsc.mark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrag(video, e) {
|
function handleDrag(video, e) {
|
||||||
const c = video.vsc.div;
|
const c = video.vsc.div;
|
||||||
const sC = convertControllerToManualPosition(video.vsc);
|
const sC = convertControllerToManualPosition(video.vsc);
|
||||||
@@ -2727,7 +3078,7 @@ function handleDrag(video, e) {
|
|||||||
pE.parentNode &&
|
pE.parentNode &&
|
||||||
pE.parentNode.offsetHeight === pE.offsetHeight &&
|
pE.parentNode.offsetHeight === pE.offsetHeight &&
|
||||||
pE.parentNode.offsetWidth === pE.offsetWidth
|
pE.parentNode.offsetWidth === pE.offsetWidth
|
||||||
)
|
)
|
||||||
pE = pE.parentNode;
|
pE = pE.parentNode;
|
||||||
video.classList.add("vcs-dragging");
|
video.classList.add("vcs-dragging");
|
||||||
sC.classList.add("dragging");
|
sC.classList.add("dragging");
|
||||||
@@ -2756,6 +3107,7 @@ function handleDrag(video, e) {
|
|||||||
pE.addEventListener("mouseleave", eD);
|
pE.addEventListener("mouseleave", eD);
|
||||||
pE.addEventListener("mousemove", sD);
|
pE.addEventListener("mousemove", sD);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showController(controller, duration = 2000, forced = false) {
|
function showController(controller, duration = 2000, forced = false) {
|
||||||
if (!controller || typeof controller.classList === "undefined") return;
|
if (!controller || typeof controller.classList === "undefined") return;
|
||||||
var restoreHidden =
|
var restoreHidden =
|
||||||
@@ -2773,7 +3125,7 @@ function showController(controller, duration = 2000, forced = false) {
|
|||||||
clearTimeout(controller.showTimeOut);
|
clearTimeout(controller.showTimeOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.showTimeOut = setTimeout(function () {
|
controller.showTimeOut = setTimeout(function() {
|
||||||
controller.classList.remove("vsc-show");
|
controller.classList.remove("vsc-show");
|
||||||
controller.classList.remove("vsc-forced-show");
|
controller.classList.remove("vsc-forced-show");
|
||||||
if (controller.restoreHiddenAfterShow === true) {
|
if (controller.restoreHiddenAfterShow === true) {
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: 2147483647 !important;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.vsc-nosource),
|
||||||
|
:host(.vsc-hidden) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.ytp-autohide:not(.vsc-hidden)),
|
||||||
|
:host(.vsc-idle-hidden:not(.vsc-hidden)) {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.ytp-autohide.vsc-show:not(.vsc-hidden)),
|
||||||
|
:host(.vsc-idle-hidden.vsc-show:not(.vsc-hidden)),
|
||||||
|
:host(.vsc-forced-show:not(.vsc-hidden)) {
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
line-height: 1.9em;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global * uses 1.9em line-height; without this, every node inside #controller
|
||||||
|
(including svg) keeps a tall line box and the bar grows + content rides high. */
|
||||||
|
#controller * {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show extra buttons on hover or keyboard :focus-visible only. Plain :focus-within
|
||||||
|
after a mouse click kept #controls visible while hover-only rules (e.g. draggable
|
||||||
|
margin) turned off when the pointer left the bar. */
|
||||||
|
#controller:hover #controls,
|
||||||
|
#controller:focus-within:has(:focus-visible) #controls,
|
||||||
|
:host(:hover) #controls {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controller {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
/* Stay above the local video surface without overtaking page-level dialogs. */
|
||||||
|
z-index: 1;
|
||||||
|
transition: top 160ms ease, left 160ms ease, transform 160ms ease,
|
||||||
|
opacity 160ms ease;
|
||||||
|
/* Insets are baked into left/top; bar must not wrap when narrow. Hover controls
|
||||||
|
extend into the inset area (past “logical” margin). */
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controller:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Space between speed readout and hover buttons — tweak this value (px) as you like */
|
||||||
|
#controller:hover > .draggable {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */
|
||||||
|
#controller[data-location="top-center"]:not([data-position-mode="manual"]) {
|
||||||
|
transform: translate(-50%, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controller[data-location="bottom-center"]:not([data-position-mode="manual"]) {
|
||||||
|
transform: translate(-50%, -100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standalone flash next to speed when N is pressed — hidden = no layout footprint */
|
||||||
|
#nudge-flash-indicator {
|
||||||
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: "Lucida Console", Monaco, monospace;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Same 24×24 footprint as #controls button */
|
||||||
|
#nudge-flash-indicator.visible {
|
||||||
|
display: inline-flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-width: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide flash indicator when hovering — the one in #controls is visible instead */
|
||||||
|
#controller:hover #nudge-flash-indicator,
|
||||||
|
:host(:hover) #nudge-flash-indicator {
|
||||||
|
display: none !important;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator[data-enabled="true"] {
|
||||||
|
color: #fff;
|
||||||
|
background: #4b9135;
|
||||||
|
border-color: #6ec754;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator[data-enabled="false"] {
|
||||||
|
color: #fff;
|
||||||
|
background: #943e3e;
|
||||||
|
border-color: #c06060;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Same 24×24 chip as control buttons (Lucide check / x inside) */
|
||||||
|
#nudge-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-indicator[data-enabled="true"] {
|
||||||
|
color: #fff;
|
||||||
|
background: #4b9135;
|
||||||
|
border-color: #6ec754;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-indicator[data-enabled="false"] {
|
||||||
|
color: #fff;
|
||||||
|
background: #943e3e;
|
||||||
|
border-color: #c06060;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-indicator[data-supported="false"] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator.visible .vsc-btn-icon,
|
||||||
|
#nudge-indicator .vsc-btn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator.visible .vsc-btn-icon svg,
|
||||||
|
#nudge-indicator .vsc-btn-icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: translateY(0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#controller.dragging {
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controller.dragging #controls {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable:active {
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
background: white;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px 6px 3px 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
border: 1px solid white;
|
||||||
|
font-family: "Lucida Console", Monaco, monospace;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon buttons: square targets, compact bar (no extra vertical stretch). */
|
||||||
|
#controls button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .vsc-btn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .vsc-btn-icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Lucide 24×24 paths sit slightly high in the viewBox */
|
||||||
|
transform: translateY(0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.rw {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hideButton {
|
||||||
|
opacity: 0.65;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "Speeder",
|
||||||
|
"short_name": "Speeder",
|
||||||
|
"version": "5.3.3.0",
|
||||||
|
"manifest_version": 2,
|
||||||
|
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
||||||
|
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}",
|
||||||
|
"data_collection_permissions": {
|
||||||
|
"required": [
|
||||||
|
"none"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icons/icon16.png",
|
||||||
|
"48": "assets/icons/icon48.png",
|
||||||
|
"128": "assets/icons/icon128.png"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"scripts": [
|
||||||
|
"background/background.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"https://cdn.jsdelivr.net/*"
|
||||||
|
],
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": {
|
||||||
|
"19": "assets/icons/icon19.png",
|
||||||
|
"38": "assets/icons/icon38.png",
|
||||||
|
"48": "assets/icons/icon48.png"
|
||||||
|
},
|
||||||
|
"default_popup": "popup/popup.html"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"all_frames": true,
|
||||||
|
"matches": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*",
|
||||||
|
"file:///*"
|
||||||
|
],
|
||||||
|
"match_about_blank": true,
|
||||||
|
"exclude_matches": [
|
||||||
|
"https://plus.google.com/hangouts/*",
|
||||||
|
"https://hangouts.google.com/*",
|
||||||
|
"https://meet.google.com/*"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"content/inject.css"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"shared/controller-utils.js",
|
||||||
|
"shared/key-bindings.js",
|
||||||
|
"shared/site-rules.js",
|
||||||
|
"shared/ui-icons.js",
|
||||||
|
"content/inject.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"content/inject.css",
|
||||||
|
"content/shadow.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Import/Export functionality for Video Speed Controller settings
|
||||||
|
var speederShared =
|
||||||
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
|
var importExportUtils = speederShared.importExport || {};
|
||||||
|
|
||||||
|
function generateBackupFilename() {
|
||||||
|
return importExportUtils.generateBackupFilename(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSettings() {
|
||||||
|
chrome.storage.sync.get(null, function (storage) {
|
||||||
|
chrome.storage.local.get(null, function (localStorage) {
|
||||||
|
const backup = importExportUtils.buildBackupPayload(
|
||||||
|
storage,
|
||||||
|
importExportUtils.filterLocalSettingsForExport(localStorage),
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataStr = JSON.stringify(backup, null, 2);
|
||||||
|
const blob = new Blob([dataStr], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = generateBackupFilename();
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showStatus("Settings exported successfully");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function importSettings() {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = ".json";
|
||||||
|
|
||||||
|
input.onchange = function (event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
try {
|
||||||
|
const parsedBackup = importExportUtils.parseImportText(e.target.result);
|
||||||
|
|
||||||
|
if (!parsedBackup) {
|
||||||
|
showStatus("Error: Invalid backup file format", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsToImport = parsedBackup.settings;
|
||||||
|
var localToImport = parsedBackup.localSettings;
|
||||||
|
|
||||||
|
function importLocalSettings(callback) {
|
||||||
|
if (parsedBackup.isWrappedBackup !== true) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.local.clear(function () {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showStatus(
|
||||||
|
"Error: Failed to clear local extension data - " +
|
||||||
|
chrome.runtime.lastError.message,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localToImport && Object.keys(localToImport).length > 0) {
|
||||||
|
chrome.storage.local.set(localToImport, function () {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showStatus(
|
||||||
|
"Error: Failed to save local extension data - " +
|
||||||
|
chrome.runtime.lastError.message,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterLocalImport() {
|
||||||
|
chrome.storage.sync.clear(function () {
|
||||||
|
chrome.storage.sync.set(settingsToImport, function () {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showStatus(
|
||||||
|
"Error: Failed to save imported settings - " +
|
||||||
|
chrome.runtime.lastError.message,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showStatus("Settings imported successfully. Reloading...");
|
||||||
|
setTimeout(function () {
|
||||||
|
if (typeof restore_options === "function") {
|
||||||
|
restore_options();
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
importLocalSettings(afterLocalImport);
|
||||||
|
} catch (err) {
|
||||||
|
showStatus("Error: Failed to parse backup file - " + err.message, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = function () {
|
||||||
|
showStatus("Error: Failed to read file", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(message, isError = false) {
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
if (status) {
|
||||||
|
status.textContent = message;
|
||||||
|
status.style.color = isError ? "#d32f2f" : "";
|
||||||
|
setTimeout(function () {
|
||||||
|
status.textContent = "";
|
||||||
|
status.style.color = "";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize import/export buttons when DOM is ready
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", initImportExport);
|
||||||
|
} else {
|
||||||
|
initImportExport();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initImportExport() {
|
||||||
|
const exportBtn = document.getElementById("exportSettings");
|
||||||
|
const importBtn = document.getElementById("importSettings");
|
||||||
|
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.addEventListener("click", exportSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importBtn) {
|
||||||
|
importBtn.addEventListener("click", importSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Lucide static icons via jsDelivr (same registry as lucide.dev).
|
||||||
|
* ISC License — https://lucide.dev
|
||||||
|
*/
|
||||||
|
var LUCIDE_STATIC_VERSION = "1.7.0";
|
||||||
|
var LUCIDE_CDN_BASE =
|
||||||
|
"https://cdn.jsdelivr.net/npm/lucide-static@" +
|
||||||
|
LUCIDE_STATIC_VERSION;
|
||||||
|
|
||||||
|
var LUCIDE_TAGS_CACHE_KEY = "lucideTagsCacheV1";
|
||||||
|
var LUCIDE_TAGS_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 7; /* 7 days */
|
||||||
|
|
||||||
|
function lucideIconSvgUrl(slug) {
|
||||||
|
if (!slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(slug)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return LUCIDE_CDN_BASE + "/icons/" + slug.toLowerCase() + ".svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
function lucideTagsJsonUrl() {
|
||||||
|
return LUCIDE_CDN_BASE + "/tags.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse whitespace for smaller storage. */
|
||||||
|
function lucideMinifySvg(s) {
|
||||||
|
return String(s).replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLucideSvg(svgText) {
|
||||||
|
if (!svgText || typeof svgText !== "string") return null;
|
||||||
|
var t = String(svgText).replace(/\0/g, "").trim();
|
||||||
|
if (!/<svg[\s>]/i.test(t)) return null;
|
||||||
|
var doc = new DOMParser().parseFromString(t, "image/svg+xml");
|
||||||
|
if (doc.querySelector("parsererror")) return null;
|
||||||
|
var svg = vscSanitizeSvgTree(doc.querySelector("svg"));
|
||||||
|
if (!svg) return null;
|
||||||
|
svg.removeAttribute("width");
|
||||||
|
svg.removeAttribute("height");
|
||||||
|
svg.setAttribute("width", "100%");
|
||||||
|
svg.setAttribute("height", "100%");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
return lucideMinifySvg(svg.outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchLucideSvg(slug) {
|
||||||
|
var url = lucideIconSvgUrl(slug);
|
||||||
|
if (!url) {
|
||||||
|
return Promise.reject(new Error("Invalid icon name"));
|
||||||
|
}
|
||||||
|
return fetch(url, { cache: "force-cache" }).then(function (r) {
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new Error("Icon not found: " + slug);
|
||||||
|
}
|
||||||
|
return r.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchAndCacheLucideTags(chromeLocal, resolve, reject) {
|
||||||
|
fetch(lucideTagsJsonUrl(), { cache: "force-cache" })
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok) throw new Error("tags.json HTTP " + r.status);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (obj) {
|
||||||
|
var payload = {};
|
||||||
|
payload[LUCIDE_TAGS_CACHE_KEY] = obj;
|
||||||
|
payload[LUCIDE_TAGS_CACHE_KEY + "At"] = Date.now();
|
||||||
|
if (chromeLocal && chromeLocal.set) {
|
||||||
|
chromeLocal.set(payload, function () {
|
||||||
|
resolve(obj);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(obj);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<Object<string, string[]>>} slug -> tags */
|
||||||
|
function getLucideTagsMap(chromeLocal, bypassCache) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!chromeLocal || !chromeLocal.get) {
|
||||||
|
fetch(lucideTagsJsonUrl(), { cache: "force-cache" })
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bypassCache) {
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chromeLocal.get(
|
||||||
|
[LUCIDE_TAGS_CACHE_KEY, LUCIDE_TAGS_CACHE_KEY + "At"],
|
||||||
|
function (stored) {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = stored[LUCIDE_TAGS_CACHE_KEY];
|
||||||
|
var at = stored[LUCIDE_TAGS_CACHE_KEY + "At"];
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === "object" &&
|
||||||
|
at &&
|
||||||
|
Date.now() - at < LUCIDE_TAGS_MAX_AGE_MS
|
||||||
|
) {
|
||||||
|
resolve(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object<string,string[]>} tagsMap
|
||||||
|
* @param {string} query
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {string[]} slugs
|
||||||
|
*/
|
||||||
|
function searchLucideSlugs(tagsMap, query, limit) {
|
||||||
|
var lim = limit != null ? limit : 60;
|
||||||
|
var q = String(query || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
if (!tagsMap || !q) return [];
|
||||||
|
var matches = [];
|
||||||
|
for (var slug in tagsMap) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(tagsMap, slug)) continue;
|
||||||
|
var hay =
|
||||||
|
slug +
|
||||||
|
" " +
|
||||||
|
(Array.isArray(tagsMap[slug]) ? tagsMap[slug].join(" ") : "");
|
||||||
|
if (hay.toLowerCase().indexOf(q) === -1) continue;
|
||||||
|
matches.push(slug);
|
||||||
|
}
|
||||||
|
matches.sort(function (a, b) {
|
||||||
|
var al = a.toLowerCase();
|
||||||
|
var bl = b.toLowerCase();
|
||||||
|
var ap = al.indexOf(q) === 0 ? 0 : 1;
|
||||||
|
var bp = bl.indexOf(q) === 0 ? 0 : 1;
|
||||||
|
if (ap !== bp) return ap - bp;
|
||||||
|
return al.localeCompare(bl);
|
||||||
|
});
|
||||||
|
return matches.slice(0, lim);
|
||||||
|
}
|
||||||
@@ -0,0 +1,1301 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #f4f5f7;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--panel-subtle: #fafbfc;
|
||||||
|
--border: #e2e5e9;
|
||||||
|
--border-strong: #d4d9e0;
|
||||||
|
--text: #17191c;
|
||||||
|
--muted: #626b76;
|
||||||
|
--accent: #111827;
|
||||||
|
--switch-track-off: #c1cad6;
|
||||||
|
--switch-track-off-border: #aeb8c5;
|
||||||
|
--switch-track-on: #111827;
|
||||||
|
--switch-track-on-border: #111827;
|
||||||
|
--switch-thumb-off: #ffffff;
|
||||||
|
--switch-thumb-on: #ffffff;
|
||||||
|
--toggle-open-fg: #111827;
|
||||||
|
--toggle-open-bg: #eef2f6;
|
||||||
|
--toggle-open-border: #c5ccd5;
|
||||||
|
--toggle-open-hover-bg: #e4eaf1;
|
||||||
|
--toggle-open-hover-border: #b5c0cc;
|
||||||
|
--danger: #b42318;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* Avoid coupling to the browser viewport: embedded options (e.g. Add-ons
|
||||||
|
* Manager iframe) must size to content, not 100vh, or a large empty band
|
||||||
|
* appears below the page. */
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 24px 16px 40px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font: 14px/1.45 "Avenir Next", "SF Pro Text", "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
width: min(880px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Avenir Next", "SF Pro Display", "Segoe UI", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle,
|
||||||
|
.section-intro {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bars-group {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bars-inner {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-nested {
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major .section-intro {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaults-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 14px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaults-sub-heading {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration-color: #c6ccd5;
|
||||||
|
text-underline-offset: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.08em 0.38em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
appearance: none;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-color: #c5ccd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
input[type="checkbox"]:focus-visible,
|
||||||
|
input[type="text"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: 2px solid rgba(17, 24, 39, 0.14);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save:hover {
|
||||||
|
background: #1f2937;
|
||||||
|
border-color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
position: relative;
|
||||||
|
width: 46px;
|
||||||
|
min-width: 46px;
|
||||||
|
height: 28px;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--switch-track-off-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--switch-track-off);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease, border-color 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--switch-thumb-off);
|
||||||
|
box-shadow: 0 1px 2px rgba(17, 24, 39, 0.18),
|
||||||
|
inset 0 0 0 1px rgba(17, 24, 39, 0.08);
|
||||||
|
transition: transform 120ms ease, background-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
background: var(--switch-track-on);
|
||||||
|
border-color: var(--switch-track-on-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked::before {
|
||||||
|
transform: translateX(18px);
|
||||||
|
background: var(--switch-thumb-on);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
label em {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 120px 120px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts-grid .shortcut-row:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-row.customs {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 120px 120px 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-label {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-label em {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customKey,
|
||||||
|
.customValue {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chevron: native menu indicator is often missing with themed controls */
|
||||||
|
#addShortcutSelector,
|
||||||
|
.site-add-shortcut-selector {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-color: var(--panel);
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
padding-right: 38px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addShortcutSelector:disabled,
|
||||||
|
.site-add-shortcut-selector:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addShortcutSelector {
|
||||||
|
width: min(220px, 100%);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeParent,
|
||||||
|
.toggle-site-rule {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeParent {
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-site-rule {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-site-rule:hover {
|
||||||
|
color: var(--toggle-open-fg);
|
||||||
|
background: var(--toggle-open-hover-bg);
|
||||||
|
border-color: var(--toggle-open-hover-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-site-rule .site-rule-toggle-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-site-rule .site-rule-toggle-icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule:not(.collapsed) .toggle-site-rule {
|
||||||
|
color: var(--toggle-open-fg);
|
||||||
|
background: var(--toggle-open-bg);
|
||||||
|
border-color: var(--toggle-open-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule:not(.collapsed) .toggle-site-rule:hover {
|
||||||
|
color: var(--toggle-open-fg);
|
||||||
|
background: var(--toggle-open-hover-bg);
|
||||||
|
border-color: var(--toggle-open-hover-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 160px;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row input[type="text"],
|
||||||
|
.row select {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-checkbox {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-checkbox input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card .row:first-of-type {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-controller-margin {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controller-margin-inputs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 116px));
|
||||||
|
gap: 8px;
|
||||||
|
width: max-content;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-pad-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 116px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-pad-mini {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controller-margin-inputs input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 116px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option.site-rule-margin-option {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-override-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-content > .site-rule-override-section:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead span {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead span em {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-override-section .site-override-fields,
|
||||||
|
.site-rule-override-section .site-placement-container,
|
||||||
|
.site-rule-override-section .site-visibility-container,
|
||||||
|
.site-rule-override-section .site-autohide-container,
|
||||||
|
.site-rule-override-section .site-playback-container,
|
||||||
|
.site-rule-override-section .site-opacity-container,
|
||||||
|
.site-rule-override-section .site-subtitleNudge-container,
|
||||||
|
.site-controlbar-container,
|
||||||
|
.site-popup-controlbar-container,
|
||||||
|
.site-shortcuts-container {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-disabled {
|
||||||
|
opacity: 0.48;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-editor-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-zone-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-dropzone {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
transition: border-color 150ms ease, background 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-dropzone.cb-over {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(17, 24, 39, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-active-zone:empty::after {
|
||||||
|
content: "Drag buttons here";
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-available-zone:empty::after {
|
||||||
|
content: "All buttons active";
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 12px 7px 8px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
transition: box-shadow 150ms ease, opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-block:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-block:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-block.cb-dragging {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-grip {
|
||||||
|
width: 6px;
|
||||||
|
min-width: 6px;
|
||||||
|
height: 14px;
|
||||||
|
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
|
||||||
|
background-size: 3px 3px;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-icon.cb-icon-nudge-pair {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
gap: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip[data-nudge-state="on"] {
|
||||||
|
background: #4b9135;
|
||||||
|
border: 1px solid #6ec754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip[data-nudge-state="off"] {
|
||||||
|
background: #943e3e;
|
||||||
|
border: 1px solid #c06060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip .vsc-btn-icon svg,
|
||||||
|
.cb-nudge-chip svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-sep {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.45;
|
||||||
|
color: var(--text);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-pair select {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-search-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-search-row .lucide-search-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 14px 0 12px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-field:focus-within {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-icon {
|
||||||
|
display: flex;
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-results {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile:hover {
|
||||||
|
background: var(--panel);
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile .lucide-result-thumb {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile.lucide-picked {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px rgba(17, 24, 39, 0.12);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-status {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #111827 !important;
|
||||||
|
border: 1px solid var(--border-strong) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply:hover {
|
||||||
|
background: #f3f4f6 !important;
|
||||||
|
border-color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .secondary {
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#siteRulesContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36px minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-pattern {
|
||||||
|
min-width: 0;
|
||||||
|
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-body {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-content.disabled-rule {
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 160px;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox > input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option > input[type="text"],
|
||||||
|
.site-rule-option > select {
|
||||||
|
justify-self: end;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option.site-rule-margin-option .controller-margin-inputs {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-body > .site-rule-option:first-child,
|
||||||
|
.site-rule-content > .site-rule-option:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option > label:not(.site-rule-split-label) {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-split-label {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-split-label span em {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-split-label input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox > .site-rule-split-label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-controlbar,
|
||||||
|
.site-rule-shortcuts {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-controlbar > label.site-override-lead,
|
||||||
|
.site-rule-shortcuts > label.site-override-lead {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-controlbar-container,
|
||||||
|
.site-popup-controlbar-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-shortcuts-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-shortcuts-container .shortcut-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr) 38px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-shortcuts-rows .shortcut-row:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-add-shortcut-selector {
|
||||||
|
width: min(220px, 100%);
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.force-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.force-label input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
min-height: 1.3em;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-text {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link:hover {
|
||||||
|
background: var(--toggle-open-bg);
|
||||||
|
border-color: var(--toggle-open-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--kofi {
|
||||||
|
background: #fff4ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--kofi:hover {
|
||||||
|
background: #ffe8de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-kofi-img {
|
||||||
|
display: block;
|
||||||
|
height: 26px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--bmc {
|
||||||
|
color: #0d0c22;
|
||||||
|
background: #ffdd00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--bmc:hover {
|
||||||
|
background: #f7d500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--bmc svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.lucide-icon-preview-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-row,
|
||||||
|
.shortcut-row.customs,
|
||||||
|
.row,
|
||||||
|
.row.row-checkbox,
|
||||||
|
.site-rule-option,
|
||||||
|
.site-shortcuts-container .shortcut-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row input[type="text"],
|
||||||
|
.row select {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option > input[type="text"],
|
||||||
|
.site-rule-option > select {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row button,
|
||||||
|
#addShortcutSelector,
|
||||||
|
.site-add-shortcut-selector {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-header {
|
||||||
|
grid-template-columns: 36px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-site-rule {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
body {
|
||||||
|
padding: 16px 12px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bars-group {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #111315;
|
||||||
|
--panel: #171a1d;
|
||||||
|
--panel-subtle: #1b1f23;
|
||||||
|
--border: #2b3138;
|
||||||
|
--border-strong: #3a414a;
|
||||||
|
--text: #f2f4f6;
|
||||||
|
--muted: #a0a8b2;
|
||||||
|
--accent: #f2f4f6;
|
||||||
|
--switch-track-off: #374151;
|
||||||
|
--switch-track-off-border: #4b5563;
|
||||||
|
--switch-track-on: #aab7c6;
|
||||||
|
--switch-track-on-border: #aab7c6;
|
||||||
|
--switch-thumb-off: #f8fafc;
|
||||||
|
--switch-thumb-on: #111315;
|
||||||
|
--toggle-open-fg: #f2f4f6;
|
||||||
|
--toggle-open-bg: #2b3138;
|
||||||
|
--toggle-open-border: #4b5563;
|
||||||
|
--toggle-open-hover-bg: #374151;
|
||||||
|
--toggle-open-hover-border: #64748b;
|
||||||
|
--danger: #ff8a80;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: #f2f4f6;
|
||||||
|
text-decoration-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save {
|
||||||
|
background: #f2f4f6;
|
||||||
|
border-color: #f2f4f6;
|
||||||
|
color: #111315;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save:hover {
|
||||||
|
background: #dfe3e8;
|
||||||
|
border-color: #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-dropzone.cb-over {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#addShortcutSelector,
|
||||||
|
.site-add-shortcut-selector {
|
||||||
|
background-color: var(--panel);
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-field:focus-within {
|
||||||
|
border-color: #6b7280;
|
||||||
|
box-shadow: 0 0 0 3px rgba(242, 244, 246, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #111315 !important;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply:hover {
|
||||||
|
background: #f3f4f6 !important;
|
||||||
|
border-color: #d1d5db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile .lucide-result-thumb {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--kofi {
|
||||||
|
background: #2c241f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--kofi:hover {
|
||||||
|
background: #3a312a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--bmc {
|
||||||
|
color: #ffdd00;
|
||||||
|
background: #2a2618;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-cta-link--bmc:hover {
|
||||||
|
background: #3d3510;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,969 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Speeder Settings</title>
|
||||||
|
<link rel="stylesheet" href="options.css" />
|
||||||
|
<script src="../shared/controller-utils.js"></script>
|
||||||
|
<script src="../shared/key-bindings.js"></script>
|
||||||
|
<script src="../shared/popup-controls.js"></script>
|
||||||
|
<script src="../shared/ui-icons.js"></script>
|
||||||
|
<script src="lucide-client.js"></script>
|
||||||
|
<script src="options.js"></script>
|
||||||
|
<script src="../shared/import-export.js"></script>
|
||||||
|
<script src="import-export.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-shell">
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="title-row">
|
||||||
|
<div class="title-block">
|
||||||
|
<h1>Speeder settings</h1>
|
||||||
|
<p class="page-subtitle">Shortcuts, defaults, and site rules.</p>
|
||||||
|
</div>
|
||||||
|
<div class="version">v<span id="app-version"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="support-cta" role="region" aria-label="Support Speeder">
|
||||||
|
<p class="support-cta-text">
|
||||||
|
If Speeder has been useful, please consider supporting its development!
|
||||||
|
</p>
|
||||||
|
<div class="support-cta-links">
|
||||||
|
<a
|
||||||
|
class="support-cta-link support-cta-link--kofi"
|
||||||
|
href="https://ko-fi.com/joshpatra"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Support on Ko-fi (opens in new tab)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="support-cta-kofi-img"
|
||||||
|
src="../assets/images/kofi_symbol.svg"
|
||||||
|
width="241"
|
||||||
|
height="194"
|
||||||
|
alt=""
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="support-cta-link support-cta-link--github"
|
||||||
|
href="https://github.com/sponsors/SoPat712"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Sponsor on GitHub (opens in new tab)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="support-cta-link support-cta-link--bmc"
|
||||||
|
href="https://buymeacoffee.com/treeman183"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Support on Buy Me a Coffee (opens in new tab)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="settings-stack">
|
||||||
|
<section id="customs" class="settings-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Shortcuts</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Backspace clears a key. Escape disables optional shortcuts. If a site
|
||||||
|
steals a shortcut, use a site rule with Override shortcuts (and
|
||||||
|
per-key blocking) for that URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="shortcuts-grid">
|
||||||
|
<div class="shortcut-row" id="display" data-action="display">
|
||||||
|
<div class="shortcut-label">Show/hide controller</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value"
|
||||||
|
value="N/A"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="move" data-action="move">
|
||||||
|
<div class="shortcut-label">Move controller</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value"
|
||||||
|
value="N/A"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="slower" data-action="slower">
|
||||||
|
<div class="shortcut-label">
|
||||||
|
Decrease speed
|
||||||
|
<em>Required: Speeder needs a key for this action.</em>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value (0.10)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="faster" data-action="faster">
|
||||||
|
<div class="shortcut-label">
|
||||||
|
Increase speed
|
||||||
|
<em>Required: Speeder needs a key for this action.</em>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value (0.10)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="rewind" data-action="rewind">
|
||||||
|
<div class="shortcut-label">Rewind</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value (10)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="advance" data-action="advance">
|
||||||
|
<div class="shortcut-label">Advance</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value (10)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="reset" data-action="reset">
|
||||||
|
<div class="shortcut-label">Reset speed</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value"
|
||||||
|
value="N/A"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-row" id="fast" data-action="fast">
|
||||||
|
<div class="shortcut-label">Preferred speed</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value (1.80)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="shortcut-row"
|
||||||
|
id="toggleSubtitleNudge"
|
||||||
|
data-action="toggleSubtitleNudge"
|
||||||
|
>
|
||||||
|
<div class="shortcut-label">Toggle subtitle nudge</div>
|
||||||
|
<input
|
||||||
|
class="customKey"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="press a key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="customValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="value"
|
||||||
|
value="N/A"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select id="addShortcutSelector">
|
||||||
|
<option value="">Add shortcut…</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="generalSettings" class="settings-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Defaults</h3>
|
||||||
|
<p class="section-intro">Used unless a site rule overrides it.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="defaults-sub-heading">General</h4>
|
||||||
|
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="enabled"
|
||||||
|
>Enable<br />
|
||||||
|
<em
|
||||||
|
>On: site rules can block sites (blacklist). Off: only matched rules keep Speeder on (whitelist).</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="enabled" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="audioBoolean"
|
||||||
|
>Work on audio<br />
|
||||||
|
<em
|
||||||
|
>Also controls plain HTML5 audio (not just video). Turn off if
|
||||||
|
you only want Speeder on video players.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="audioBoolean" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="defaults-divider"></div>
|
||||||
|
<h4 class="defaults-sub-heading">Playback</h4>
|
||||||
|
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="rememberSpeed"
|
||||||
|
>Remember playback speed<br />
|
||||||
|
<em
|
||||||
|
>Stores speed per source so revisiting the same media can restore
|
||||||
|
it. Separate from “Force last saved speed,” which
|
||||||
|
fights players that reset rate.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="rememberSpeed" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="forceLastSavedSpeed"
|
||||||
|
>Force last saved speed<br />
|
||||||
|
<em
|
||||||
|
>Useful when a video player tries to override the speed you set
|
||||||
|
in Speeder.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="forceLastSavedSpeed" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="defaults-divider"></div>
|
||||||
|
<h4 class="defaults-sub-heading">Controller</h4>
|
||||||
|
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="startHidden"
|
||||||
|
>Hide controller by default<br />
|
||||||
|
<em
|
||||||
|
>Starts with the overlay hidden; use shortcuts (show/hide,
|
||||||
|
move) or site behavior to reveal it.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="startHidden" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="controllerLocation"
|
||||||
|
>Default controller location<br />
|
||||||
|
<em
|
||||||
|
>Corner or edge anchor for the hover bar. Site rules can override
|
||||||
|
this for specific URLs.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<select id="controllerLocation">
|
||||||
|
<option value="top-left">Top left</option>
|
||||||
|
<option value="top-center">Top center</option>
|
||||||
|
<option value="top-right">Top right</option>
|
||||||
|
<option value="middle-right">Middle right</option>
|
||||||
|
<option value="bottom-right">Bottom right</option>
|
||||||
|
<option value="bottom-center">Bottom center</option>
|
||||||
|
<option value="bottom-left">Bottom left</option>
|
||||||
|
<option value="middle-left">Middle left</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="controllerOpacity"
|
||||||
|
>Controller opacity<br />
|
||||||
|
<em
|
||||||
|
>0–1 (decimals). Lower is more transparent. Applies to the
|
||||||
|
in-page controller only.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="controllerOpacity" type="text" value="" />
|
||||||
|
</div>
|
||||||
|
<div class="row row-controller-margin">
|
||||||
|
<label for="controllerMarginTop"
|
||||||
|
>Controller margin (px)<br />
|
||||||
|
<em
|
||||||
|
>Shifts the whole control from its preset position (CSS
|
||||||
|
margins). Top and bottom. 0–200.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<div class="controller-margin-inputs" aria-label="Controller margin in pixels">
|
||||||
|
<div class="margin-pad-cell">
|
||||||
|
<span class="margin-pad-mini">Top</span>
|
||||||
|
<input id="controllerMarginTop" type="text" inputmode="numeric" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<div class="margin-pad-cell">
|
||||||
|
<span class="margin-pad-mini">Bottom</span>
|
||||||
|
<input id="controllerMarginBottom" type="text" inputmode="numeric" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="hideWithControls"
|
||||||
|
>Hide with controls<br />
|
||||||
|
<em
|
||||||
|
>Fade the controller in and out with the video interface:
|
||||||
|
perfect sync on YouTube, idle-based elsewhere.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="hideWithControls" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="hideWithControlsTimer"
|
||||||
|
>Auto-hide timer (seconds)<br />
|
||||||
|
<em
|
||||||
|
>Seconds of inactivity before hiding: 0.1–15 for
|
||||||
|
non-YouTube sites.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
||||||
|
</div>
|
||||||
|
<div class="defaults-divider"></div>
|
||||||
|
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
||||||
|
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="enableSubtitleNudge"
|
||||||
|
>Enable subtitle nudge<br /><em
|
||||||
|
>Makes tiny playback changes to help keep subtitles aligned and
|
||||||
|
allows nudge toggle controls.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="enableSubtitleNudge" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="subtitleNudgeEnabledByDefault"
|
||||||
|
>Start subtitle nudge enabled<br /><em
|
||||||
|
>When off, each video starts with nudging disabled until you
|
||||||
|
toggle it.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="subtitleNudgeEnabledByDefault" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="subtitleNudgeInterval"
|
||||||
|
>Nudge interval (milliseconds)<br /><em
|
||||||
|
>How often to nudge: 10–1000. Smaller values are more
|
||||||
|
frequent. Default: 50.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="subtitleNudgeInterval"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="controlBarsGroup"
|
||||||
|
class="settings-card control-bars-group"
|
||||||
|
aria-labelledby="controlBarsGroupHeading"
|
||||||
|
>
|
||||||
|
<div class="section-heading section-heading-major">
|
||||||
|
<h2 id="controlBarsGroupHeading">Control bars</h2>
|
||||||
|
<p class="section-intro">
|
||||||
|
In-page hover bar, extension popup bar, and Lucide icons for
|
||||||
|
buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="control-bars-inner">
|
||||||
|
<section id="controlBarSettings" class="settings-card settings-card-nested">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Hover control bar</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Drag blocks to reorder. Move between Active and Available to
|
||||||
|
show or hide buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cb-editor">
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Active</div>
|
||||||
|
<div
|
||||||
|
id="controlBarActive"
|
||||||
|
class="cb-dropzone cb-active-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div
|
||||||
|
id="controlBarAvailable"
|
||||||
|
class="cb-dropzone cb-available-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="popupControlBarSettings" class="settings-card settings-card-nested">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Popup control bar</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Configure which buttons appear in the browser popup control bar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="showPopupControlBar"
|
||||||
|
>Show popup control bar<br />
|
||||||
|
<em
|
||||||
|
>Shows buttons in the extension popup (toolbar icon). Can be
|
||||||
|
overridden per site in site rules.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="showPopupControlBar" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="row row-checkbox">
|
||||||
|
<label for="popupMatchHoverControls"
|
||||||
|
>Match hover controls<br />
|
||||||
|
<em
|
||||||
|
>When on, the popup copies the hover bar’s buttons and
|
||||||
|
order. When off, customize the popup layout below.</em
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input id="popupMatchHoverControls" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Active</div>
|
||||||
|
<div
|
||||||
|
id="popupControlBarActive"
|
||||||
|
class="cb-dropzone cb-active-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div
|
||||||
|
id="popupControlBarAvailable"
|
||||||
|
class="cb-dropzone cb-available-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="lucideIconSettings" class="settings-card settings-card-nested">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Button icons (Lucide)</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Search icons from the
|
||||||
|
<a
|
||||||
|
href="https://lucide.dev"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Lucide</a
|
||||||
|
>
|
||||||
|
set (fetched from jsDelivr). Custom icons are cached in local
|
||||||
|
storage and included when you export settings. Subtitle nudge
|
||||||
|
icons use two menu entries (enabled and disabled), not the bar
|
||||||
|
block id
|
||||||
|
<code>nudge</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row row-lucide-pair">
|
||||||
|
<label for="lucideIconActionSelect">Controller action</label>
|
||||||
|
<select id="lucideIconActionSelect"></select>
|
||||||
|
</div>
|
||||||
|
<div class="row row-lucide-search-row">
|
||||||
|
<label for="lucideIconSearch" class="lucide-search-label"
|
||||||
|
>Search icons</label
|
||||||
|
>
|
||||||
|
<div class="lucide-search-field">
|
||||||
|
<span class="lucide-search-icon" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="lucideIconSearch"
|
||||||
|
class="lucide-search-input"
|
||||||
|
placeholder="Search by name or tag (e.g. star, arrow, media)…"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="lucideIconResults"
|
||||||
|
class="lucide-icon-results"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="Matching Lucide icons"
|
||||||
|
></div>
|
||||||
|
<p id="lucideIconStatus" class="lucide-icon-status" aria-live="polite"></p>
|
||||||
|
<div class="lucide-icon-preview-row">
|
||||||
|
<div
|
||||||
|
id="lucideIconPreview"
|
||||||
|
class="lucide-icon-preview"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
<div class="lucide-icon-actions">
|
||||||
|
<button type="button" id="lucideIconApply" class="lucide-apply">
|
||||||
|
Apply to action
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconClearAction" class="secondary">
|
||||||
|
Clear this action
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconClearAll" class="secondary">
|
||||||
|
Clear all custom icons
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconReloadTags" class="secondary">
|
||||||
|
Refresh icon list from network
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="siteRules" class="settings-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Site rules</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Use plain domains or
|
||||||
|
<a
|
||||||
|
href="https://www.regexpal.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Regex</a
|
||||||
|
>
|
||||||
|
patterns like <code>/(.+)youtube\.com(\/*)$/gi</code>. Turn on a
|
||||||
|
row only when you want that group to override the general defaults
|
||||||
|
above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="siteRulesContainer"></div>
|
||||||
|
<button id="addSiteRule" type="button">Add Site Rule</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template id="siteRuleTemplate">
|
||||||
|
<div class="site-rule">
|
||||||
|
<div class="site-rule-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-site-rule"
|
||||||
|
title="Expand site rule"
|
||||||
|
aria-label="Expand site rule"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="site-rule-toggle-icon" aria-hidden="true">…</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="site-pattern"
|
||||||
|
placeholder="e.g., youtube.com or /regex/gi"
|
||||||
|
/>
|
||||||
|
<button type="button" class="remove-site-rule">Remove</button>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-body">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label class="site-rule-split-label">
|
||||||
|
<span
|
||||||
|
>Enable Speeder on this site<br /><em
|
||||||
|
>For this URL pattern only: off blocks when global Speeder
|
||||||
|
is on (blacklist); on allows when global is off
|
||||||
|
(whitelist)—same pairing as Defaults →
|
||||||
|
Enable.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-enabled" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-content">
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override placement for this site<br /><em
|
||||||
|
>When on, location and margin below replace general
|
||||||
|
defaults for matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-placement" />
|
||||||
|
</label>
|
||||||
|
<div class="site-placement-container">
|
||||||
|
<div class="site-rule-option site-rule-option-field">
|
||||||
|
<label
|
||||||
|
>Default controller location:<br /><em
|
||||||
|
>Corner or edge anchor for the hover bar. Replaces the
|
||||||
|
general default for matching URLs only.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<select class="site-controllerLocation">
|
||||||
|
<option value="top-left">Top left</option>
|
||||||
|
<option value="top-center">Top center</option>
|
||||||
|
<option value="top-right">Top right</option>
|
||||||
|
<option value="middle-right">Middle right</option>
|
||||||
|
<option value="bottom-right">Bottom right</option>
|
||||||
|
<option value="bottom-center">Bottom center</option>
|
||||||
|
<option value="bottom-left">Bottom left</option>
|
||||||
|
<option value="middle-left">Middle left</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-margin-option">
|
||||||
|
<label
|
||||||
|
>Controller margin (px):<br /><em
|
||||||
|
>Shifts the whole control from its preset position (CSS
|
||||||
|
margins). Top and bottom. 0–200.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<div class="controller-margin-inputs">
|
||||||
|
<div class="margin-pad-cell">
|
||||||
|
<span class="margin-pad-mini">T</span>
|
||||||
|
<input type="text" class="site-controllerMarginTop" inputmode="numeric" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<div class="margin-pad-cell">
|
||||||
|
<span class="margin-pad-mini">B</span>
|
||||||
|
<input type="text" class="site-controllerMarginBottom" inputmode="numeric" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override hide-by-default for this site<br /><em
|
||||||
|
>When on, the hide-by-default toggle below replaces the
|
||||||
|
general default for matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-visibility" />
|
||||||
|
</label>
|
||||||
|
<div class="site-visibility-container">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Hide controller by default:<br /><em
|
||||||
|
>Starts with the overlay hidden; use shortcuts
|
||||||
|
(show/hide, move) or site behavior to reveal it.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-startHidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override auto-hide for this site<br /><em
|
||||||
|
>When on, hide-with-controls and timer below replace
|
||||||
|
general defaults for matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-autohide" />
|
||||||
|
</label>
|
||||||
|
<div class="site-autohide-container">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label class="site-rule-split-label">
|
||||||
|
<span
|
||||||
|
>Hide with controls (idle-based)<br /><em
|
||||||
|
>Fade the controller in and out with the video
|
||||||
|
interface: perfect sync on YouTube, idle-based
|
||||||
|
elsewhere.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-hideWithControls" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-option-field">
|
||||||
|
<label
|
||||||
|
>Auto-hide timer (0.1–15s):<br /><em
|
||||||
|
>Seconds of inactivity before hiding: 0.1–15 for
|
||||||
|
non-YouTube sites.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="text" class="site-hideWithControlsTimer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override playback for this site<br /><em
|
||||||
|
>When on, remember speed / force / audio below replace
|
||||||
|
general defaults for matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-playback" />
|
||||||
|
</label>
|
||||||
|
<div class="site-playback-container">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Remember playback speed:<br /><em
|
||||||
|
>Stores speed per source so revisiting the same media
|
||||||
|
can restore it. Separate from “Force last saved
|
||||||
|
speed,” which fights players that reset
|
||||||
|
rate.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-rememberSpeed" />
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Force last saved speed:<br /><em
|
||||||
|
>Useful when a video player tries to override the speed
|
||||||
|
you set in Speeder.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Work on audio:<br /><em
|
||||||
|
>Also controls plain HTML5 audio (not just video). Turn
|
||||||
|
off if you only want Speeder on video players.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-audioBoolean" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override opacity for this site<br /><em
|
||||||
|
>When on, opacity below replaces the general default for
|
||||||
|
matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-opacity" />
|
||||||
|
</label>
|
||||||
|
<div class="site-opacity-container">
|
||||||
|
<div class="site-rule-option site-rule-option-field">
|
||||||
|
<label
|
||||||
|
>Controller opacity:<br /><em
|
||||||
|
>0–1 (decimals). Lower is more transparent.
|
||||||
|
Applies to the in-page controller only.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="text" class="site-controllerOpacity" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-override-section">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override subtitle nudge for this site<br /><em
|
||||||
|
>When on, nudge options below replace general defaults for
|
||||||
|
matching URLs.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-subtitleNudge" />
|
||||||
|
</label>
|
||||||
|
<div class="site-subtitleNudge-container">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Enable subtitle nudge:<br /><em
|
||||||
|
>Makes tiny playback changes to help keep subtitles
|
||||||
|
aligned.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Start subtitle nudge enabled:<br /><em
|
||||||
|
>When off, matching videos start with nudging disabled
|
||||||
|
until you toggle it.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-subtitleNudgeEnabledByDefault" />
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-option site-rule-option-field">
|
||||||
|
<label
|
||||||
|
>Nudge interval (10–1000ms):<br /><em
|
||||||
|
>How often to nudge: 10–1000. Smaller values are
|
||||||
|
more frequent. Default: 50.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-controlbar">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override in-player control bar for this site<br /><em
|
||||||
|
>Same idea as Hover control bar: drag blocks between
|
||||||
|
Active and Available for matching URLs only.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-controlbar" />
|
||||||
|
</label>
|
||||||
|
<div class="site-controlbar-container">
|
||||||
|
<div class="cb-editor">
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Active</div>
|
||||||
|
<div class="cb-dropzone cb-active-zone site-cb-active"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div class="cb-dropzone cb-available-zone site-cb-available"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-controlbar">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override extension popup for this site<br /><em
|
||||||
|
>Popup layout for matching URLs; mirrors the global Popup
|
||||||
|
control bar when you customize it here.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-popup-controlbar" />
|
||||||
|
</label>
|
||||||
|
<div class="site-popup-controlbar-container">
|
||||||
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
|
<label
|
||||||
|
>Show popup control bar<br /><em
|
||||||
|
>Shows buttons in the extension popup (toolbar icon).
|
||||||
|
Replaces the general default for this pattern.</em
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="site-showPopupControlBar" />
|
||||||
|
</div>
|
||||||
|
<div class="cb-editor">
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Active</div>
|
||||||
|
<div class="cb-dropzone cb-active-zone site-popup-cb-active"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div class="cb-dropzone cb-available-zone site-popup-cb-available"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="site-rule-shortcuts">
|
||||||
|
<label class="site-override-lead">
|
||||||
|
<span
|
||||||
|
>Override shortcuts for this site<br /><em
|
||||||
|
>Add shortcuts from the menu; none by default. Leave off
|
||||||
|
to use global Shortcuts.</em
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="override-shortcuts" />
|
||||||
|
</label>
|
||||||
|
<div class="site-shortcuts-container">
|
||||||
|
<div class="site-shortcuts-rows"></div>
|
||||||
|
<select
|
||||||
|
class="site-add-shortcut-selector"
|
||||||
|
aria-label="Add shortcut for this site"
|
||||||
|
>
|
||||||
|
<option value="">Add shortcut…</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<section class="settings-card action-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Actions</h3>
|
||||||
|
<p class="section-intro">Save, restore, export, or import settings.</p>
|
||||||
|
</div>
|
||||||
|
<div class="action-row">
|
||||||
|
<button id="save">Save Changes</button>
|
||||||
|
<button id="restore">Restore Defaults</button>
|
||||||
|
<button id="exportSettings">Export Settings</button>
|
||||||
|
<button id="importSettings">Import Settings</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" role="status" aria-live="polite"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="faq" class="settings-card info-card">
|
||||||
|
<h4>Extension controls not appearing?</h4>
|
||||||
|
<p>
|
||||||
|
This extension only works with HTML5 audio and video. If the
|
||||||
|
controls never appear, you may be looking at Flash content instead.
|
||||||
|
Right-click the player to check: if the menu mentions Flash, that
|
||||||
|
is the issue. Most sites will fall back to HTML5 when Flash is not
|
||||||
|
available, so disabling Flash in the browser can help.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,106 +1,44 @@
|
|||||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||||
|
var speederShared =
|
||||||
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
|
var controllerUtils = speederShared.controllerUtils || {};
|
||||||
|
var keyBindingUtils = speederShared.keyBindings || {};
|
||||||
|
var popupControlUtils = speederShared.popupControls || {};
|
||||||
|
|
||||||
var keyBindings = [];
|
var keyBindings = [];
|
||||||
|
|
||||||
var keyCodeAliases = {
|
var bindingCodeAliases = {
|
||||||
0: "null",
|
Space: "Space",
|
||||||
null: "null",
|
ArrowLeft: "Left",
|
||||||
undefined: "null",
|
ArrowUp: "Up",
|
||||||
32: "Space",
|
ArrowRight: "Right",
|
||||||
37: "Left",
|
ArrowDown: "Down",
|
||||||
38: "Up",
|
Numpad0: "Num 0",
|
||||||
39: "Right",
|
Numpad1: "Num 1",
|
||||||
40: "Down",
|
Numpad2: "Num 2",
|
||||||
96: "Num 0",
|
Numpad3: "Num 3",
|
||||||
97: "Num 1",
|
Numpad4: "Num 4",
|
||||||
98: "Num 2",
|
Numpad5: "Num 5",
|
||||||
99: "Num 3",
|
Numpad6: "Num 6",
|
||||||
100: "Num 4",
|
Numpad7: "Num 7",
|
||||||
101: "Num 5",
|
Numpad8: "Num 8",
|
||||||
102: "Num 6",
|
Numpad9: "Num 9",
|
||||||
103: "Num 7",
|
NumpadMultiply: "Num *",
|
||||||
104: "Num 8",
|
NumpadAdd: "Num +",
|
||||||
105: "Num 9",
|
NumpadSubtract: "Num -",
|
||||||
106: "Num *",
|
NumpadDecimal: "Num .",
|
||||||
107: "Num +",
|
NumpadDivide: "Num /",
|
||||||
109: "Num -",
|
Backquote: "`",
|
||||||
110: "Num .",
|
Minus: "-",
|
||||||
111: "Num /",
|
Equal: "=",
|
||||||
112: "F1",
|
BracketLeft: "[",
|
||||||
113: "F2",
|
BracketRight: "]",
|
||||||
114: "F3",
|
Backslash: "\\",
|
||||||
115: "F4",
|
Semicolon: ";",
|
||||||
116: "F5",
|
Quote: "'",
|
||||||
117: "F6",
|
Comma: ",",
|
||||||
118: "F7",
|
Period: ".",
|
||||||
119: "F8",
|
Slash: "/"
|
||||||
120: "F9",
|
|
||||||
121: "F10",
|
|
||||||
122: "F11",
|
|
||||||
123: "F12",
|
|
||||||
186: ";",
|
|
||||||
188: "<",
|
|
||||||
189: "-",
|
|
||||||
187: "+",
|
|
||||||
190: ">",
|
|
||||||
191: "/",
|
|
||||||
192: "~",
|
|
||||||
219: "[",
|
|
||||||
220: "\\",
|
|
||||||
221: "]",
|
|
||||||
222: "'",
|
|
||||||
59: ";",
|
|
||||||
61: "+",
|
|
||||||
173: "-"
|
|
||||||
};
|
|
||||||
|
|
||||||
var keyCodeToKey = {
|
|
||||||
32: " ",
|
|
||||||
37: "ArrowLeft",
|
|
||||||
38: "ArrowUp",
|
|
||||||
39: "ArrowRight",
|
|
||||||
40: "ArrowDown",
|
|
||||||
96: "0",
|
|
||||||
97: "1",
|
|
||||||
98: "2",
|
|
||||||
99: "3",
|
|
||||||
100: "4",
|
|
||||||
101: "5",
|
|
||||||
102: "6",
|
|
||||||
103: "7",
|
|
||||||
104: "8",
|
|
||||||
105: "9",
|
|
||||||
106: "*",
|
|
||||||
107: "+",
|
|
||||||
109: "-",
|
|
||||||
110: ".",
|
|
||||||
111: "/",
|
|
||||||
112: "F1",
|
|
||||||
113: "F2",
|
|
||||||
114: "F3",
|
|
||||||
115: "F4",
|
|
||||||
116: "F5",
|
|
||||||
117: "F6",
|
|
||||||
118: "F7",
|
|
||||||
119: "F8",
|
|
||||||
120: "F9",
|
|
||||||
121: "F10",
|
|
||||||
122: "F11",
|
|
||||||
123: "F12",
|
|
||||||
186: ";",
|
|
||||||
188: "<",
|
|
||||||
189: "-",
|
|
||||||
187: "+",
|
|
||||||
190: ">",
|
|
||||||
191: "/",
|
|
||||||
192: "~",
|
|
||||||
219: "[",
|
|
||||||
220: "\\",
|
|
||||||
221: "]",
|
|
||||||
222: "'",
|
|
||||||
59: ";",
|
|
||||||
61: "+",
|
|
||||||
173: "-"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var modifierKeys = new Set([
|
var modifierKeys = new Set([
|
||||||
@@ -114,23 +52,18 @@ var modifierKeys = new Set([
|
|||||||
"Shift"
|
"Shift"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var displayKeyAliases = {
|
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||||
" ": "Space",
|
? controllerUtils.controllerLocations.slice()
|
||||||
ArrowLeft: "Left",
|
: [
|
||||||
ArrowUp: "Up",
|
"top-left",
|
||||||
ArrowRight: "Right",
|
"top-center",
|
||||||
ArrowDown: "Down"
|
"top-right",
|
||||||
};
|
"middle-right",
|
||||||
var controllerLocations = [
|
"bottom-right",
|
||||||
"top-left",
|
"bottom-center",
|
||||||
"top-center",
|
"bottom-left",
|
||||||
"top-right",
|
"middle-left"
|
||||||
"middle-right",
|
];
|
||||||
"bottom-right",
|
|
||||||
"bottom-center",
|
|
||||||
"bottom-left",
|
|
||||||
"middle-left"
|
|
||||||
];
|
|
||||||
|
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { icon: "\u00AB", name: "Rewind" },
|
rewind: { icon: "\u00AB", name: "Rewind" },
|
||||||
@@ -138,21 +71,91 @@ var controllerButtonDefs = {
|
|||||||
faster: { icon: "+", name: "Increase speed" },
|
faster: { icon: "+", name: "Increase speed" },
|
||||||
advance: { icon: "\u00BB", name: "Advance" },
|
advance: { icon: "\u00BB", name: "Advance" },
|
||||||
display: { icon: "\u00D7", name: "Close controller" },
|
display: { icon: "\u00D7", name: "Close controller" },
|
||||||
reset: { icon: "\u21BA", name: "Reset speed" },
|
reset: { icon: "\u21BB", name: "Reset speed" },
|
||||||
fast: { icon: "\u2605", name: "Preferred speed" },
|
fast: { icon: "\u2605", name: "Preferred speed" },
|
||||||
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
||||||
settings: { icon: "\u2699", name: "Settings" },
|
pause: { icon: "\u23EF", name: "Play / Pause" },
|
||||||
pause: { icon: "\u23EF", name: "Pause / Play" },
|
|
||||||
muted: { icon: "M", name: "Mute / Unmute" },
|
muted: { icon: "M", name: "Mute / Unmute" },
|
||||||
|
louder: { icon: "+", name: "Increase volume" },
|
||||||
|
softer: { icon: "\u2212", name: "Decrease volume" },
|
||||||
mark: { icon: "\u2691", name: "Set marker" },
|
mark: { icon: "\u2691", name: "Set marker" },
|
||||||
jump: { icon: "\u21E5", name: "Jump to marker" }
|
jump: { icon: "\u21E5", name: "Jump to marker" },
|
||||||
|
settings: { icon: "\u2699", name: "Settings" },
|
||||||
|
};
|
||||||
|
var popupExcludedButtonIds = new Set(["settings"]);
|
||||||
|
|
||||||
|
/** Lucide picker only — not control-bar blocks (chip uses subtitleNudgeOn/Off). */
|
||||||
|
var lucideSubtitleNudgeActionLabels = {
|
||||||
|
subtitleNudgeOn: "Subtitle nudge — enabled",
|
||||||
|
subtitleNudgeOff: "Subtitle nudge — disabled"
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDefaultBinding(action, key, keyCode, value) {
|
function sanitizePopupButtonOrder(buttonIds) {
|
||||||
|
return popupControlUtils.sanitizeButtonOrder(
|
||||||
|
buttonIds,
|
||||||
|
controllerButtonDefs,
|
||||||
|
popupExcludedButtonIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
|
||||||
|
var customButtonIconsLive = {};
|
||||||
|
|
||||||
|
function fillControlBarIconElement(icon, buttonId) {
|
||||||
|
if (!icon || !buttonId) return;
|
||||||
|
var doc = icon.ownerDocument || document;
|
||||||
|
if (buttonId === "nudge") {
|
||||||
|
vscClearElement(icon);
|
||||||
|
icon.className = "cb-icon cb-icon-nudge-pair";
|
||||||
|
function nudgeChipMarkup(action) {
|
||||||
|
var c = customButtonIconsLive[action];
|
||||||
|
if (c && c.svg) return c.svg;
|
||||||
|
if (typeof vscIconSvgString === "function") {
|
||||||
|
return vscIconSvgString(action, 14) || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
function appendChip(action, stateKey) {
|
||||||
|
var sp = document.createElement("span");
|
||||||
|
sp.className = "cb-nudge-chip";
|
||||||
|
sp.setAttribute("data-nudge-state", stateKey);
|
||||||
|
var inner = nudgeChipMarkup(action);
|
||||||
|
if (inner) {
|
||||||
|
var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon");
|
||||||
|
if (wrap) {
|
||||||
|
sp.appendChild(wrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon.appendChild(sp);
|
||||||
|
}
|
||||||
|
appendChip("subtitleNudgeOn", "on");
|
||||||
|
var sep = document.createElement("span");
|
||||||
|
sep.className = "cb-nudge-sep";
|
||||||
|
sep.textContent = "/";
|
||||||
|
icon.appendChild(sep);
|
||||||
|
appendChip("subtitleNudgeOff", "off");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
icon.className = "cb-icon";
|
||||||
|
var custom = customButtonIconsLive[buttonId];
|
||||||
|
if (custom && custom.svg) {
|
||||||
|
if (vscSetSvgContent(icon, custom.svg)) return;
|
||||||
|
}
|
||||||
|
if (typeof vscIconSvgString === "function") {
|
||||||
|
var svgHtml = vscIconSvgString(buttonId, 16);
|
||||||
|
if (svgHtml) {
|
||||||
|
if (vscSetSvgContent(icon, svgHtml)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vscClearElement(icon);
|
||||||
|
var def = controllerButtonDefs[buttonId];
|
||||||
|
icon.textContent = (def && def.icon) || "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultBinding(action, code, value) {
|
||||||
return {
|
return {
|
||||||
action: action,
|
action: action,
|
||||||
key: key,
|
code: code,
|
||||||
keyCode: keyCode,
|
|
||||||
value: value,
|
value: value,
|
||||||
force: false,
|
force: false,
|
||||||
predefined: true
|
predefined: true
|
||||||
@@ -162,7 +165,6 @@ function createDefaultBinding(action, key, keyCode, value) {
|
|||||||
var tcDefaults = {
|
var tcDefaults = {
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
lastSpeed: 1.0,
|
lastSpeed: 1.0,
|
||||||
displayKeyCode: 86,
|
|
||||||
rememberSpeed: false,
|
rememberSpeed: false,
|
||||||
audioBoolean: false,
|
audioBoolean: false,
|
||||||
startHidden: false,
|
startHidden: false,
|
||||||
@@ -178,15 +180,15 @@ var tcDefaults = {
|
|||||||
controllerMarginBottom: 65,
|
controllerMarginBottom: 65,
|
||||||
controllerMarginLeft: 0,
|
controllerMarginLeft: 0,
|
||||||
keyBindings: [
|
keyBindings: [
|
||||||
createDefaultBinding("display", "V", 86, 0),
|
createDefaultBinding("display", "KeyV", 0),
|
||||||
createDefaultBinding("move", "P", 80, 0),
|
createDefaultBinding("move", "KeyP", 0),
|
||||||
createDefaultBinding("slower", "S", 83, 0.1),
|
createDefaultBinding("slower", "KeyS", 0.1),
|
||||||
createDefaultBinding("faster", "D", 68, 0.1),
|
createDefaultBinding("faster", "KeyD", 0.1),
|
||||||
createDefaultBinding("rewind", "Z", 90, 10),
|
createDefaultBinding("rewind", "KeyZ", 10),
|
||||||
createDefaultBinding("advance", "X", 88, 10),
|
createDefaultBinding("advance", "KeyX", 10),
|
||||||
createDefaultBinding("reset", "R", 82, 1),
|
createDefaultBinding("reset", "KeyR", 1),
|
||||||
createDefaultBinding("fast", "G", 71, 1.8),
|
createDefaultBinding("fast", "KeyG", 1.8),
|
||||||
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
|
createDefaultBinding("toggleSubtitleNudge", "KeyN", 0)
|
||||||
],
|
],
|
||||||
siteRules: [
|
siteRules: [
|
||||||
{
|
{
|
||||||
@@ -198,6 +200,7 @@ var tcDefaults = {
|
|||||||
{
|
{
|
||||||
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
rememberSpeed: true,
|
||||||
controllerMarginTop: 60,
|
controllerMarginTop: 60,
|
||||||
controllerMarginBottom: 85
|
controllerMarginBottom: 85
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,7 @@ var tcDefaults = {
|
|||||||
popupMatchHoverControls: true,
|
popupMatchHoverControls: true,
|
||||||
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||||
enableSubtitleNudge: false,
|
enableSubtitleNudge: false,
|
||||||
|
subtitleNudgeEnabledByDefault: true,
|
||||||
subtitleNudgeInterval: 50,
|
subtitleNudgeInterval: 50,
|
||||||
subtitleNudgeAmount: 0.001
|
subtitleNudgeAmount: 0.001
|
||||||
};
|
};
|
||||||
@@ -220,13 +224,53 @@ const actionLabels = {
|
|||||||
advance: "Advance",
|
advance: "Advance",
|
||||||
reset: "Reset speed",
|
reset: "Reset speed",
|
||||||
fast: "Preferred speed",
|
fast: "Preferred speed",
|
||||||
muted: "Mute",
|
toggleSubtitleNudge: "Toggle subtitle nudge",
|
||||||
pause: "Pause",
|
pause: "Play / Pause",
|
||||||
|
muted: "Mute / Unmute",
|
||||||
|
louder: "Increase volume",
|
||||||
|
softer: "Decrease volume",
|
||||||
mark: "Set marker",
|
mark: "Set marker",
|
||||||
jump: "Jump to marker",
|
jump: "Jump to marker"
|
||||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const speedBindingActions = ["slower", "faster", "fast", "softer", "louder"];
|
||||||
|
const requiredShortcutActions = new Set(["slower", "faster"]);
|
||||||
|
|
||||||
|
function formatSpeedBindingDisplay(action, value) {
|
||||||
|
if (!speedBindingActions.includes(action)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
var n = Number(value);
|
||||||
|
if (!isFinite(n)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return n.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultShortcutValue(action) {
|
||||||
|
if (action === "louder" || action === "softer") {
|
||||||
|
return 0.1;
|
||||||
|
}
|
||||||
|
var defaultBinding = tcDefaults.keyBindings.find(function (binding) {
|
||||||
|
return binding.action === action;
|
||||||
|
});
|
||||||
|
if (defaultBinding && Number.isFinite(Number(defaultBinding.value))) {
|
||||||
|
return Number(defaultBinding.value);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveShortcutValue(action, value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return getDefaultShortcutValue(action);
|
||||||
|
}
|
||||||
|
var numericValue = Number(value);
|
||||||
|
if (Number.isFinite(numericValue)) {
|
||||||
|
return numericValue;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const customActionsNoValues = [
|
const customActionsNoValues = [
|
||||||
"reset",
|
"reset",
|
||||||
"display",
|
"display",
|
||||||
@@ -276,21 +320,155 @@ function refreshAddShortcutSelector() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDefaultBinding(storage, action, key, keyCode, value) {
|
function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
||||||
|
if (!ruleEl) return;
|
||||||
|
var selector = ruleEl.querySelector(".site-add-shortcut-selector");
|
||||||
|
if (!selector) return;
|
||||||
|
|
||||||
|
while (selector.options.length > 1) {
|
||||||
|
selector.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var usedActions = new Set();
|
||||||
|
ruleEl.querySelectorAll(".site-shortcuts-rows .shortcut-row.customs").forEach(function (row) {
|
||||||
|
var action = row.dataset.action;
|
||||||
|
if (action) usedActions.add(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(actionLabels).forEach(function (action) {
|
||||||
|
if (!usedActions.has(action)) {
|
||||||
|
var option = document.createElement("option");
|
||||||
|
option.value = action;
|
||||||
|
option.textContent = actionLabels[action];
|
||||||
|
selector.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var overrideShortcutsOn =
|
||||||
|
ruleEl.querySelector(".override-shortcuts") &&
|
||||||
|
ruleEl.querySelector(".override-shortcuts").checked;
|
||||||
|
|
||||||
|
if (selector.options.length === 1) {
|
||||||
|
selector.disabled = true;
|
||||||
|
selector.options[0].text = "All shortcuts added";
|
||||||
|
} else {
|
||||||
|
selector.disabled = !overrideShortcutsOn;
|
||||||
|
selector.options[0].text = "Add shortcut\u2026";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGlobalBindingSnapshotForSiteShortcut(action) {
|
||||||
|
var row = document.querySelector(
|
||||||
|
'#customs .shortcut-row[data-action="' + action + '"]'
|
||||||
|
);
|
||||||
|
if (row) {
|
||||||
|
var keyInput = row.querySelector(".customKey");
|
||||||
|
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
|
||||||
|
if (binding) {
|
||||||
|
var valueInput = row.querySelector(".customValue");
|
||||||
|
var value = customActionsNoValues.includes(action)
|
||||||
|
? 0
|
||||||
|
: Number(valueInput && valueInput.value);
|
||||||
|
return { binding: binding, value: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var def = tcDefaults.keyBindings.find(function (b) {
|
||||||
|
return b.action === action;
|
||||||
|
});
|
||||||
|
if (def) {
|
||||||
|
return {
|
||||||
|
binding: normalizeStoredBinding(def),
|
||||||
|
value: def.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { binding: null, value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDefaultBinding(storage, action, code, value) {
|
||||||
if (storage.keyBindings.some((item) => item.action === action)) return;
|
if (storage.keyBindings.some((item) => item.action === action)) return;
|
||||||
|
|
||||||
storage.keyBindings.push(createDefaultBinding(action, key, keyCode, value));
|
storage.keyBindings.push(createDefaultBinding(action, code, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeControllerLocation(location) {
|
function normalizeControllerLocation(location) {
|
||||||
if (controllerLocations.includes(location)) return location;
|
return controllerUtils.normalizeControllerLocation(
|
||||||
return tcDefaults.controllerLocation;
|
location,
|
||||||
|
tcDefaults.controllerLocation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampMarginPxInput(el, fallback) {
|
function clampMarginPxInput(el, fallback) {
|
||||||
var n = parseInt(el && el.value, 10);
|
return controllerUtils.clampControllerMarginPx(el && el.value, fallback);
|
||||||
if (!Number.isFinite(n)) return fallback;
|
}
|
||||||
return Math.min(200, Math.max(0, n));
|
|
||||||
|
function parseFiniteNumberOrFallback(value, fallback) {
|
||||||
|
var numericValue = parseFloat(value);
|
||||||
|
return Number.isFinite(numericValue) ? numericValue : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSiteRuleToggleIcon(toggleButton, action) {
|
||||||
|
if (!toggleButton) return;
|
||||||
|
var iconEl = toggleButton.querySelector(".site-rule-toggle-icon");
|
||||||
|
if (!iconEl) return;
|
||||||
|
|
||||||
|
if (typeof vscIconSvgString === "function" && typeof vscSetSvgContent === "function") {
|
||||||
|
var svgHtml = vscIconSvgString(action, 16);
|
||||||
|
if (svgHtml && vscSetSvgContent(iconEl, svgHtml)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconEl.textContent = action === "chevronUp" ? "\u2212" : "\u2026";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSiteRuleExpandedState(ruleEl, expanded) {
|
||||||
|
if (!ruleEl) return;
|
||||||
|
|
||||||
|
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
||||||
|
var toggleButton = ruleEl.querySelector(".toggle-site-rule");
|
||||||
|
if (ruleBody) {
|
||||||
|
ruleBody.style.display = expanded ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleEl.classList.toggle("collapsed", !expanded);
|
||||||
|
|
||||||
|
if (!toggleButton) return;
|
||||||
|
var label = expanded ? "Collapse site rule" : "Expand site rule";
|
||||||
|
toggleButton.title = label;
|
||||||
|
toggleButton.setAttribute("aria-label", label);
|
||||||
|
toggleButton.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||||
|
updateSiteRuleToggleIcon(toggleButton, expanded ? "chevronUp" : "moreHorizontal");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSiteOverrideContainerState(container, enabled) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.classList.toggle("site-override-disabled", !enabled);
|
||||||
|
container.setAttribute("aria-disabled", enabled ? "false" : "true");
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
container.querySelectorAll("input, select, textarea, button"),
|
||||||
|
function (control) {
|
||||||
|
control.disabled = !enabled;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
container.querySelectorAll(".cb-block"),
|
||||||
|
function (block) {
|
||||||
|
block.draggable = enabled;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySiteRuleOverrideState(ruleEl, checkboxClass, containerClass) {
|
||||||
|
if (!ruleEl) return;
|
||||||
|
var checkbox = ruleEl.querySelector("." + checkboxClass);
|
||||||
|
var container = ruleEl.querySelector("." + containerClass);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.style.display = "block";
|
||||||
|
setSiteOverrideContainerState(container, checkbox ? checkbox.checked : false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
||||||
@@ -310,128 +488,88 @@ function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBindingKey(key) {
|
function normalizeBindingKey(key) {
|
||||||
if (typeof key !== "string" || key.length === 0) return null;
|
return keyBindingUtils.normalizeBindingKey(key);
|
||||||
if (key === "Spacebar") return " ";
|
|
||||||
if (key === "Esc") return "Escape";
|
|
||||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLegacyKeyCode(binding) {
|
function getLegacyKeyCode(binding) {
|
||||||
if (!binding) return null;
|
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
|
||||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
|
||||||
return binding.key;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyKeyCodeToBinding(keyCode) {
|
function legacyBindingKeyToCode(key) {
|
||||||
if (!Number.isInteger(keyCode)) return null;
|
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||||
var normalizedKey = keyCodeToKey[keyCode];
|
}
|
||||||
if (!normalizedKey && keyCode >= 48 && keyCode <= 57) {
|
|
||||||
normalizedKey = String.fromCharCode(keyCode);
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
}
|
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
||||||
if (!normalizedKey && keyCode >= 65 && keyCode <= 90) {
|
}
|
||||||
normalizedKey = String.fromCharCode(keyCode);
|
|
||||||
}
|
function inferBindingCode(binding, fallbackCode) {
|
||||||
return {
|
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
|
||||||
key: normalizeBindingKey(normalizedKey),
|
|
||||||
keyCode: keyCode,
|
|
||||||
code: null,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDisabledBinding() {
|
function createDisabledBinding() {
|
||||||
return {
|
return {
|
||||||
key: null,
|
|
||||||
keyCode: null,
|
|
||||||
code: null,
|
code: null,
|
||||||
disabled: true
|
disabled: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
function normalizeStoredBinding(binding, fallbackCode) {
|
||||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return fallbackBinding;
|
if (!fallbackCode) return null;
|
||||||
|
return {
|
||||||
|
code: fallbackCode,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding.disabled === true ||
|
binding.disabled === true ||
|
||||||
(binding.key === null &&
|
(binding.code === null &&
|
||||||
binding.keyCode === null &&
|
binding.key === null &&
|
||||||
binding.code === null)
|
binding.keyCode === null)
|
||||||
) {
|
) {
|
||||||
return createDisabledBinding();
|
return createDisabledBinding();
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = {
|
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
||||||
key: null,
|
if (!normalizedCode) {
|
||||||
keyCode: null,
|
|
||||||
code:
|
|
||||||
typeof binding.code === "string" && binding.code.length > 0
|
|
||||||
? binding.code
|
|
||||||
: null,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof binding.key === "string") {
|
|
||||||
normalized.key = normalizeBindingKey(binding.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
|
||||||
if (Number.isInteger(legacyKeyCode)) {
|
|
||||||
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
|
|
||||||
if (legacyBinding) {
|
|
||||||
normalized.key = normalized.key || legacyBinding.key;
|
|
||||||
normalized.keyCode = legacyKeyCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isInteger(binding.keyCode)) {
|
|
||||||
normalized.keyCode = binding.keyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalized.key && fallbackBinding) {
|
|
||||||
normalized.key = fallbackBinding.key;
|
|
||||||
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalized = {
|
||||||
|
code: normalizedCode,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBindingCode(code) {
|
||||||
|
if (typeof code !== "string" || code.length === 0) return "";
|
||||||
|
if (bindingCodeAliases[code]) return bindingCodeAliases[code];
|
||||||
|
if (/^Key[A-Z]$/.test(code)) return code.substring(3);
|
||||||
|
if (/^Digit[0-9]$/.test(code)) return code.substring(5);
|
||||||
|
if (/^F([1-9]|1[0-2])$/.test(code)) return code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
function getBindingLabel(binding) {
|
function getBindingLabel(binding) {
|
||||||
if (!binding) return "";
|
if (!binding) return "";
|
||||||
if (binding.disabled) return "";
|
if (binding.disabled) return "";
|
||||||
if (binding.key) {
|
return formatBindingCode(binding.code);
|
||||||
return displayKeyAliases[binding.key] || binding.key;
|
|
||||||
}
|
|
||||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
|
||||||
if (keyCodeAliases[legacyKeyCode]) return keyCodeAliases[legacyKeyCode];
|
|
||||||
if (Number.isInteger(legacyKeyCode)) return String.fromCharCode(legacyKeyCode);
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setShortcutInputBinding(input, binding) {
|
function setShortcutInputBinding(input, binding) {
|
||||||
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
||||||
input.keyCode =
|
|
||||||
binding && Number.isInteger(binding.keyCode) ? binding.keyCode : null;
|
|
||||||
input.value = getBindingLabel(binding);
|
input.value = getBindingLabel(binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureBindingFromEvent(event) {
|
function captureBindingFromEvent(event) {
|
||||||
var normalizedKey = normalizeBindingKey(event.key);
|
if (modifierKeys.has(event.key)) return null;
|
||||||
if (!normalizedKey || modifierKeys.has(normalizedKey)) return null;
|
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
||||||
return {
|
return {
|
||||||
key: normalizedKey,
|
code: event.code,
|
||||||
keyCode: Number.isInteger(event.keyCode) ? event.keyCode : null,
|
|
||||||
code: event.code || null,
|
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -462,8 +600,10 @@ function recordKeyPress(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function inputFilterNumbersOnly(event) {
|
function inputFilterNumbersOnly(event) {
|
||||||
var char = String.fromCharCode(event.keyCode);
|
var char = event.key;
|
||||||
if (
|
if (
|
||||||
|
typeof char !== "string" ||
|
||||||
|
char.length !== 1 ||
|
||||||
!/[\d\.]$/.test(char) ||
|
!/[\d\.]$/.test(char) ||
|
||||||
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
||||||
) {
|
) {
|
||||||
@@ -490,7 +630,15 @@ function updateCustomShortcutInputText(inputItem, bindingOrKeyCode) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShortcutInputBinding(inputItem, legacyKeyCodeToBinding(bindingOrKeyCode));
|
if (typeof bindingOrKeyCode === "string") {
|
||||||
|
setShortcutInputBinding(inputItem, { code: bindingOrKeyCode, disabled: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShortcutInputBinding(
|
||||||
|
inputItem,
|
||||||
|
normalizeStoredBinding({ keyCode: bindingOrKeyCode })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSelectOptions(select, options) {
|
function appendSelectOptions(select, options) {
|
||||||
@@ -526,7 +674,10 @@ function add_shortcut(action, value) {
|
|||||||
valueInput.value = "N/A";
|
valueInput.value = "N/A";
|
||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
valueInput.value = value || 0;
|
valueInput.value = formatSpeedBindingDisplay(
|
||||||
|
action,
|
||||||
|
resolveShortcutValue(action, value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var removeButton = document.createElement("button");
|
var removeButton = document.createElement("button");
|
||||||
@@ -550,23 +701,33 @@ function createKeyBindings(item) {
|
|||||||
var input = item.querySelector(".customKey");
|
var input = item.querySelector(".customKey");
|
||||||
var valueInput = item.querySelector(".customValue");
|
var valueInput = item.querySelector(".customValue");
|
||||||
var predefined = !!item.id;
|
var predefined = !!item.id;
|
||||||
var fallbackKeyCode =
|
var binding = normalizeStoredBinding(input.vscBinding);
|
||||||
predefined && action === "display"
|
|
||||||
? tcDefaults.displayKeyCode
|
|
||||||
: undefined;
|
|
||||||
var binding = normalizeStoredBinding(input.vscBinding, fallbackKeyCode);
|
|
||||||
|
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
|
if (requiredShortcutActions.has(action)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message:
|
||||||
|
"Error: Shortcut for " +
|
||||||
|
(actionLabels[action] || action) +
|
||||||
|
" cannot be empty. Unable to save"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
binding = createDisabledBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.disabled === true && requiredShortcutActions.has(action)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: "Error: Shortcut for " + action + " is invalid. Unable to save"
|
message:
|
||||||
|
"Error: Shortcut for " +
|
||||||
|
(actionLabels[action] || action) +
|
||||||
|
" cannot be empty. Unable to save"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBindings.push({
|
keyBindings.push({
|
||||||
action: action,
|
action: action,
|
||||||
key: binding.key,
|
|
||||||
keyCode: binding.keyCode,
|
|
||||||
code: binding.code,
|
code: binding.code,
|
||||||
disabled: binding.disabled === true,
|
disabled: binding.disabled === true,
|
||||||
value: customActionsNoValues.includes(action)
|
value: customActionsNoValues.includes(action)
|
||||||
@@ -646,8 +807,10 @@ function save_options() {
|
|||||||
document.getElementById("controllerLocation").value
|
document.getElementById("controllerLocation").value
|
||||||
);
|
);
|
||||||
settings.controllerOpacity =
|
settings.controllerOpacity =
|
||||||
parseFloat(document.getElementById("controllerOpacity").value) ||
|
parseFiniteNumberOrFallback(
|
||||||
tcDefaults.controllerOpacity;
|
document.getElementById("controllerOpacity").value,
|
||||||
|
tcDefaults.controllerOpacity
|
||||||
|
);
|
||||||
|
|
||||||
settings.controllerMarginTop = clampMarginPxInput(
|
settings.controllerMarginTop = clampMarginPxInput(
|
||||||
document.getElementById("controllerMarginTop"),
|
document.getElementById("controllerMarginTop"),
|
||||||
@@ -661,6 +824,8 @@ function save_options() {
|
|||||||
settings.keyBindings = keyBindings;
|
settings.keyBindings = keyBindings;
|
||||||
settings.enableSubtitleNudge =
|
settings.enableSubtitleNudge =
|
||||||
document.getElementById("enableSubtitleNudge").checked;
|
document.getElementById("enableSubtitleNudge").checked;
|
||||||
|
settings.subtitleNudgeEnabledByDefault =
|
||||||
|
document.getElementById("subtitleNudgeEnabledByDefault").checked;
|
||||||
settings.subtitleNudgeInterval =
|
settings.subtitleNudgeInterval =
|
||||||
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
|
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
|
||||||
tcDefaults.subtitleNudgeInterval;
|
tcDefaults.subtitleNudgeInterval;
|
||||||
@@ -678,7 +843,7 @@ function save_options() {
|
|||||||
document.getElementById("showPopupControlBar").checked;
|
document.getElementById("showPopupControlBar").checked;
|
||||||
settings.popupMatchHoverControls =
|
settings.popupMatchHoverControls =
|
||||||
document.getElementById("popupMatchHoverControls").checked;
|
document.getElementById("popupMatchHoverControls").checked;
|
||||||
settings.popupControllerButtons = getPopupControlBarOrder();
|
settings.popupControllerButtons = sanitizePopupButtonOrder(getPopupControlBarOrder());
|
||||||
|
|
||||||
// Collect site rules
|
// Collect site rules
|
||||||
settings.siteRules = [];
|
settings.siteRules = [];
|
||||||
@@ -735,13 +900,17 @@ function save_options() {
|
|||||||
|
|
||||||
if (ruleEl.querySelector(".override-opacity").checked) {
|
if (ruleEl.querySelector(".override-opacity").checked) {
|
||||||
rule.controllerOpacity =
|
rule.controllerOpacity =
|
||||||
parseFloat(ruleEl.querySelector(".site-controllerOpacity").value) ||
|
parseFiniteNumberOrFallback(
|
||||||
settings.controllerOpacity;
|
ruleEl.querySelector(".site-controllerOpacity").value,
|
||||||
|
settings.controllerOpacity
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
||||||
rule.enableSubtitleNudge =
|
rule.enableSubtitleNudge =
|
||||||
ruleEl.querySelector(".site-enableSubtitleNudge").checked;
|
ruleEl.querySelector(".site-enableSubtitleNudge").checked;
|
||||||
|
rule.subtitleNudgeEnabledByDefault =
|
||||||
|
ruleEl.querySelector(".site-subtitleNudgeEnabledByDefault").checked;
|
||||||
var nudgeIv = parseInt(
|
var nudgeIv = parseInt(
|
||||||
ruleEl.querySelector(".site-subtitleNudgeInterval").value,
|
ruleEl.querySelector(".site-subtitleNudgeInterval").value,
|
||||||
10
|
10
|
||||||
@@ -767,33 +936,52 @@ function save_options() {
|
|||||||
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
||||||
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
||||||
if (popupActiveZone) {
|
if (popupActiveZone) {
|
||||||
rule.popupControllerButtons = readControlBarOrder(popupActiveZone);
|
rule.popupControllerButtons = sanitizePopupButtonOrder(
|
||||||
|
readControlBarOrder(popupActiveZone)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
||||||
var shortcuts = [];
|
var shortcuts = [];
|
||||||
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
||||||
|
if (saveError) return;
|
||||||
var action = shortcutRow.dataset.action;
|
var action = shortcutRow.dataset.action;
|
||||||
var keyInput = shortcutRow.querySelector(".customKey");
|
var keyInput = shortcutRow.querySelector(".customKey");
|
||||||
var valueInput = shortcutRow.querySelector(".customValue");
|
var valueInput = shortcutRow.querySelector(".customValue");
|
||||||
var forceCheckbox = shortcutRow.querySelector(".customForce");
|
var forceCheckbox = shortcutRow.querySelector(".customForce");
|
||||||
var binding = normalizeStoredBinding(keyInput.vscBinding);
|
var binding = normalizeStoredBinding(keyInput.vscBinding);
|
||||||
|
|
||||||
if (binding) {
|
if (!binding) {
|
||||||
shortcuts.push({
|
if (requiredShortcutActions.has(action)) {
|
||||||
action: action,
|
saveError =
|
||||||
key: binding.key,
|
"Error: Site rule shortcut for " +
|
||||||
keyCode: binding.keyCode,
|
(actionLabels[action] || action) +
|
||||||
code: binding.code,
|
" cannot be empty. Unable to save";
|
||||||
disabled: binding.disabled === true,
|
return;
|
||||||
value: customActionsNoValues.includes(action)
|
}
|
||||||
? 0
|
binding = createDisabledBinding();
|
||||||
: Number(valueInput.value),
|
|
||||||
force: forceCheckbox ? forceCheckbox.checked : false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (binding.disabled === true && requiredShortcutActions.has(action)) {
|
||||||
|
saveError =
|
||||||
|
"Error: Site rule shortcut for " +
|
||||||
|
(actionLabels[action] || action) +
|
||||||
|
" cannot be empty. Unable to save";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcuts.push({
|
||||||
|
action: action,
|
||||||
|
code: binding.code,
|
||||||
|
disabled: binding.disabled === true,
|
||||||
|
value: customActionsNoValues.includes(action)
|
||||||
|
? 0
|
||||||
|
: Number(valueInput.value),
|
||||||
|
force: forceCheckbox ? forceCheckbox.checked : false
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
if (saveError) return;
|
||||||
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,69 +1007,22 @@ function save_options() {
|
|||||||
|
|
||||||
function ensureAllDefaultBindings(storage) {
|
function ensureAllDefaultBindings(storage) {
|
||||||
tcDefaults.keyBindings.forEach((binding) => {
|
tcDefaults.keyBindings.forEach((binding) => {
|
||||||
// Special case for "display" to support legacy displayKeyCode
|
ensureDefaultBinding(storage, binding.action, binding.code, binding.value);
|
||||||
if (binding.action === "display" && storage.displayKeyCode) {
|
|
||||||
ensureDefaultBinding(storage, "display", "V", storage.displayKeyCode, 0);
|
|
||||||
} else {
|
|
||||||
ensureDefaultBinding(
|
|
||||||
storage,
|
|
||||||
binding.action,
|
|
||||||
binding.key,
|
|
||||||
binding.keyCode,
|
|
||||||
binding.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateLegacyBlacklist(storage) {
|
function addSiteRuleShortcut(rowsEl, action, binding, value, force) {
|
||||||
if (!storage.blacklist || typeof storage.blacklist !== "string") {
|
if (!rowsEl) return;
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var siteRules = [];
|
|
||||||
var lines = storage.blacklist.split("\n");
|
|
||||||
|
|
||||||
lines.forEach((line) => {
|
|
||||||
var pattern = line.replace(regStrip, "");
|
|
||||||
if (pattern.length === 0) return;
|
|
||||||
|
|
||||||
siteRules.push({
|
|
||||||
pattern: pattern,
|
|
||||||
disableExtension: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return siteRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSiteRuleShortcut(container, action, binding, value, force) {
|
|
||||||
var div = document.createElement("div");
|
var div = document.createElement("div");
|
||||||
div.setAttribute("class", "shortcut-row customs");
|
div.setAttribute("class", "shortcut-row customs");
|
||||||
div.dataset.action = action;
|
div.dataset.action = action;
|
||||||
|
|
||||||
var actionLabel = document.createElement("div");
|
var actionLabel = document.createElement("div");
|
||||||
actionLabel.className = "shortcut-label";
|
actionLabel.className = "shortcut-label";
|
||||||
var actionLabels = {
|
|
||||||
display: "Show/hide controller",
|
|
||||||
move: "Move controller",
|
|
||||||
slower: "Decrease speed",
|
|
||||||
faster: "Increase speed",
|
|
||||||
rewind: "Rewind",
|
|
||||||
advance: "Advance",
|
|
||||||
reset: "Reset speed",
|
|
||||||
fast: "Preferred speed",
|
|
||||||
muted: "Mute",
|
|
||||||
pause: "Pause",
|
|
||||||
mark: "Set marker",
|
|
||||||
jump: "Jump to marker",
|
|
||||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
|
||||||
};
|
|
||||||
var actionLabelText = actionLabels[action] || action;
|
var actionLabelText = actionLabels[action] || action;
|
||||||
if (action === "toggleSubtitleNudge") {
|
if (action === "toggleSubtitleNudge") {
|
||||||
// Check if the site rule is for YouTube.
|
var ruleEl = rowsEl.closest(".site-rule");
|
||||||
// We look up the pattern from the site rule element this container belongs to.
|
|
||||||
var ruleEl = container.closest(".site-rule");
|
|
||||||
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
|
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
|
||||||
if (!pattern.toLowerCase().includes("youtube.com")) {
|
if (!pattern.toLowerCase().includes("youtube.com")) {
|
||||||
actionLabelText += " (only for YouTube embeds)";
|
actionLabelText += " (only for YouTube embeds)";
|
||||||
@@ -899,9 +1040,14 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
|||||||
valueInput.className = "customValue";
|
valueInput.className = "customValue";
|
||||||
valueInput.type = "text";
|
valueInput.type = "text";
|
||||||
valueInput.placeholder = "value (0.10)";
|
valueInput.placeholder = "value (0.10)";
|
||||||
valueInput.value = value || 0;
|
|
||||||
if (customActionsNoValues.includes(action)) {
|
if (customActionsNoValues.includes(action)) {
|
||||||
|
valueInput.value = "N/A";
|
||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
valueInput.value = formatSpeedBindingDisplay(
|
||||||
|
action,
|
||||||
|
resolveShortcutValue(action, value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var forceLabel = document.createElement("label");
|
var forceLabel = document.createElement("label");
|
||||||
@@ -920,12 +1066,18 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
|||||||
forceLabel.appendChild(forceCheckbox);
|
forceLabel.appendChild(forceCheckbox);
|
||||||
forceLabel.appendChild(forceText);
|
forceLabel.appendChild(forceText);
|
||||||
|
|
||||||
|
var removeButton = document.createElement("button");
|
||||||
|
removeButton.className = "removeParent";
|
||||||
|
removeButton.type = "button";
|
||||||
|
removeButton.textContent = "\u00d7";
|
||||||
|
|
||||||
div.appendChild(actionLabel);
|
div.appendChild(actionLabel);
|
||||||
div.appendChild(keyInput);
|
div.appendChild(keyInput);
|
||||||
div.appendChild(valueInput);
|
div.appendChild(valueInput);
|
||||||
div.appendChild(forceLabel);
|
div.appendChild(forceLabel);
|
||||||
|
div.appendChild(removeButton);
|
||||||
|
|
||||||
container.appendChild(div);
|
rowsEl.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSiteRule(rule) {
|
function createSiteRule(rule) {
|
||||||
@@ -937,9 +1089,7 @@ function createSiteRule(rule) {
|
|||||||
ruleEl.querySelector(".site-pattern").value = pattern;
|
ruleEl.querySelector(".site-pattern").value = pattern;
|
||||||
|
|
||||||
// Make the rule body collapsed by default
|
// Make the rule body collapsed by default
|
||||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
setSiteRuleExpandedState(ruleEl, false);
|
||||||
ruleBody.style.display = "none";
|
|
||||||
ruleEl.classList.add("collapsed");
|
|
||||||
|
|
||||||
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
||||||
var contentEl = ruleEl.querySelector(".site-rule-content");
|
var contentEl = ruleEl.querySelector(".site-rule-content");
|
||||||
@@ -974,111 +1124,104 @@ function createSiteRule(rule) {
|
|||||||
];
|
];
|
||||||
var hasPlacementOverride =
|
var hasPlacementOverride =
|
||||||
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
||||||
if (hasPlacementOverride) {
|
ruleEl.querySelector(".override-placement").checked = Boolean(hasPlacementOverride);
|
||||||
ruleEl.querySelector(".override-placement").checked = true;
|
|
||||||
ruleEl.querySelector(".site-placement-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-placement", "site-placement-container");
|
||||||
|
|
||||||
if (rule && rule.startHidden !== undefined) {
|
ruleEl.querySelector(".override-visibility").checked = Boolean(
|
||||||
ruleEl.querySelector(".override-visibility").checked = true;
|
rule && rule.startHidden !== undefined
|
||||||
ruleEl.querySelector(".site-visibility-container").style.display = "block";
|
);
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-visibility", "site-visibility-container");
|
||||||
|
|
||||||
if (
|
var hasAutohideOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.hideWithControls !== undefined ||
|
(rule.hideWithControls !== undefined ||
|
||||||
rule.hideWithControlsTimer !== undefined)
|
rule.hideWithControlsTimer !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-autohide").checked = true;
|
ruleEl.querySelector(".override-autohide").checked = hasAutohideOverride;
|
||||||
ruleEl.querySelector(".site-autohide-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-autohide", "site-autohide-container");
|
||||||
|
|
||||||
if (
|
var hasPlaybackOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.rememberSpeed !== undefined ||
|
(rule.rememberSpeed !== undefined ||
|
||||||
rule.forceLastSavedSpeed !== undefined ||
|
rule.forceLastSavedSpeed !== undefined ||
|
||||||
rule.audioBoolean !== undefined)
|
rule.audioBoolean !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-playback").checked = true;
|
ruleEl.querySelector(".override-playback").checked = hasPlaybackOverride;
|
||||||
ruleEl.querySelector(".site-playback-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-playback", "site-playback-container");
|
||||||
|
|
||||||
if (rule && rule.controllerOpacity !== undefined) {
|
ruleEl.querySelector(".override-opacity").checked = Boolean(
|
||||||
ruleEl.querySelector(".override-opacity").checked = true;
|
rule && rule.controllerOpacity !== undefined
|
||||||
ruleEl.querySelector(".site-opacity-container").style.display = "block";
|
);
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-opacity", "site-opacity-container");
|
||||||
|
|
||||||
if (
|
var hasSubtitleNudgeOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.enableSubtitleNudge !== undefined ||
|
(rule.enableSubtitleNudge !== undefined ||
|
||||||
|
rule.subtitleNudgeEnabledByDefault !== undefined ||
|
||||||
rule.subtitleNudgeInterval !== undefined)
|
rule.subtitleNudgeInterval !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-subtitleNudge").checked = true;
|
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
|
||||||
ruleEl.querySelector(".site-subtitleNudge-container").style.display =
|
|
||||||
"block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
||||||
|
syncSiteRuleField(ruleEl, rule, "subtitleNudgeEnabledByDefault", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
||||||
|
applySiteRuleOverrideState(
|
||||||
|
ruleEl,
|
||||||
|
"override-subtitleNudge",
|
||||||
|
"site-subtitleNudge-container"
|
||||||
|
);
|
||||||
|
|
||||||
if (rule && Array.isArray(rule.controllerButtons)) {
|
var hasControlbarOverride = Boolean(rule && Array.isArray(rule.controllerButtons));
|
||||||
ruleEl.querySelector(".override-controlbar").checked = true;
|
ruleEl.querySelector(".override-controlbar").checked = hasControlbarOverride;
|
||||||
var cbContainer = ruleEl.querySelector(".site-controlbar-container");
|
populateControlBarZones(
|
||||||
cbContainer.style.display = "block";
|
ruleEl.querySelector(".site-cb-active"),
|
||||||
populateControlBarZones(
|
ruleEl.querySelector(".site-cb-available"),
|
||||||
ruleEl.querySelector(".site-cb-active"),
|
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
|
||||||
ruleEl.querySelector(".site-cb-available"),
|
);
|
||||||
rule.controllerButtons
|
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
var hasPopupControlbarOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.showPopupControlBar !== undefined ||
|
(rule.showPopupControlBar !== undefined ||
|
||||||
Array.isArray(rule.popupControllerButtons))
|
Array.isArray(rule.popupControllerButtons))
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
ruleEl.querySelector(".override-popup-controlbar").checked =
|
||||||
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
|
hasPopupControlbarOverride;
|
||||||
popupCbContainer.style.display = "block";
|
populateControlBarZones(
|
||||||
var sitePopupActive = ruleEl.querySelector(".site-popup-cb-active");
|
ruleEl.querySelector(".site-popup-cb-active"),
|
||||||
var sitePopupAvailable = ruleEl.querySelector(".site-popup-cb-available");
|
ruleEl.querySelector(".site-popup-cb-available"),
|
||||||
if (Array.isArray(rule.popupControllerButtons)) {
|
hasPopupControlbarOverride && Array.isArray(rule.popupControllerButtons)
|
||||||
populateControlBarZones(
|
? sanitizePopupButtonOrder(rule.popupControllerButtons)
|
||||||
sitePopupActive,
|
: getPopupControlBarOrder(),
|
||||||
sitePopupAvailable,
|
function (id) {
|
||||||
rule.popupControllerButtons
|
return !popupExcludedButtonIds.has(id);
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
sitePopupActive &&
|
|
||||||
sitePopupAvailable &&
|
|
||||||
sitePopupActive.children.length === 0
|
|
||||||
) {
|
|
||||||
populateControlBarZones(
|
|
||||||
sitePopupActive,
|
|
||||||
sitePopupAvailable,
|
|
||||||
getPopupControlBarOrder()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
||||||
|
applySiteRuleOverrideState(
|
||||||
|
ruleEl,
|
||||||
|
"override-popup-controlbar",
|
||||||
|
"site-popup-controlbar-container"
|
||||||
|
);
|
||||||
|
|
||||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
var hasShortcutOverride = Boolean(
|
||||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
|
||||||
var container = ruleEl.querySelector(".site-shortcuts-container");
|
);
|
||||||
container.style.display = "block";
|
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
|
||||||
|
var rowsEl = ruleEl.querySelector(".site-shortcuts-rows");
|
||||||
|
if (hasShortcutOverride) {
|
||||||
rule.shortcuts.forEach((shortcut) => {
|
rule.shortcuts.forEach((shortcut) => {
|
||||||
addSiteRuleShortcut(
|
addSiteRuleShortcut(
|
||||||
container,
|
rowsEl,
|
||||||
shortcut.action,
|
shortcut.action,
|
||||||
shortcut,
|
shortcut,
|
||||||
shortcut.value,
|
shortcut.value,
|
||||||
@@ -1086,16 +1229,12 @@ function createSiteRule(rule) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
||||||
|
refreshSiteRuleAddShortcutSelector(ruleEl);
|
||||||
|
|
||||||
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateDefaultSiteShortcuts(container) {
|
|
||||||
tcDefaults.keyBindings.forEach((binding) => {
|
|
||||||
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createControlBarBlock(buttonId) {
|
function createControlBarBlock(buttonId) {
|
||||||
var def = controllerButtonDefs[buttonId];
|
var def = controllerButtonDefs[buttonId];
|
||||||
if (!def) return null;
|
if (!def) return null;
|
||||||
@@ -1110,7 +1249,7 @@ function createControlBarBlock(buttonId) {
|
|||||||
|
|
||||||
var icon = document.createElement("span");
|
var icon = document.createElement("span");
|
||||||
icon.className = "cb-icon";
|
icon.className = "cb-icon";
|
||||||
icon.textContent = def.icon;
|
fillControlBarIconElement(icon, buttonId);
|
||||||
|
|
||||||
var label = document.createElement("span");
|
var label = document.createElement("span");
|
||||||
label.className = "cb-label";
|
label.className = "cb-label";
|
||||||
@@ -1123,16 +1262,23 @@ function createControlBarBlock(buttonId) {
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateControlBarZones(activeZone, availableZone, activeIds) {
|
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
|
||||||
activeZone.innerHTML = "";
|
vscClearElement(activeZone);
|
||||||
availableZone.innerHTML = "";
|
vscClearElement(availableZone);
|
||||||
|
|
||||||
|
var allowed = function (id) {
|
||||||
|
if (!controllerButtonDefs[id]) return false;
|
||||||
|
return typeof allowButtonId === "function" ? Boolean(allowButtonId(id)) : true;
|
||||||
|
};
|
||||||
|
|
||||||
activeIds.forEach(function (id) {
|
activeIds.forEach(function (id) {
|
||||||
|
if (!allowed(id)) return;
|
||||||
var block = createControlBarBlock(id);
|
var block = createControlBarBlock(id);
|
||||||
if (block) activeZone.appendChild(block);
|
if (block) activeZone.appendChild(block);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(controllerButtonDefs).forEach(function (id) {
|
Object.keys(controllerButtonDefs).forEach(function (id) {
|
||||||
|
if (!allowed(id)) return;
|
||||||
if (!activeIds.includes(id)) {
|
if (!activeIds.includes(id)) {
|
||||||
var block = createControlBarBlock(id);
|
var block = createControlBarBlock(id);
|
||||||
if (block) availableZone.appendChild(block);
|
if (block) availableZone.appendChild(block);
|
||||||
@@ -1160,15 +1306,21 @@ function getControlBarOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populatePopupControlBarEditor(activeIds) {
|
function populatePopupControlBarEditor(activeIds) {
|
||||||
|
var popupActiveIds = sanitizePopupButtonOrder(activeIds);
|
||||||
populateControlBarZones(
|
populateControlBarZones(
|
||||||
document.getElementById("popupControlBarActive"),
|
document.getElementById("popupControlBarActive"),
|
||||||
document.getElementById("popupControlBarAvailable"),
|
document.getElementById("popupControlBarAvailable"),
|
||||||
activeIds
|
popupActiveIds,
|
||||||
|
function (id) {
|
||||||
|
return !popupExcludedButtonIds.has(id);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPopupControlBarOrder() {
|
function getPopupControlBarOrder() {
|
||||||
return readControlBarOrder(document.getElementById("popupControlBarActive"));
|
return sanitizePopupButtonOrder(
|
||||||
|
readControlBarOrder(document.getElementById("popupControlBarActive"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePopupEditorDisabledState() {
|
function updatePopupEditorDisabledState() {
|
||||||
@@ -1265,8 +1417,218 @@ function initControlBarEditor() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lucidePickerSelectedSlug = null;
|
||||||
|
var lucideSearchTimer = null;
|
||||||
|
|
||||||
|
function setLucideStatus(msg) {
|
||||||
|
var el = document.getElementById("lucideIconStatus");
|
||||||
|
if (el) el.textContent = msg || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function repaintAllCbIconsFromCustomMap() {
|
||||||
|
document.querySelectorAll(".cb-block .cb-icon").forEach(function (icon) {
|
||||||
|
var block = icon.closest(".cb-block");
|
||||||
|
if (!block) return;
|
||||||
|
fillControlBarIconElement(icon, block.dataset.buttonId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCustomButtonIcons(map, callback) {
|
||||||
|
chrome.storage.local.set({ customButtonIcons: map }, function () {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
setLucideStatus(
|
||||||
|
"Could not save icons: " + chrome.runtime.lastError.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customButtonIconsLive = map;
|
||||||
|
if (callback) callback();
|
||||||
|
repaintAllCbIconsFromCustomMap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLucideButtonIconsUI() {
|
||||||
|
var actionSel = document.getElementById("lucideIconActionSelect");
|
||||||
|
var searchInput = document.getElementById("lucideIconSearch");
|
||||||
|
var resultsEl = document.getElementById("lucideIconResults");
|
||||||
|
var previewEl = document.getElementById("lucideIconPreview");
|
||||||
|
if (!actionSel || !searchInput || !resultsEl || !previewEl) return;
|
||||||
|
if (typeof getLucideTagsMap !== "function") return;
|
||||||
|
|
||||||
|
if (!actionSel.dataset.lucideInit) {
|
||||||
|
actionSel.dataset.lucideInit = "1";
|
||||||
|
vscClearElement(actionSel);
|
||||||
|
Object.keys(controllerButtonDefs).forEach(function (aid) {
|
||||||
|
if (aid === "nudge") {
|
||||||
|
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
|
||||||
|
var o2 = document.createElement("option");
|
||||||
|
o2.value = subId;
|
||||||
|
o2.textContent =
|
||||||
|
lucideSubtitleNudgeActionLabels[subId] + " (" + subId + ")";
|
||||||
|
actionSel.appendChild(o2);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var o = document.createElement("option");
|
||||||
|
o.value = aid;
|
||||||
|
o.textContent =
|
||||||
|
controllerButtonDefs[aid].name + " (" + aid + ")";
|
||||||
|
actionSel.appendChild(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(slugs) {
|
||||||
|
vscClearElement(resultsEl);
|
||||||
|
slugs.forEach(function (slug) {
|
||||||
|
var b = document.createElement("button");
|
||||||
|
b.type = "button";
|
||||||
|
b.className = "lucide-result-tile";
|
||||||
|
b.dataset.slug = slug;
|
||||||
|
b.title = slug;
|
||||||
|
b.setAttribute("aria-label", slug);
|
||||||
|
if (slug === lucidePickerSelectedSlug) {
|
||||||
|
b.classList.add("lucide-picked");
|
||||||
|
}
|
||||||
|
var url =
|
||||||
|
typeof lucideIconSvgUrl === "function" ? lucideIconSvgUrl(slug) : "";
|
||||||
|
if (url) {
|
||||||
|
var img = document.createElement("img");
|
||||||
|
img.className = "lucide-result-thumb";
|
||||||
|
img.src = url;
|
||||||
|
img.alt = "";
|
||||||
|
img.loading = "lazy";
|
||||||
|
b.appendChild(img);
|
||||||
|
} else {
|
||||||
|
b.textContent = slug.slice(0, 3);
|
||||||
|
}
|
||||||
|
b.addEventListener("click", function () {
|
||||||
|
lucidePickerSelectedSlug = slug;
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
resultsEl.querySelectorAll("button"),
|
||||||
|
function (x) {
|
||||||
|
x.classList.toggle("lucide-picked", x.dataset.slug === slug);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fetchLucideSvg(slug)
|
||||||
|
.then(function (txt) {
|
||||||
|
var safe = sanitizeLucideSvg(txt);
|
||||||
|
if (!safe) throw new Error("Bad SVG");
|
||||||
|
if (!vscSetSvgContent(previewEl, safe)) {
|
||||||
|
throw new Error("Preview render failed");
|
||||||
|
}
|
||||||
|
setLucideStatus("Preview: " + slug);
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
vscClearElement(previewEl);
|
||||||
|
setLucideStatus(
|
||||||
|
"Could not load: " + slug + " — " + e.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
resultsEl.appendChild(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchInput.dataset.lucideBound) {
|
||||||
|
searchInput.dataset.lucideBound = "1";
|
||||||
|
searchInput.addEventListener("input", function () {
|
||||||
|
clearTimeout(lucideSearchTimer);
|
||||||
|
lucideSearchTimer = setTimeout(function () {
|
||||||
|
getLucideTagsMap(chrome.storage.local, false)
|
||||||
|
.then(function (map) {
|
||||||
|
var q = searchInput.value;
|
||||||
|
if (!q.trim()) {
|
||||||
|
vscClearElement(resultsEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderResults(searchLucideSlugs(map, q, 48));
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Icon list error: " + e.message);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyBtn = document.getElementById("lucideIconApply");
|
||||||
|
if (applyBtn && !applyBtn.dataset.lucideBound) {
|
||||||
|
applyBtn.dataset.lucideBound = "1";
|
||||||
|
applyBtn.addEventListener("click", function () {
|
||||||
|
var action = actionSel.value;
|
||||||
|
var slug = lucidePickerSelectedSlug;
|
||||||
|
if (!action || !slug) {
|
||||||
|
setLucideStatus("Pick an action and click an icon first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchLucideSvg(slug)
|
||||||
|
.then(function (txt) {
|
||||||
|
var safe = sanitizeLucideSvg(txt);
|
||||||
|
if (!safe) throw new Error("Sanitize failed");
|
||||||
|
var next = Object.assign({}, customButtonIconsLive);
|
||||||
|
next[action] = { slug: slug, svg: safe };
|
||||||
|
persistCustomButtonIcons(next, function () {
|
||||||
|
setLucideStatus(
|
||||||
|
"Saved " +
|
||||||
|
slug +
|
||||||
|
" for " +
|
||||||
|
action +
|
||||||
|
". Reload pages for the hover bar."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Apply failed: " + e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clrOne = document.getElementById("lucideIconClearAction");
|
||||||
|
if (clrOne && !clrOne.dataset.lucideBound) {
|
||||||
|
clrOne.dataset.lucideBound = "1";
|
||||||
|
clrOne.addEventListener("click", function () {
|
||||||
|
var action = actionSel.value;
|
||||||
|
if (!action) return;
|
||||||
|
var next = Object.assign({}, customButtonIconsLive);
|
||||||
|
delete next[action];
|
||||||
|
persistCustomButtonIcons(next, function () {
|
||||||
|
setLucideStatus("Cleared custom icon for " + action + ".");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clrAll = document.getElementById("lucideIconClearAll");
|
||||||
|
if (clrAll && !clrAll.dataset.lucideBound) {
|
||||||
|
clrAll.dataset.lucideBound = "1";
|
||||||
|
clrAll.addEventListener("click", function () {
|
||||||
|
persistCustomButtonIcons({}, function () {
|
||||||
|
setLucideStatus("All custom icons cleared.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var reloadTags = document.getElementById("lucideIconReloadTags");
|
||||||
|
if (reloadTags && !reloadTags.dataset.lucideBound) {
|
||||||
|
reloadTags.dataset.lucideBound = "1";
|
||||||
|
reloadTags.addEventListener("click", function () {
|
||||||
|
getLucideTagsMap(chrome.storage.local, true)
|
||||||
|
.then(function () {
|
||||||
|
setLucideStatus("Icon name list refreshed.");
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Refresh failed: " + e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function restore_options() {
|
function restore_options() {
|
||||||
chrome.storage.sync.get(tcDefaults, function (storage) {
|
chrome.storage.sync.get(tcDefaults, function (storage) {
|
||||||
|
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||||
|
customButtonIconsLive =
|
||||||
|
loc && loc.customButtonIcons && typeof loc.customButtonIcons === "object"
|
||||||
|
? loc.customButtonIcons
|
||||||
|
: {};
|
||||||
|
|
||||||
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
||||||
document.getElementById("forceLastSavedSpeed").checked =
|
document.getElementById("forceLastSavedSpeed").checked =
|
||||||
storage.forceLastSavedSpeed;
|
storage.forceLastSavedSpeed;
|
||||||
@@ -1295,6 +1657,8 @@ function restore_options() {
|
|||||||
storage.showPopupControlBar !== false;
|
storage.showPopupControlBar !== false;
|
||||||
document.getElementById("enableSubtitleNudge").checked =
|
document.getElementById("enableSubtitleNudge").checked =
|
||||||
storage.enableSubtitleNudge;
|
storage.enableSubtitleNudge;
|
||||||
|
document.getElementById("subtitleNudgeEnabledByDefault").checked =
|
||||||
|
storage.subtitleNudgeEnabledByDefault;
|
||||||
document.getElementById("subtitleNudgeInterval").value =
|
document.getElementById("subtitleNudgeInterval").value =
|
||||||
storage.subtitleNudgeInterval;
|
storage.subtitleNudgeInterval;
|
||||||
|
|
||||||
@@ -1329,24 +1693,19 @@ function restore_options() {
|
|||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
}
|
}
|
||||||
} else if (valueInput) {
|
} else if (valueInput) {
|
||||||
valueInput.value = item.value;
|
valueInput.value = formatSpeedBindingDisplay(item.action, item.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshAddShortcutSelector();
|
refreshAddShortcutSelector();
|
||||||
|
|
||||||
// Load site rules (use defaults if none in storage or if storage has empty array)
|
// Load site rules (use defaults if none in storage or empty array)
|
||||||
var siteRules = Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
var siteRules =
|
||||||
? storage.siteRules
|
Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
||||||
: (storage.blacklist ? migrateLegacyBlacklist(storage) : (tcDefaults.siteRules || []));
|
? storage.siteRules
|
||||||
|
: tcDefaults.siteRules || [];
|
||||||
|
|
||||||
// If we migrated from blacklist, save the new format
|
vscClearElement(document.getElementById("siteRulesContainer"));
|
||||||
if (storage.blacklist && siteRules.length > 0) {
|
|
||||||
chrome.storage.sync.set({ siteRules: siteRules });
|
|
||||||
chrome.storage.sync.remove(["blacklist"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("siteRulesContainer").innerHTML = "";
|
|
||||||
if (siteRules.length > 0) {
|
if (siteRules.length > 0) {
|
||||||
siteRules.forEach((rule) => {
|
siteRules.forEach((rule) => {
|
||||||
if (rule && rule.pattern) {
|
if (rule && rule.pattern) {
|
||||||
@@ -1368,12 +1727,20 @@ function restore_options() {
|
|||||||
: tcDefaults.popupControllerButtons;
|
: tcDefaults.popupControllerButtons;
|
||||||
populatePopupControlBarEditor(popupButtons);
|
populatePopupControlBarEditor(popupButtons);
|
||||||
updatePopupEditorDisabledState();
|
updatePopupEditorDisabledState();
|
||||||
|
|
||||||
|
initLucideButtonIconsUI();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore_defaults() {
|
function restore_defaults() {
|
||||||
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
||||||
|
|
||||||
|
chrome.storage.local.remove(
|
||||||
|
["customButtonIcons", "lucideTagsCacheV1", "lucideTagsCacheV1At"],
|
||||||
|
function () {}
|
||||||
|
);
|
||||||
|
|
||||||
chrome.storage.sync.set(tcDefaults, function () {
|
chrome.storage.sync.set(tcDefaults, function () {
|
||||||
restore_options();
|
restore_options();
|
||||||
var status = document.getElementById("status");
|
var status = document.getElementById("status");
|
||||||
@@ -1437,31 +1804,33 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
eventCaller(event, "customKey", recordKeyPress)
|
eventCaller(event, "customKey", recordKeyPress)
|
||||||
);
|
);
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (event.target.classList.contains("removeParent")) {
|
var target = event.target;
|
||||||
event.target.parentNode.remove();
|
var targetEl = target && target.closest ? target : target.parentElement;
|
||||||
|
if (!targetEl) return;
|
||||||
|
|
||||||
|
var removeParentButton = targetEl.closest(".removeParent");
|
||||||
|
if (removeParentButton) {
|
||||||
|
var removedRow = removeParentButton.parentNode;
|
||||||
|
var siteRuleForShortcut = removedRow.closest(".site-rule");
|
||||||
|
removedRow.remove();
|
||||||
refreshAddShortcutSelector();
|
refreshAddShortcutSelector();
|
||||||
return;
|
if (siteRuleForShortcut) {
|
||||||
}
|
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
|
||||||
if (event.target.classList.contains("remove-site-rule")) {
|
|
||||||
event.target.closest(".site-rule").remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.target.classList.contains("toggle-site-rule")) {
|
|
||||||
var ruleEl = event.target.closest(".site-rule");
|
|
||||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
|
||||||
var isCollapsed = ruleEl.classList.contains("collapsed");
|
|
||||||
|
|
||||||
if (isCollapsed) {
|
|
||||||
ruleBody.style.display = "block";
|
|
||||||
ruleEl.classList.remove("collapsed");
|
|
||||||
event.target.textContent = "\u2212";
|
|
||||||
} else {
|
|
||||||
ruleBody.style.display = "none";
|
|
||||||
ruleEl.classList.add("collapsed");
|
|
||||||
event.target.textContent = "\u002b";
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||||
|
if (removeSiteRuleButton) {
|
||||||
|
removeSiteRuleButton.closest(".site-rule").remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var toggleButton = targetEl.closest(".toggle-site-rule");
|
||||||
|
if (toggleButton) {
|
||||||
|
var ruleEl = toggleButton.closest(".site-rule");
|
||||||
|
var isCollapsed = ruleEl.classList.contains("collapsed");
|
||||||
|
setSiteRuleExpandedState(ruleEl, isCollapsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("change", (event) => {
|
document.addEventListener("change", (event) => {
|
||||||
if (event.target.classList.contains("customDo")) {
|
if (event.target.classList.contains("customDo")) {
|
||||||
@@ -1474,6 +1843,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.target.classList.contains("site-add-shortcut-selector")) {
|
||||||
|
var action = event.target.value;
|
||||||
|
if (!action) return;
|
||||||
|
var siteRuleRoot = event.target.closest(".site-rule");
|
||||||
|
var rows = siteRuleRoot && siteRuleRoot.querySelector(".site-shortcuts-rows");
|
||||||
|
if (rows) {
|
||||||
|
var snap = getGlobalBindingSnapshotForSiteShortcut(action);
|
||||||
|
addSiteRuleShortcut(
|
||||||
|
rows,
|
||||||
|
action,
|
||||||
|
snap.binding,
|
||||||
|
snap.value,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
|
||||||
|
}
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Site rule: show/hide optional override sections
|
// Site rule: show/hide optional override sections
|
||||||
var siteOverrideContainers = {
|
var siteOverrideContainers = {
|
||||||
"override-placement": "site-placement-container",
|
"override-placement": "site-placement-container",
|
||||||
@@ -1481,7 +1870,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
"override-autohide": "site-autohide-container",
|
"override-autohide": "site-autohide-container",
|
||||||
"override-playback": "site-playback-container",
|
"override-playback": "site-playback-container",
|
||||||
"override-opacity": "site-opacity-container",
|
"override-opacity": "site-opacity-container",
|
||||||
"override-subtitleNudge": "site-subtitleNudge-container"
|
"override-subtitleNudge": "site-subtitleNudge-container",
|
||||||
|
"override-controlbar": "site-controlbar-container",
|
||||||
|
"override-popup-controlbar": "site-popup-controlbar-container",
|
||||||
|
"override-shortcuts": "site-shortcuts-container"
|
||||||
};
|
};
|
||||||
for (var ocb in siteOverrideContainers) {
|
for (var ocb in siteOverrideContainers) {
|
||||||
if (event.target.classList.contains(ocb)) {
|
if (event.target.classList.contains(ocb)) {
|
||||||
@@ -1490,75 +1882,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
"." + siteOverrideContainers[ocb]
|
"." + siteOverrideContainers[ocb]
|
||||||
);
|
);
|
||||||
if (targetBox) {
|
if (targetBox) {
|
||||||
targetBox.style.display = event.target.checked ? "block" : "none";
|
setSiteOverrideContainerState(targetBox, event.target.checked);
|
||||||
|
}
|
||||||
|
if (ocb === "override-shortcuts") {
|
||||||
|
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle site rule override checkboxes
|
|
||||||
if (event.target.classList.contains("override-shortcuts")) {
|
|
||||||
var container = event.target
|
|
||||||
.closest(".site-rule-shortcuts")
|
|
||||||
.querySelector(".site-shortcuts-container");
|
|
||||||
if (event.target.checked) {
|
|
||||||
container.style.display = "block";
|
|
||||||
if (container.children.length === 0) {
|
|
||||||
populateDefaultSiteShortcuts(container);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
container.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.target.classList.contains("override-controlbar")) {
|
|
||||||
var cbContainer = event.target
|
|
||||||
.closest(".site-rule-controlbar")
|
|
||||||
.querySelector(".site-controlbar-container");
|
|
||||||
if (event.target.checked) {
|
|
||||||
cbContainer.style.display = "block";
|
|
||||||
var activeZone = cbContainer.querySelector(".site-cb-active");
|
|
||||||
var availableZone = cbContainer.querySelector(".site-cb-available");
|
|
||||||
if (
|
|
||||||
activeZone &&
|
|
||||||
availableZone &&
|
|
||||||
activeZone.children.length === 0 &&
|
|
||||||
availableZone.children.length === 0
|
|
||||||
) {
|
|
||||||
populateControlBarZones(
|
|
||||||
activeZone,
|
|
||||||
availableZone,
|
|
||||||
getControlBarOrder()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cbContainer.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.target.classList.contains("override-popup-controlbar")) {
|
|
||||||
var popupCbContainer = event.target
|
|
||||||
.closest(".site-rule-controlbar")
|
|
||||||
.querySelector(".site-popup-controlbar-container");
|
|
||||||
if (event.target.checked) {
|
|
||||||
popupCbContainer.style.display = "block";
|
|
||||||
var popupActiveZone = popupCbContainer.querySelector(".site-popup-cb-active");
|
|
||||||
var popupAvailableZone = popupCbContainer.querySelector(".site-popup-cb-available");
|
|
||||||
if (
|
|
||||||
popupActiveZone &&
|
|
||||||
popupAvailableZone &&
|
|
||||||
popupActiveZone.children.length === 0 &&
|
|
||||||
popupAvailableZone.children.length === 0
|
|
||||||
) {
|
|
||||||
populateControlBarZones(
|
|
||||||
popupActiveZone,
|
|
||||||
popupAvailableZone,
|
|
||||||
getPopupControlBarOrder()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
popupCbContainer.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -94,10 +94,24 @@ button:focus-visible {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#refresh:hover {
|
#refresh:hover {
|
||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
border-color: #1f2937;
|
border-color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-compact-action {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-compact-action[aria-pressed="true"] {
|
||||||
|
background: #e8f3e5;
|
||||||
|
border-color: #9fbd98;
|
||||||
|
color: #285d21;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-divider {
|
.popup-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -159,6 +173,22 @@ button:focus-visible {
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-control-bar button .vsc-btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-control-bar button .vsc-btn-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-status {
|
.popup-status {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -194,21 +224,63 @@ button:focus-visible {
|
|||||||
|
|
||||||
.donate-split {
|
.donate-split {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-split button {
|
.donate-icon-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:hover {
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-color: #c5ccd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:active {
|
||||||
|
background: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:focus-visible {
|
||||||
|
outline: 2px solid rgba(17, 24, 39, 0.14);
|
||||||
|
outline-offset: 2px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn--kofi img {
|
||||||
|
display: block;
|
||||||
|
height: 22px;
|
||||||
width: auto;
|
width: auto;
|
||||||
border-radius: 0;
|
max-width: 40px;
|
||||||
min-height: 28px;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-split button:first-child {
|
.donate-icon-btn:first-child {
|
||||||
border-radius: 8px 0 0 8px;
|
border-radius: 8px 0 0 8px;
|
||||||
border-right: 0;
|
border-right-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-split button:last-child {
|
.donate-icon-btn:nth-child(2) {
|
||||||
|
border-radius: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:last-child {
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,8 +318,23 @@ button:focus-visible {
|
|||||||
color: #111315;
|
color: #111315;
|
||||||
}
|
}
|
||||||
|
|
||||||
#refresh:hover {
|
#refresh:hover {
|
||||||
background: #dfe3e8;
|
background: #dfe3e8;
|
||||||
border-color: #dfe3e8;
|
border-color: #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-compact-action[aria-pressed="true"] {
|
||||||
|
background: #21351f;
|
||||||
|
border-color: #52724d;
|
||||||
|
color: #b8ddb1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:hover {
|
||||||
|
background: #1f2226;
|
||||||
|
border-color: #4a515a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-icon-btn:active {
|
||||||
|
background: #252a2f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Speeder</title>
|
||||||
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
<script src="../shared/site-rules.js"></script>
|
||||||
|
<script src="../shared/popup-controls.js"></script>
|
||||||
|
<script src="../shared/ui-icons.js"></script>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="popup-shell">
|
||||||
|
<div class="popup-header">
|
||||||
|
<span class="popup-title">Speeder</span>
|
||||||
|
<span class="popup-version">v<span id="app-version"></span></span>
|
||||||
|
</div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="refresh">Rescan page for videos</button>
|
||||||
|
<button
|
||||||
|
id="forceLastSavedSpeed"
|
||||||
|
class="popup-compact-action"
|
||||||
|
type="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
>Force last saved speed</button>
|
||||||
|
<div class="popup-divider"></div>
|
||||||
|
<div id="popupControlBar" class="popup-control-bar">
|
||||||
|
<span id="popupSpeed" class="popup-speed">1.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="popup-divider"></div>
|
||||||
|
<button id="enable" class="hide">Enable</button>
|
||||||
|
<button id="disable">Disable</button>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="popup-status hide"></div>
|
||||||
|
<div class="popup-links">
|
||||||
|
<button id="config">Settings</button>
|
||||||
|
<div class="popup-secondary">
|
||||||
|
<button id="feedback" class="secondary">Feedback</button>
|
||||||
|
<button id="about" class="secondary">About</button>
|
||||||
|
</div>
|
||||||
|
<div id="donateWrap" class="donate-wrap">
|
||||||
|
<button id="donate" class="secondary">Donate</button>
|
||||||
|
<div id="donateOptions" class="donate-split hide">
|
||||||
|
<a
|
||||||
|
id="donateGithub"
|
||||||
|
class="donate-icon-btn donate-icon-btn--github"
|
||||||
|
href="https://github.com/sponsors/SoPat712"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Sponsor on GitHub (opens in new tab)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id="donateKofi"
|
||||||
|
class="donate-icon-btn donate-icon-btn--kofi"
|
||||||
|
href="https://ko-fi.com/joshpatra"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Support on Ko-fi (opens in new tab)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="../assets/images/kofi_symbol.svg"
|
||||||
|
width="28"
|
||||||
|
height="22"
|
||||||
|
alt=""
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id="donateBmc"
|
||||||
|
class="donate-icon-btn donate-icon-btn--bmc"
|
||||||
|
href="https://buymeacoffee.com/treeman183"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Support on Buy Me a Coffee (opens in new tab)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,115 +1,65 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
var speederShared =
|
||||||
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
|
var siteRuleUtils = speederShared.siteRules || {};
|
||||||
|
var popupControlUtils = speederShared.popupControls || {};
|
||||||
|
|
||||||
|
/* `label` is only used if shared/ui-icons.js has no path for this action (fallback). */
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { label: "\u00AB", className: "rw" },
|
rewind: { label: "", className: "rw" },
|
||||||
slower: { label: "\u2212", className: "" },
|
slower: { label: "", className: "" },
|
||||||
faster: { label: "+", className: "" },
|
faster: { label: "", className: "" },
|
||||||
advance: { label: "\u00BB", className: "rw" },
|
advance: { label: "", className: "rw" },
|
||||||
display: { label: "\u00D7", className: "hideButton" },
|
display: { label: "", className: "hideButton" },
|
||||||
reset: { label: "\u21BA", className: "" },
|
reset: { label: "\u21BB", className: "" },
|
||||||
fast: { label: "\u2605", className: "" },
|
fast: { label: "", className: "" },
|
||||||
settings: { label: "\u2699", className: "" },
|
nudge: { label: "", className: "" },
|
||||||
pause: { label: "\u23EF", className: "" },
|
pause: { label: "", className: "" },
|
||||||
muted: { label: "M", className: "" },
|
muted: { label: "", className: "" },
|
||||||
mark: { label: "\u2691", className: "" },
|
louder: { label: "", className: "" },
|
||||||
jump: { label: "\u21E5", className: "" }
|
softer: { label: "", className: "" },
|
||||||
|
mark: { label: "", className: "" },
|
||||||
|
jump: { label: "", className: "" },
|
||||||
|
settings: { label: "", className: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
||||||
|
var popupExcludedButtonIds = new Set(["settings"]);
|
||||||
var storageDefaults = {
|
var storageDefaults = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
lastSpeed: 1.0,
|
||||||
|
forceLastSavedSpeed: false,
|
||||||
showPopupControlBar: true,
|
showPopupControlBar: true,
|
||||||
controllerButtons: defaultButtons,
|
controllerButtons: defaultButtons,
|
||||||
popupMatchHoverControls: true,
|
popupMatchHoverControls: true,
|
||||||
popupControllerButtons: defaultButtons,
|
popupControllerButtons: defaultButtons,
|
||||||
siteRules: [],
|
siteRules: []
|
||||||
blacklist: `\
|
|
||||||
www.instagram.com
|
|
||||||
twitter.com
|
|
||||||
vine.co
|
|
||||||
imgur.com
|
|
||||||
teams.microsoft.com
|
|
||||||
`.replace(/^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm, "")
|
|
||||||
};
|
};
|
||||||
var renderToken = 0;
|
var renderToken = 0;
|
||||||
|
|
||||||
function escapeStringRegExp(str) {
|
function updateForceButton(enabled) {
|
||||||
const m = /[|\\{}()[\]^$+*?.]/g;
|
var button = document.getElementById("forceLastSavedSpeed");
|
||||||
return str.replace(m, "\\$&");
|
if (!button) return;
|
||||||
}
|
button.setAttribute("aria-pressed", enabled ? "true" : "false");
|
||||||
|
button.title = enabled
|
||||||
function isBlacklisted(url, blacklist) {
|
? "Stop forcing the saved speed"
|
||||||
let b = false;
|
: "Keep this page at the last speed saved by Speeder";
|
||||||
const l = blacklist ? blacklist.split("\n") : [];
|
|
||||||
l.forEach((m) => {
|
|
||||||
if (b) return;
|
|
||||||
m = m.replace(regStrip, "");
|
|
||||||
if (m.length == 0) return;
|
|
||||||
let r;
|
|
||||||
if (m.startsWith("/") && m.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
const ls = m.lastIndexOf("/");
|
|
||||||
r = new RegExp(m.substring(1, ls), m.substring(ls + 1));
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else r = new RegExp(escapeStringRegExp(m));
|
|
||||||
if (r && r.test(url)) b = true;
|
|
||||||
});
|
|
||||||
return b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchSiteRule(url, siteRules) {
|
function matchSiteRule(url, siteRules) {
|
||||||
if (!url || !Array.isArray(siteRules)) return null;
|
return siteRuleUtils.matchSiteRule(url, siteRules);
|
||||||
for (var i = 0; i < siteRules.length; i++) {
|
|
||||||
var rule = siteRules[i];
|
|
||||||
if (!rule || !rule.pattern) continue;
|
|
||||||
var pattern = rule.pattern.replace(regStrip, "");
|
|
||||||
if (pattern.length === 0) continue;
|
|
||||||
var re;
|
|
||||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
var ls = pattern.lastIndexOf("/");
|
|
||||||
re = new RegExp(pattern.substring(1, ls), pattern.substring(ls + 1));
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
re = new RegExp(escapeStringRegExp(pattern));
|
|
||||||
}
|
|
||||||
if (re && re.test(url)) return rule;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSiteRuleDisabled(rule) {
|
function isSiteRuleDisabled(rule) {
|
||||||
return Boolean(
|
return siteRuleUtils.isSiteRuleDisabled(rule);
|
||||||
rule &&
|
|
||||||
(rule.enabled === false || rule.disableExtension === true)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePopupButtons(storage, siteRule) {
|
function resolvePopupButtons(storage, siteRule) {
|
||||||
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
return popupControlUtils.resolvePopupButtons(storage, siteRule, {
|
||||||
return siteRule.popupControllerButtons;
|
controllerButtonDefs: controllerButtonDefs,
|
||||||
}
|
defaultButtons: defaultButtons,
|
||||||
|
excludedIds: popupExcludedButtonIds
|
||||||
if (storage.popupMatchHoverControls) {
|
});
|
||||||
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
|
||||||
return siteRule.controllerButtons;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storage.controllerButtons)) {
|
|
||||||
return storage.controllerButtons;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storage.popupControllerButtons)) {
|
|
||||||
return storage.popupControllerButtons;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultButtons;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setControlBarVisible(visible) {
|
function setControlBarVisible(visible) {
|
||||||
@@ -171,43 +121,95 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySpeedAndResetFromResponse(response) {
|
||||||
|
if (response && response.speed != null) {
|
||||||
|
updateSpeedDisplay(response.speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestFrameSpeedResult(results) {
|
||||||
|
return popupControlUtils.pickBestFrameSpeedResult(results);
|
||||||
|
}
|
||||||
|
|
||||||
function querySpeed() {
|
function querySpeed() {
|
||||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||||
if (response && response.speed != null) {
|
if (!tabs[0] || tabs[0].id == null) {
|
||||||
updateSpeedDisplay(response.speed);
|
return;
|
||||||
}
|
}
|
||||||
|
var tabId = tabs[0].id;
|
||||||
|
chrome.tabs.executeScript(
|
||||||
|
tabId,
|
||||||
|
{ allFrames: true, file: "content/frame-speed-snapshot.js" },
|
||||||
|
function (results) {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||||
|
applySpeedAndResetFromResponse(response || { speed: 1 });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var best = pickBestFrameSpeedResult(results);
|
||||||
|
if (best) {
|
||||||
|
applySpeedAndResetFromResponse(best);
|
||||||
|
} else {
|
||||||
|
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||||
|
applySpeedAndResetFromResponse(response || { speed: 1 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildControlBar(buttons) {
|
function buildControlBar(buttons, customIconsMap) {
|
||||||
var bar = document.getElementById("popupControlBar");
|
var bar = document.getElementById("popupControlBar");
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
|
|
||||||
var existing = bar.querySelectorAll("button");
|
var existing = bar.querySelectorAll("button");
|
||||||
existing.forEach(function (btn) { btn.remove(); });
|
existing.forEach(function (btn) { btn.remove(); });
|
||||||
|
|
||||||
|
var customMap = customIconsMap || {};
|
||||||
|
|
||||||
buttons.forEach(function (btnId) {
|
buttons.forEach(function (btnId) {
|
||||||
if (btnId === "nudge") return;
|
|
||||||
var def = controllerButtonDefs[btnId];
|
var def = controllerButtonDefs[btnId];
|
||||||
if (!def) return;
|
if (!def) return;
|
||||||
|
|
||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.dataset.action = btnId;
|
btn.dataset.action = btnId;
|
||||||
btn.textContent = def.label;
|
var customEntry = customMap[btnId];
|
||||||
|
if (customEntry && customEntry.svg) {
|
||||||
|
var customSpan = vscCreateSvgWrap(document, customEntry.svg, "vsc-btn-icon");
|
||||||
|
if (customSpan) {
|
||||||
|
btn.appendChild(customSpan);
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconSvgString === "function") {
|
||||||
|
var svgStr = vscIconSvgString(btnId, 16);
|
||||||
|
if (svgStr) {
|
||||||
|
var iconSpan = vscCreateSvgWrap(document, svgStr, "vsc-btn-icon");
|
||||||
|
if (iconSpan) {
|
||||||
|
btn.appendChild(iconSpan);
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
if (def.className) btn.className = def.className;
|
if (def.className) btn.className = def.className;
|
||||||
btn.title = btnId.charAt(0).toUpperCase() + btnId.slice(1);
|
btn.title = btnId.charAt(0).toUpperCase() + btnId.slice(1);
|
||||||
|
|
||||||
btn.addEventListener("click", function () {
|
btn.addEventListener("click", function () {
|
||||||
if (btnId === "settings") {
|
if (btnId === "settings") {
|
||||||
window.open(chrome.runtime.getURL("options.html"));
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendToActiveTab(
|
sendToActiveTab(
|
||||||
{ action: "run_action", actionName: btnId },
|
{ action: "run_action", actionName: btnId },
|
||||||
function (response) {
|
function () {
|
||||||
if (response && response.speed != null) {
|
querySpeed();
|
||||||
updateSpeedDisplay(response.speed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -219,11 +221,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
var manifest = chrome.runtime.getManifest();
|
var manifest = chrome.runtime.getManifest();
|
||||||
var versionElement = document.querySelector("#app-version");
|
var versionElement = document.querySelector("#app-version");
|
||||||
if (versionElement) {
|
if (versionElement) {
|
||||||
versionElement.innerText = manifest.version;
|
versionElement.textContent = manifest.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector("#config").addEventListener("click", function () {
|
document.querySelector("#config").addEventListener("click", function () {
|
||||||
window.open(chrome.runtime.getURL("options.html"));
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#about").addEventListener("click", function () {
|
document.querySelector("#about").addEventListener("click", function () {
|
||||||
@@ -239,14 +241,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
document.querySelector("#donateOptions").classList.remove("hide");
|
document.querySelector("#donateOptions").classList.remove("hide");
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#donateKofi").addEventListener("click", function () {
|
|
||||||
window.open("https://ko-fi.com/joshpatra");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#donateGithub").addEventListener("click", function () {
|
|
||||||
window.open("https://github.com/sponsors/SoPat712");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#enable").addEventListener("click", function () {
|
document.querySelector("#enable").addEventListener("click", function () {
|
||||||
toggleEnabled(true, settingsSavedReloadMessage);
|
toggleEnabled(true, settingsSavedReloadMessage);
|
||||||
});
|
});
|
||||||
@@ -269,21 +263,66 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var forceLastSavedSpeedButton = document.querySelector(
|
||||||
|
"#forceLastSavedSpeed"
|
||||||
|
);
|
||||||
|
if (!forceLastSavedSpeedButton.dataset.listenerAttached) {
|
||||||
|
forceLastSavedSpeedButton.dataset.listenerAttached = "true";
|
||||||
|
forceLastSavedSpeedButton.addEventListener("click", function () {
|
||||||
|
var button = this;
|
||||||
|
var enabled = button.getAttribute("aria-pressed") !== "true";
|
||||||
|
chrome.storage.sync.get({ lastSpeed: 1.0 }, function (storage) {
|
||||||
|
chrome.storage.sync.set({ forceLastSavedSpeed: enabled }, function () {
|
||||||
|
updateForceButton(enabled);
|
||||||
|
sendToActiveTab(
|
||||||
|
{
|
||||||
|
action: "set_force_last_saved_speed",
|
||||||
|
enabled: enabled,
|
||||||
|
speed: Number(storage.lastSpeed) || 1.0
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
if (response && response.speed != null) {
|
||||||
|
updateSpeedDisplay(response.speed);
|
||||||
|
setStatusMessage(
|
||||||
|
enabled ? "Saved speed is now forced." : "Speed forcing is off."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setStatusMessage(
|
||||||
|
enabled
|
||||||
|
? "Force enabled. No video found on this page."
|
||||||
|
: "Speed forcing is off."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderForActiveTab() {
|
function renderForActiveTab() {
|
||||||
var currentRenderToken = ++renderToken;
|
var currentRenderToken = ++renderToken;
|
||||||
|
|
||||||
chrome.storage.sync.get(storageDefaults, function (storage) {
|
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
var customIconsMap =
|
||||||
|
loc && loc.customButtonIcons && typeof loc.customButtonIcons === "object"
|
||||||
|
? loc.customButtonIcons
|
||||||
|
: {};
|
||||||
|
|
||||||
|
chrome.storage.sync.get(storageDefaults, function (storage) {
|
||||||
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
getActiveTabContext(function (context) {
|
getActiveTabContext(function (context) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
var url = context && context.url ? context.url : "";
|
var url = context && context.url ? context.url : "";
|
||||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||||
var blacklisted = isBlacklisted(url, storage.blacklist);
|
|
||||||
var siteDisabled = isSiteRuleDisabled(siteRule);
|
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||||
var siteAvailable =
|
var siteAvailable = siteRuleUtils.isSpeederActiveForSite(
|
||||||
storage.enabled !== false && !blacklisted && !siteDisabled;
|
storage.enabled,
|
||||||
|
siteRule
|
||||||
|
);
|
||||||
var showBar = storage.showPopupControlBar !== false;
|
var showBar = storage.showPopupControlBar !== false;
|
||||||
|
|
||||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||||
@@ -291,15 +330,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleEnabledUI(storage.enabled !== false);
|
toggleEnabledUI(storage.enabled !== false);
|
||||||
buildControlBar(resolvePopupButtons(storage, siteRule));
|
updateForceButton(storage.forceLastSavedSpeed === true);
|
||||||
|
buildControlBar(
|
||||||
|
resolvePopupButtons(storage, siteRule),
|
||||||
|
customIconsMap
|
||||||
|
);
|
||||||
setControlBarVisible(siteAvailable && showBar);
|
setControlBarVisible(siteAvailable && showBar);
|
||||||
|
|
||||||
if (blacklisted) {
|
|
||||||
setStatusMessage("Site is blacklisted.");
|
|
||||||
updateSpeedDisplay(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siteDisabled) {
|
if (siteDisabled) {
|
||||||
setStatusMessage("Speeder is disabled for this site.");
|
setStatusMessage("Speeder is disabled for this site.");
|
||||||
updateSpeedDisplay(1);
|
updateSpeedDisplay(1);
|
||||||
@@ -313,6 +350,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
updateSpeedDisplay(1);
|
updateSpeedDisplay(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,15 +366,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener(function (changes, areaName) {
|
chrome.storage.onChanged.addListener(function (changes, areaName) {
|
||||||
|
if (areaName === "local" && changes.customButtonIcons) {
|
||||||
|
renderForActiveTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (areaName !== "sync") return;
|
if (areaName !== "sync") return;
|
||||||
if (
|
if (
|
||||||
changes.enabled ||
|
changes.enabled ||
|
||||||
|
changes.forceLastSavedSpeed ||
|
||||||
changes.showPopupControlBar ||
|
changes.showPopupControlBar ||
|
||||||
changes.controllerButtons ||
|
changes.controllerButtons ||
|
||||||
changes.popupMatchHoverControls ||
|
changes.popupMatchHoverControls ||
|
||||||
changes.popupControllerButtons ||
|
changes.popupControllerButtons ||
|
||||||
changes.siteRules ||
|
changes.siteRules
|
||||||
changes.blacklist
|
|
||||||
) {
|
) {
|
||||||
renderForActiveTab();
|
renderForActiveTab();
|
||||||
}
|
}
|
||||||
@@ -358,9 +400,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
const suffix = `${enabled ? "" : "_disabled"}.png`;
|
const suffix = `${enabled ? "" : "_disabled"}.png`;
|
||||||
chrome.browserAction.setIcon({
|
chrome.browserAction.setIcon({
|
||||||
path: {
|
path: {
|
||||||
19: "icons/icon19" + suffix,
|
19: "assets/icons/icon19" + suffix,
|
||||||
38: "icons/icon38" + suffix,
|
38: "assets/icons/icon38" + suffix,
|
||||||
48: "icons/icon48" + suffix
|
48: "assets/icons/icon48" + suffix
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -374,12 +416,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
function setStatusMessage(str) {
|
function setStatusMessage(str) {
|
||||||
const status_element = document.querySelector("#status");
|
const status_element = document.querySelector("#status");
|
||||||
status_element.classList.toggle("hide", false);
|
status_element.classList.toggle("hide", false);
|
||||||
status_element.innerText = str;
|
status_element.textContent = str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearStatusMessage() {
|
function clearStatusMessage() {
|
||||||
const status_element = document.querySelector("#status");
|
const status_element = document.querySelector("#status");
|
||||||
status_element.classList.toggle("hide", true);
|
status_element.classList.toggle("hide", true);
|
||||||
status_element.innerText = "";
|
status_element.textContent = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.controllerUtils = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||||
|
var controllerLocations = [
|
||||||
|
"top-left",
|
||||||
|
"top-center",
|
||||||
|
"top-right",
|
||||||
|
"middle-right",
|
||||||
|
"bottom-right",
|
||||||
|
"bottom-center",
|
||||||
|
"bottom-left",
|
||||||
|
"middle-left"
|
||||||
|
];
|
||||||
|
var defaultControllerLocation = controllerLocations[0];
|
||||||
|
|
||||||
|
function normalizeControllerLocation(location, fallback) {
|
||||||
|
if (controllerLocations.includes(location)) return location;
|
||||||
|
return typeof fallback === "string"
|
||||||
|
? fallback
|
||||||
|
: defaultControllerLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampControllerMarginPx(value, fallback) {
|
||||||
|
var numericValue = Number(value);
|
||||||
|
if (!Number.isFinite(numericValue)) return fallback;
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
CONTROLLER_MARGIN_MAX_PX,
|
||||||
|
Math.max(0, Math.round(numericValue))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextControllerLocation(location) {
|
||||||
|
var normalizedLocation = normalizeControllerLocation(location);
|
||||||
|
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
||||||
|
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
CONTROLLER_MARGIN_MAX_PX: CONTROLLER_MARGIN_MAX_PX,
|
||||||
|
clampControllerMarginPx: clampControllerMarginPx,
|
||||||
|
controllerLocations: controllerLocations.slice(),
|
||||||
|
defaultControllerLocation: defaultControllerLocation,
|
||||||
|
getNextControllerLocation: getNextControllerLocation,
|
||||||
|
normalizeControllerLocation: normalizeControllerLocation
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.importExport = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var rawSettingsKeys = new Set([
|
||||||
|
"audioBoolean",
|
||||||
|
"controllerButtons",
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"controllerMarginLeft",
|
||||||
|
"controllerMarginRight",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerOpacity",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"enabled",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"hideWithYouTubeControls",
|
||||||
|
"keyBindings",
|
||||||
|
"lastSpeed",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"popupMatchHoverControls",
|
||||||
|
"rememberSpeed",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"siteRules",
|
||||||
|
"speed",
|
||||||
|
"startHidden",
|
||||||
|
"subtitleNudgeAmount",
|
||||||
|
"subtitleNudgeEnabledByDefault",
|
||||||
|
"subtitleNudgeInterval"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isRecognizedRawSettingsObject(backup) {
|
||||||
|
if (!backup || typeof backup !== "object" || Array.isArray(backup)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(backup).some(function(key) {
|
||||||
|
return rawSettingsKeys.has(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local-only keys excluded from backup JSON. These are disposable caches
|
||||||
|
* (e.g. Lucide tags.json) that bloat exports and are refetched when needed.
|
||||||
|
* Keep in sync with options/lucide-client.js (LUCIDE_TAGS_CACHE_KEY + "At").
|
||||||
|
*/
|
||||||
|
var localSettingsKeysOmittedFromExport = [
|
||||||
|
"lucideTagsCacheV1",
|
||||||
|
"lucideTagsCacheV1At"
|
||||||
|
];
|
||||||
|
|
||||||
|
function filterLocalSettingsForExport(local) {
|
||||||
|
if (!local || typeof local !== "object" || Array.isArray(local)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
var out = {};
|
||||||
|
for (var key in local) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(local, key)) continue;
|
||||||
|
if (localSettingsKeysOmittedFromExport.indexOf(key) !== -1) continue;
|
||||||
|
out[key] = local[key];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBackupFilename(now) {
|
||||||
|
var date = now instanceof Date ? now : new Date(now || Date.now());
|
||||||
|
var year = date.getFullYear();
|
||||||
|
var month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
var day = String(date.getDate()).padStart(2, "0");
|
||||||
|
var hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
var minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
var seconds = String(date.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
"speeder-backup_" +
|
||||||
|
year +
|
||||||
|
"-" +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
day +
|
||||||
|
"_" +
|
||||||
|
hours +
|
||||||
|
"." +
|
||||||
|
minutes +
|
||||||
|
"." +
|
||||||
|
seconds +
|
||||||
|
".json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBackupPayload(settings, localSettings, now) {
|
||||||
|
return {
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: new Date(now || Date.now()).toISOString(),
|
||||||
|
settings: settings,
|
||||||
|
localSettings: localSettings || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImportSettings(backup) {
|
||||||
|
var settingsToImport = null;
|
||||||
|
var isWrappedBackup = false;
|
||||||
|
|
||||||
|
if (backup && backup.settings && typeof backup.settings === "object") {
|
||||||
|
settingsToImport = backup.settings;
|
||||||
|
isWrappedBackup = true;
|
||||||
|
} else if (
|
||||||
|
backup &&
|
||||||
|
typeof backup === "object" &&
|
||||||
|
isRecognizedRawSettingsObject(backup)
|
||||||
|
) {
|
||||||
|
settingsToImport = backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsToImport) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isWrappedBackup: isWrappedBackup,
|
||||||
|
settings: settingsToImport,
|
||||||
|
localSettings:
|
||||||
|
backup &&
|
||||||
|
backup.localSettings &&
|
||||||
|
typeof backup.localSettings === "object"
|
||||||
|
? backup.localSettings
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportText(text) {
|
||||||
|
return extractImportSettings(JSON.parse(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildBackupPayload: buildBackupPayload,
|
||||||
|
extractImportSettings: extractImportSettings,
|
||||||
|
filterLocalSettingsForExport: filterLocalSettingsForExport,
|
||||||
|
generateBackupFilename: generateBackupFilename,
|
||||||
|
isRecognizedRawSettingsObject: isRecognizedRawSettingsObject,
|
||||||
|
parseImportText: parseImportText
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.keyBindings = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
function normalizeBindingKey(key) {
|
||||||
|
if (typeof key !== "string" || key.length === 0) return null;
|
||||||
|
if (key === "Spacebar") return " ";
|
||||||
|
if (key === "Esc") return "Escape";
|
||||||
|
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLegacyKeyCode(binding) {
|
||||||
|
if (!binding) return null;
|
||||||
|
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
||||||
|
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
||||||
|
return binding.key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyBindingKeyToCode(key) {
|
||||||
|
var normalizedKey = normalizeBindingKey(key);
|
||||||
|
if (!normalizedKey) return null;
|
||||||
|
if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
|
||||||
|
if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
|
||||||
|
if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
|
||||||
|
|
||||||
|
var keyMap = {
|
||||||
|
" ": "Space",
|
||||||
|
ArrowLeft: "ArrowLeft",
|
||||||
|
ArrowUp: "ArrowUp",
|
||||||
|
ArrowRight: "ArrowRight",
|
||||||
|
ArrowDown: "ArrowDown",
|
||||||
|
";": "Semicolon",
|
||||||
|
"<": "Comma",
|
||||||
|
"-": "Minus",
|
||||||
|
"+": "Equal",
|
||||||
|
">": "Period",
|
||||||
|
"/": "Slash",
|
||||||
|
"~": "Backquote",
|
||||||
|
"[": "BracketLeft",
|
||||||
|
"\\": "Backslash",
|
||||||
|
"]": "BracketRight",
|
||||||
|
"'": "Quote"
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyMap[normalizedKey] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
|
if (!Number.isInteger(keyCode)) return null;
|
||||||
|
if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
|
||||||
|
if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
|
||||||
|
if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
|
||||||
|
if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
|
||||||
|
|
||||||
|
var keyCodeMap = {
|
||||||
|
32: "Space",
|
||||||
|
37: "ArrowLeft",
|
||||||
|
38: "ArrowUp",
|
||||||
|
39: "ArrowRight",
|
||||||
|
40: "ArrowDown",
|
||||||
|
106: "NumpadMultiply",
|
||||||
|
107: "NumpadAdd",
|
||||||
|
109: "NumpadSubtract",
|
||||||
|
110: "NumpadDecimal",
|
||||||
|
111: "NumpadDivide",
|
||||||
|
186: "Semicolon",
|
||||||
|
188: "Comma",
|
||||||
|
189: "Minus",
|
||||||
|
187: "Equal",
|
||||||
|
190: "Period",
|
||||||
|
191: "Slash",
|
||||||
|
192: "Backquote",
|
||||||
|
219: "BracketLeft",
|
||||||
|
220: "Backslash",
|
||||||
|
221: "BracketRight",
|
||||||
|
222: "Quote",
|
||||||
|
59: "Semicolon",
|
||||||
|
61: "Equal",
|
||||||
|
173: "Minus"
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyCodeMap[keyCode] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferBindingCode(binding, fallbackCode) {
|
||||||
|
if (binding && typeof binding.code === "string" && binding.code.length > 0) {
|
||||||
|
return binding.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding && typeof binding.key === "string") {
|
||||||
|
var codeFromKey = legacyBindingKeyToCode(binding.key);
|
||||||
|
if (codeFromKey) return codeFromKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||||
|
if (Number.isInteger(legacyKeyCode)) {
|
||||||
|
var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
|
||||||
|
if (codeFromKeyCode) return codeFromKeyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof fallbackCode === "string" && fallbackCode.length > 0
|
||||||
|
? fallbackCode
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLegacyKeyCode: getLegacyKeyCode,
|
||||||
|
inferBindingCode: inferBindingCode,
|
||||||
|
legacyBindingKeyToCode: legacyBindingKeyToCode,
|
||||||
|
legacyKeyCodeToCode: legacyKeyCodeToCode,
|
||||||
|
normalizeBindingKey: normalizeBindingKey
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.popupControls = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
function normalizeExcludedIds(excludedIds) {
|
||||||
|
if (excludedIds instanceof Set) return excludedIds;
|
||||||
|
return new Set(Array.isArray(excludedIds) ? excludedIds : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds) {
|
||||||
|
if (!Array.isArray(buttonIds)) return [];
|
||||||
|
|
||||||
|
var seen = new Set();
|
||||||
|
var excluded = normalizeExcludedIds(excludedIds);
|
||||||
|
|
||||||
|
return buttonIds.filter(function(id) {
|
||||||
|
if (!controllerButtonDefs[id] || excluded.has(id) || seen.has(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePopupButtons(storage, siteRule, options) {
|
||||||
|
var settings = storage || {};
|
||||||
|
var config = options || {};
|
||||||
|
var controllerButtonDefs = config.controllerButtonDefs || {};
|
||||||
|
var defaultButtons = Array.isArray(config.defaultButtons)
|
||||||
|
? config.defaultButtons
|
||||||
|
: [];
|
||||||
|
var excludedIds = config.excludedIds;
|
||||||
|
|
||||||
|
function sanitize(buttonIds) {
|
||||||
|
return sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
||||||
|
return sanitize(siteRule.popupControllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.popupMatchHoverControls) {
|
||||||
|
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
||||||
|
return sanitize(siteRule.controllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(settings.controllerButtons)) {
|
||||||
|
return sanitize(settings.controllerButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(settings.popupControllerButtons)) {
|
||||||
|
return sanitize(settings.popupControllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitize(defaultButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestFrameSpeedResult(results) {
|
||||||
|
if (!results || !results.length) return null;
|
||||||
|
|
||||||
|
var fallback = null;
|
||||||
|
for (var i = 0; i < results.length; i++) {
|
||||||
|
var result = results[i];
|
||||||
|
if (!result || typeof result.speed !== "number") continue;
|
||||||
|
if (result.preferred) return { speed: result.speed };
|
||||||
|
if (!fallback) fallback = { speed: result.speed };
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||||
|
resolvePopupButtons: resolvePopupButtons,
|
||||||
|
sanitizeButtonOrder: sanitizeButtonOrder
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
|
||||||
|
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
|
||||||
|
var SITE_RULE_OVERRIDE_KEYS = [
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"startHidden",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"rememberSpeed",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"audioBoolean",
|
||||||
|
"controllerOpacity",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"subtitleNudgeEnabledByDefault",
|
||||||
|
"subtitleNudgeInterval",
|
||||||
|
"controllerButtons",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"shortcuts",
|
||||||
|
"preferredSpeed"
|
||||||
|
];
|
||||||
|
var DIFFABLE_OPTION_KEYS = [
|
||||||
|
"rememberSpeed",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"audioBoolean",
|
||||||
|
"enabled",
|
||||||
|
"startHidden",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerOpacity",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"keyBindings",
|
||||||
|
"siteRules",
|
||||||
|
"siteRulesMeta",
|
||||||
|
"siteRulesFormat",
|
||||||
|
"controllerButtons",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"popupMatchHoverControls",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"subtitleNudgeEnabledByDefault",
|
||||||
|
"subtitleNudgeInterval",
|
||||||
|
"subtitleNudgeAmount"
|
||||||
|
];
|
||||||
|
var MANAGED_SYNC_KEYS = DIFFABLE_OPTION_KEYS.concat([
|
||||||
|
"hideWithYouTubeControls"
|
||||||
|
]);
|
||||||
|
|
||||||
|
var DEFAULT_SETTINGS = {
|
||||||
|
speed: 1.0,
|
||||||
|
lastSpeed: 1.0,
|
||||||
|
displayKeyCode: 86,
|
||||||
|
rememberSpeed: false,
|
||||||
|
audioBoolean: false,
|
||||||
|
startHidden: false,
|
||||||
|
hideWithYouTubeControls: false,
|
||||||
|
hideWithControls: false,
|
||||||
|
hideWithControlsTimer: 2.0,
|
||||||
|
controllerLocation: "top-left",
|
||||||
|
forceLastSavedSpeed: false,
|
||||||
|
enabled: true,
|
||||||
|
controllerOpacity: 0.3,
|
||||||
|
controllerMarginTop: 0,
|
||||||
|
controllerMarginRight: 0,
|
||||||
|
controllerMarginBottom: 65,
|
||||||
|
controllerMarginLeft: 0,
|
||||||
|
keyBindings: [
|
||||||
|
{
|
||||||
|
action: "display",
|
||||||
|
key: "V",
|
||||||
|
keyCode: 86,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "move",
|
||||||
|
key: "P",
|
||||||
|
keyCode: 80,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "slower",
|
||||||
|
key: "S",
|
||||||
|
keyCode: 83,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0.1,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "faster",
|
||||||
|
key: "D",
|
||||||
|
keyCode: 68,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0.1,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "rewind",
|
||||||
|
key: "Z",
|
||||||
|
keyCode: 90,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 10,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "advance",
|
||||||
|
key: "X",
|
||||||
|
keyCode: 88,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 10,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "reset",
|
||||||
|
key: "R",
|
||||||
|
keyCode: 82,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "fast",
|
||||||
|
key: "G",
|
||||||
|
keyCode: 71,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 1.8,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "toggleSubtitleNudge",
|
||||||
|
key: "N",
|
||||||
|
keyCode: 78,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
|
||||||
|
enabled: true,
|
||||||
|
enableSubtitleNudge: true,
|
||||||
|
subtitleNudgeInterval: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
||||||
|
enabled: true,
|
||||||
|
rememberSpeed: true,
|
||||||
|
controllerMarginTop: 60,
|
||||||
|
controllerMarginBottom: 85
|
||||||
|
}
|
||||||
|
],
|
||||||
|
controllerButtons: DEFAULT_BUTTONS.slice(),
|
||||||
|
showPopupControlBar: true,
|
||||||
|
popupMatchHoverControls: true,
|
||||||
|
popupControllerButtons: DEFAULT_BUTTONS.slice(),
|
||||||
|
enableSubtitleNudge: false,
|
||||||
|
subtitleNudgeEnabledByDefault: true,
|
||||||
|
subtitleNudgeInterval: 50,
|
||||||
|
subtitleNudgeAmount: 0.001
|
||||||
|
};
|
||||||
|
|
||||||
|
function clonePlainData(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOwn(obj, key) {
|
||||||
|
return Boolean(obj) && Object.prototype.hasOwnProperty.call(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortComparableValue(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(sortComparableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
var sorted = {};
|
||||||
|
Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.forEach(function (key) {
|
||||||
|
if (value[key] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sorted[key] = sortComparableValue(value[key]);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areComparableValuesEqual(a, b) {
|
||||||
|
return (
|
||||||
|
JSON.stringify(sortComparableValue(a)) ===
|
||||||
|
JSON.stringify(sortComparableValue(b))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMergeDefaults(defaults, overrides) {
|
||||||
|
if (Array.isArray(defaults)) {
|
||||||
|
return Array.isArray(overrides)
|
||||||
|
? clonePlainData(overrides)
|
||||||
|
: clonePlainData(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(defaults)) {
|
||||||
|
var result = clonePlainData(defaults) || {};
|
||||||
|
if (!isPlainObject(overrides)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(overrides).forEach(function (key) {
|
||||||
|
if (overrides[key] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOwn(defaults, key)) {
|
||||||
|
result[key] = deepMergeDefaults(defaults[key], overrides[key]);
|
||||||
|
} else {
|
||||||
|
result[key] = clonePlainData(overrides[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides === undefined
|
||||||
|
? clonePlainData(defaults)
|
||||||
|
: clonePlainData(overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepDiff(current, defaults) {
|
||||||
|
if (current === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(current)) {
|
||||||
|
return areComparableValuesEqual(current, defaults)
|
||||||
|
? undefined
|
||||||
|
: clonePlainData(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(current)) {
|
||||||
|
var result = {};
|
||||||
|
Object.keys(current).forEach(function (key) {
|
||||||
|
var diff = deepDiff(current[key], defaults && defaults[key]);
|
||||||
|
if (diff !== undefined) {
|
||||||
|
result[key] = diff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areComparableValuesEqual(current, defaults)
|
||||||
|
? undefined
|
||||||
|
: clonePlainData(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSiteRules() {
|
||||||
|
return clonePlainData(DEFAULT_SETTINGS.siteRules) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSiteRulesByPattern() {
|
||||||
|
var map = Object.create(null);
|
||||||
|
getDefaultSiteRules().forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule.pattern !== "string" || !rule.pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map[rule.pattern] = rule;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSiteRuleForDiff(rule, baseSettings) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (!pattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = { pattern: pattern };
|
||||||
|
var baseEnabled = hasOwn(baseSettings, "enabled")
|
||||||
|
? Boolean(baseSettings.enabled)
|
||||||
|
: true;
|
||||||
|
var ruleEnabled = hasOwn(rule, "enabled")
|
||||||
|
? Boolean(rule.enabled)
|
||||||
|
: hasOwn(rule, "disableExtension")
|
||||||
|
? !Boolean(rule.disableExtension)
|
||||||
|
: baseEnabled;
|
||||||
|
|
||||||
|
if (!areComparableValuesEqual(ruleEnabled, baseEnabled)) {
|
||||||
|
normalized.enabled = ruleEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
SITE_RULE_OVERRIDE_KEYS.forEach(function (key) {
|
||||||
|
var baseValue = clonePlainData(baseSettings[key]);
|
||||||
|
var effectiveValue = hasOwn(rule, key)
|
||||||
|
? clonePlainData(rule[key])
|
||||||
|
: baseValue;
|
||||||
|
|
||||||
|
if (!areComparableValuesEqual(effectiveValue, baseValue)) {
|
||||||
|
normalized[key] = effectiveValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(rule).forEach(function (key) {
|
||||||
|
if (
|
||||||
|
key === "pattern" ||
|
||||||
|
key === "enabled" ||
|
||||||
|
key === "disableExtension" ||
|
||||||
|
SITE_RULE_OVERRIDE_KEYS.indexOf(key) !== -1 ||
|
||||||
|
rule[key] === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized[key] = clonePlainData(rule[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressSiteRules(siteRules, baseSettings) {
|
||||||
|
if (!Array.isArray(siteRules)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRules = getDefaultSiteRules();
|
||||||
|
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
|
||||||
|
var currentPatterns = new Set();
|
||||||
|
var exportRules = [];
|
||||||
|
|
||||||
|
siteRules.forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (pattern) {
|
||||||
|
currentPatterns.add(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedRule = normalizeSiteRuleForDiff(rule, baseSettings);
|
||||||
|
if (!normalizedRule || Object.keys(normalizedRule).length === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRule = pattern ? defaultRulesByPattern[pattern] : null;
|
||||||
|
var normalizedDefaultRule = defaultRule
|
||||||
|
? normalizeSiteRuleForDiff(defaultRule, baseSettings)
|
||||||
|
: null;
|
||||||
|
if (normalizedDefaultRule) {
|
||||||
|
if (areComparableValuesEqual(normalizedRule, normalizedDefaultRule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRuleDiff = deepDiff(normalizedRule, normalizedDefaultRule);
|
||||||
|
if (defaultRuleDiff && Object.keys(defaultRuleDiff).length > 0) {
|
||||||
|
defaultRuleDiff.pattern = pattern;
|
||||||
|
exportRules.push(defaultRuleDiff);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportRules.push(normalizedRule);
|
||||||
|
});
|
||||||
|
|
||||||
|
var removedDefaultPatterns = defaultRules
|
||||||
|
.map(function (rule) {
|
||||||
|
return rule && typeof rule.pattern === "string" ? rule.pattern : "";
|
||||||
|
})
|
||||||
|
.filter(function (pattern) {
|
||||||
|
return pattern && !currentPatterns.has(pattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
if (exportRules.length > 0) {
|
||||||
|
result.siteRules = exportRules;
|
||||||
|
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
|
||||||
|
}
|
||||||
|
if (removedDefaultPatterns.length > 0) {
|
||||||
|
result.siteRulesMeta = {
|
||||||
|
removedDefaultPatterns: removedDefaultPatterns
|
||||||
|
};
|
||||||
|
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandSiteRules(siteRules, siteRulesMeta) {
|
||||||
|
var defaultRules = getDefaultSiteRules();
|
||||||
|
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
|
||||||
|
if (defaultRules.length === 0) {
|
||||||
|
return Array.isArray(siteRules) ? clonePlainData(siteRules) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedDefaultPatterns = new Set(
|
||||||
|
siteRulesMeta && Array.isArray(siteRulesMeta.removedDefaultPatterns)
|
||||||
|
? siteRulesMeta.removedDefaultPatterns
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
var modifiedDefaultRules = Object.create(null);
|
||||||
|
var customRules = [];
|
||||||
|
|
||||||
|
if (Array.isArray(siteRules)) {
|
||||||
|
siteRules.forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (
|
||||||
|
pattern &&
|
||||||
|
Object.prototype.hasOwnProperty.call(defaultRulesByPattern, pattern)
|
||||||
|
) {
|
||||||
|
modifiedDefaultRules[pattern] = clonePlainData(rule);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customRules.push(clonePlainData(rule));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedRules = [];
|
||||||
|
|
||||||
|
defaultRules.forEach(function (rule) {
|
||||||
|
var pattern = rule && typeof rule.pattern === "string" ? rule.pattern : "";
|
||||||
|
if (!pattern || removedDefaultPatterns.has(pattern)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedDefaultRules[pattern]) {
|
||||||
|
mergedRules.push(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
clonePlainData(rule),
|
||||||
|
clonePlainData(modifiedDefaultRules[pattern])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedRules.push(clonePlainData(rule));
|
||||||
|
});
|
||||||
|
|
||||||
|
customRules.forEach(function (rule) {
|
||||||
|
mergedRules.push(rule);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStoredSettingsDiff(currentSettings) {
|
||||||
|
var defaults = clonePlainData(DEFAULT_SETTINGS);
|
||||||
|
var normalized = deepMergeDefaults(defaults, currentSettings || {});
|
||||||
|
var siteRuleData = compressSiteRules(normalized.siteRules, normalized);
|
||||||
|
var diffDefaults = {};
|
||||||
|
var diff = {};
|
||||||
|
|
||||||
|
delete normalized.siteRules;
|
||||||
|
delete normalized.siteRulesMeta;
|
||||||
|
delete normalized.siteRulesFormat;
|
||||||
|
delete normalized.hideWithYouTubeControls;
|
||||||
|
|
||||||
|
if (siteRuleData.siteRules) {
|
||||||
|
normalized.siteRules = siteRuleData.siteRules;
|
||||||
|
}
|
||||||
|
if (siteRuleData.siteRulesMeta) {
|
||||||
|
normalized.siteRulesMeta = siteRuleData.siteRulesMeta;
|
||||||
|
}
|
||||||
|
if (siteRuleData.siteRulesFormat) {
|
||||||
|
normalized.siteRulesFormat = siteRuleData.siteRulesFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIFFABLE_OPTION_KEYS.forEach(function (key) {
|
||||||
|
if (hasOwn(DEFAULT_SETTINGS, key)) {
|
||||||
|
diffDefaults[key] = clonePlainData(DEFAULT_SETTINGS[key]);
|
||||||
|
}
|
||||||
|
if (!hasOwn(normalized, key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var valueDiff = deepDiff(normalized[key], diffDefaults[key]);
|
||||||
|
if (valueDiff !== undefined) {
|
||||||
|
diff[key] = valueDiff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandStoredSettings(storage) {
|
||||||
|
var raw = clonePlainData(storage) || {};
|
||||||
|
var expanded = deepMergeDefaults(DEFAULT_SETTINGS, raw);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasOwn(raw, "hideWithControls") &&
|
||||||
|
hasOwn(raw, "hideWithYouTubeControls")
|
||||||
|
) {
|
||||||
|
expanded.hideWithControls = Boolean(raw.hideWithYouTubeControls);
|
||||||
|
}
|
||||||
|
expanded.hideWithYouTubeControls = expanded.hideWithControls;
|
||||||
|
|
||||||
|
if (raw.siteRulesFormat === SITE_RULES_DIFF_FORMAT) {
|
||||||
|
expanded.siteRules = expandSiteRules(raw.siteRules, raw.siteRulesMeta);
|
||||||
|
} else if (Array.isArray(raw.siteRules)) {
|
||||||
|
expanded.siteRules = clonePlainData(raw.siteRules);
|
||||||
|
} else {
|
||||||
|
expanded.siteRules = getDefaultSiteRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeStringRegExp(str) {
|
||||||
|
var matcher = /[|\\{}()[\]^$+*?.]/g;
|
||||||
|
return String(str).replace(matcher, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function siteRuleMatchesUrl(rule, currentUrl) {
|
||||||
|
if (!rule || !rule.pattern || !currentUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = String(rule.pattern).trim();
|
||||||
|
if (!pattern) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex;
|
||||||
|
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
||||||
|
try {
|
||||||
|
var lastSlash = pattern.lastIndexOf("/");
|
||||||
|
regex = new RegExp(
|
||||||
|
pattern.substring(1, lastSlash),
|
||||||
|
pattern.substring(lastSlash + 1)
|
||||||
|
);
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(escapeStringRegExp(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(regex && regex.test(currentUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMatchingSiteRules(currentUrl, siteRules) {
|
||||||
|
if (!currentUrl || !Array.isArray(siteRules)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedRules = [];
|
||||||
|
for (var i = 0; i < siteRules.length; i++) {
|
||||||
|
if (siteRuleMatchesUrl(siteRules[i], currentUrl)) {
|
||||||
|
matchedRules.push(siteRules[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedRules.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedRule = {};
|
||||||
|
matchedRules.forEach(function (rule) {
|
||||||
|
Object.keys(rule).forEach(function (key) {
|
||||||
|
var value = rule[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
mergedRule[key] = clonePlainData(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
mergedRule[key] = clonePlainData(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mergedRule[key] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSiteRuleDisabled(rule) {
|
||||||
|
return Boolean(
|
||||||
|
rule &&
|
||||||
|
(
|
||||||
|
rule.enabled === false ||
|
||||||
|
(typeof rule.enabled === "undefined" && rule.disableExtension === true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.vscClonePlainData = clonePlainData;
|
||||||
|
global.vscAreComparableValuesEqual = areComparableValuesEqual;
|
||||||
|
global.vscDeepMergeDefaults = deepMergeDefaults;
|
||||||
|
global.vscBuildStoredSettingsDiff = buildStoredSettingsDiff;
|
||||||
|
global.vscExpandStoredSettings = expandStoredSettings;
|
||||||
|
global.vscGetSettingsDefaults = function () {
|
||||||
|
return clonePlainData(DEFAULT_SETTINGS);
|
||||||
|
};
|
||||||
|
global.vscGetManagedSyncKeys = function () {
|
||||||
|
return MANAGED_SYNC_KEYS.slice();
|
||||||
|
};
|
||||||
|
global.vscGetSiteRulesDiffFormat = function () {
|
||||||
|
return SITE_RULES_DIFF_FORMAT;
|
||||||
|
};
|
||||||
|
global.vscMatchSiteRule = mergeMatchingSiteRules;
|
||||||
|
global.vscSiteRuleMatchesUrl = siteRuleMatchesUrl;
|
||||||
|
global.vscIsSiteRuleDisabled = isSiteRuleDisabled;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this);
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.siteRules = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||||
|
|
||||||
|
function escapeStringRegExp(str) {
|
||||||
|
return String(str).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileSiteRulePattern(pattern) {
|
||||||
|
if (typeof pattern !== "string") return null;
|
||||||
|
|
||||||
|
var normalizedPattern = pattern.replace(regStrip, "");
|
||||||
|
if (normalizedPattern.length === 0) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedPattern.startsWith("/") &&
|
||||||
|
normalizedPattern.lastIndexOf("/") > 0
|
||||||
|
) {
|
||||||
|
var lastSlash = normalizedPattern.lastIndexOf("/");
|
||||||
|
return new RegExp(
|
||||||
|
normalizedPattern.substring(1, lastSlash),
|
||||||
|
normalizedPattern.substring(lastSlash + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(escapeStringRegExp(normalizedPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSiteRule(url, siteRules) {
|
||||||
|
if (!url || !Array.isArray(siteRules)) return null;
|
||||||
|
|
||||||
|
for (var i = 0; i < siteRules.length; i++) {
|
||||||
|
var rule = siteRules[i];
|
||||||
|
if (!rule || !rule.pattern) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var re = compileSiteRulePattern(rule.pattern);
|
||||||
|
if (re && re.test(url)) {
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSiteRuleDisabled(rule) {
|
||||||
|
return Boolean(
|
||||||
|
rule &&
|
||||||
|
(rule.enabled === false || rule.disableExtension === true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Speeder should run on this URL given global enabled and the matched rule (if any).
|
||||||
|
* - No rule: follows global (enabled unless explicitly false).
|
||||||
|
* - Rule with site "off" / disableExtension: always inactive (blacklist).
|
||||||
|
* - Rule with site "on": active even when global is off (whitelist).
|
||||||
|
*/
|
||||||
|
function isSpeederActiveForSite(globalEnabled, siteRule) {
|
||||||
|
var globalOn = globalEnabled !== false;
|
||||||
|
if (!siteRule) {
|
||||||
|
return globalOn;
|
||||||
|
}
|
||||||
|
if (isSiteRuleDisabled(siteRule)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
compileSiteRulePattern: compileSiteRulePattern,
|
||||||
|
escapeStringRegExp: escapeStringRegExp,
|
||||||
|
isSiteRuleDisabled: isSiteRuleDisabled,
|
||||||
|
isSpeederActiveForSite: isSpeederActiveForSite,
|
||||||
|
matchSiteRule: matchSiteRule
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Inline SVG icons (Lucide-style strokes, compatible with https://lucide.dev — ISC license).
|
||||||
|
* Use stroke="currentColor" so buttons inherit foreground for monochrome UI.
|
||||||
|
*/
|
||||||
|
var VSC_ICON_SIZE_DEFAULT = 18;
|
||||||
|
var VSC_SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
/** Inner SVG markup only (paths / shapes inside <svg>). */
|
||||||
|
var vscUiIconPaths = {
|
||||||
|
rewind:
|
||||||
|
'<polygon points="11 19 2 12 11 5 11 19"/><polygon points="22 19 13 12 22 5 22 19"/>',
|
||||||
|
advance:
|
||||||
|
'<polygon points="13 19 22 12 13 5 13 19"/><polygon points="2 19 11 12 2 5 2 19"/>',
|
||||||
|
reset:
|
||||||
|
'<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>',
|
||||||
|
slower: '<line x1="5" y1="12" x2="19" y2="12"/>',
|
||||||
|
faster:
|
||||||
|
'<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
||||||
|
softer:
|
||||||
|
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="16" y1="12" x2="22" y2="12"/>',
|
||||||
|
louder:
|
||||||
|
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="17" y1="9" x2="17" y2="15"/><line x1="14" y1="12" x2="20" y2="12"/>',
|
||||||
|
moreHorizontal:
|
||||||
|
'<circle cx="6" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="18" cy="12" r="1.5"/>',
|
||||||
|
chevronUp: '<path d="m18 15-6-6-6 6"/>',
|
||||||
|
display:
|
||||||
|
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
|
fast: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
|
||||||
|
settings:
|
||||||
|
'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
||||||
|
pause:
|
||||||
|
'<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>',
|
||||||
|
muted:
|
||||||
|
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>',
|
||||||
|
mark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>',
|
||||||
|
jump:
|
||||||
|
'<polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/>',
|
||||||
|
nudge: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
||||||
|
/** Lucide check — subtitle nudge on */
|
||||||
|
subtitleNudgeOn: '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
/** Lucide x — subtitle nudge off */
|
||||||
|
subtitleNudgeOff:
|
||||||
|
'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} [size] - width/height in px
|
||||||
|
* @returns {string} full <svg>…</svg>
|
||||||
|
*/
|
||||||
|
function vscIconSvgString(action, size) {
|
||||||
|
var inner = vscUiIconPaths[action];
|
||||||
|
if (!inner) return "";
|
||||||
|
var s = size != null ? size : VSC_ICON_SIZE_DEFAULT;
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
||||||
|
s +
|
||||||
|
'" height="' +
|
||||||
|
s +
|
||||||
|
'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
||||||
|
inner +
|
||||||
|
"</svg>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscClearElement(el) {
|
||||||
|
if (!el) return;
|
||||||
|
while (el.firstChild) {
|
||||||
|
el.removeChild(el.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscSanitizeSvgTree(svg) {
|
||||||
|
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
|
||||||
|
|
||||||
|
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
[svg].concat(Array.from(svg.querySelectorAll("*"))).forEach(function (el) {
|
||||||
|
for (var i = el.attributes.length - 1; i >= 0; i--) {
|
||||||
|
var attr = el.attributes[i];
|
||||||
|
var name = attr.name.toLowerCase();
|
||||||
|
var val = attr.value;
|
||||||
|
if (name.indexOf("on") === 0) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(name === "href" || name === "xlink:href") &&
|
||||||
|
/^\s*javascript:/i.test(val)
|
||||||
|
) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", VSC_SVG_NS);
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscCreateSvgNode(doc, svgText) {
|
||||||
|
if (!doc || !svgText || typeof svgText !== "string") return null;
|
||||||
|
var clean = String(svgText).replace(/\0/g, "").trim();
|
||||||
|
if (!clean || !/<svg[\s>]/i.test(clean)) return null;
|
||||||
|
|
||||||
|
var parsed = new DOMParser().parseFromString(clean, "image/svg+xml");
|
||||||
|
if (parsed.querySelector("parsererror")) return null;
|
||||||
|
|
||||||
|
var svg = vscSanitizeSvgTree(parsed.querySelector("svg"));
|
||||||
|
if (!svg) return null;
|
||||||
|
|
||||||
|
return doc.importNode(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscSetSvgContent(el, svgText) {
|
||||||
|
if (!el) return false;
|
||||||
|
vscClearElement(el);
|
||||||
|
|
||||||
|
var doc = el.ownerDocument || document;
|
||||||
|
var svg = vscCreateSvgNode(doc, svgText);
|
||||||
|
if (!svg) return false;
|
||||||
|
|
||||||
|
el.appendChild(svg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscCreateSvgWrap(doc, svgText, className) {
|
||||||
|
if (!doc) return null;
|
||||||
|
var span = doc.createElement("span");
|
||||||
|
span.className = className || "vsc-btn-icon";
|
||||||
|
if (!vscSetSvgContent(span, svgText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Document} doc
|
||||||
|
* @param {string} action
|
||||||
|
* @returns {HTMLElement|null} wrapper span containing svg, or null if no icon
|
||||||
|
*/
|
||||||
|
function vscIconWrap(doc, action, size) {
|
||||||
|
var html = vscIconSvgString(action, size);
|
||||||
|
if (!html) return null;
|
||||||
|
return vscCreateSvgWrap(doc, html, "vsc-btn-icon");
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
// Import/Export functionality for Video Speed Controller settings
|
|
||||||
|
|
||||||
function generateBackupFilename() {
|
|
||||||
const now = new Date();
|
|
||||||
const year = now.getFullYear();
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(now.getDate()).padStart(2, "0");
|
|
||||||
const hours = String(now.getHours()).padStart(2, "0");
|
|
||||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
||||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
||||||
return `speeder-backup_${year}-${month}-${day}_${hours}.${minutes}.${seconds}.json`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportSettings() {
|
|
||||||
chrome.storage.sync.get(null, function (storage) {
|
|
||||||
const backup = {
|
|
||||||
version: "1.0",
|
|
||||||
exportDate: new Date().toISOString(),
|
|
||||||
settings: storage
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataStr = JSON.stringify(backup, null, 2);
|
|
||||||
const blob = new Blob([dataStr], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = generateBackupFilename();
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
showStatus("Settings exported successfully");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function importSettings() {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "file";
|
|
||||||
input.accept = ".json";
|
|
||||||
|
|
||||||
input.onchange = function (event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
try {
|
|
||||||
const backup = JSON.parse(e.target.result);
|
|
||||||
let settingsToImport = null;
|
|
||||||
|
|
||||||
// Detect backup format: check for 'settings' wrapper or raw storage keys
|
|
||||||
if (backup.settings && typeof backup.settings === "object") {
|
|
||||||
settingsToImport = backup.settings;
|
|
||||||
} else if (typeof backup === "object" && (backup.keyBindings || backup.rememberSpeed !== undefined)) {
|
|
||||||
settingsToImport = backup; // Raw storage object
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settingsToImport) {
|
|
||||||
showStatus("Error: Invalid backup file format", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import all settings
|
|
||||||
chrome.storage.sync.clear(function () {
|
|
||||||
// If clear fails, we still try to set
|
|
||||||
chrome.storage.sync.set(settingsToImport, function () {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
showStatus("Error: Failed to save imported settings - " + chrome.runtime.lastError.message, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showStatus("Settings imported successfully. Reloading...");
|
|
||||||
setTimeout(function () {
|
|
||||||
if (typeof restore_options === "function") {
|
|
||||||
restore_options();
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
showStatus("Error: Failed to parse backup file - " + err.message, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.onerror = function () {
|
|
||||||
showStatus("Error: Failed to read file", true);
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showStatus(message, isError = false) {
|
|
||||||
const status = document.getElementById("status");
|
|
||||||
if (status) {
|
|
||||||
status.textContent = message;
|
|
||||||
status.style.color = isError ? "#d32f2f" : "";
|
|
||||||
setTimeout(function () {
|
|
||||||
status.textContent = "";
|
|
||||||
status.style.color = "";
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize import/export buttons when DOM is ready
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", initImportExport);
|
|
||||||
} else {
|
|
||||||
initImportExport();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initImportExport() {
|
|
||||||
const exportBtn = document.getElementById("exportSettings");
|
|
||||||
const importBtn = document.getElementById("importSettings");
|
|
||||||
|
|
||||||
if (exportBtn) {
|
|
||||||
exportBtn.addEventListener("click", exportSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importBtn) {
|
|
||||||
importBtn.addEventListener("click", importSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Speeder",
|
|
||||||
"short_name": "Speeder",
|
|
||||||
"version": "5.0.2",
|
|
||||||
"manifest_version": 2,
|
|
||||||
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
|
||||||
"homepage_url": "https://github.com/SoPat712/speeder",
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}",
|
|
||||||
"data_collection_permissions": {
|
|
||||||
"required": ["none"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"16": "icons/icon16.png",
|
|
||||||
"48": "icons/icon48.png",
|
|
||||||
"128": "icons/icon128.png"
|
|
||||||
},
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
},
|
|
||||||
"permissions": ["storage"],
|
|
||||||
"options_ui": {
|
|
||||||
"page": "options.html",
|
|
||||||
"open_in_tab": false
|
|
||||||
},
|
|
||||||
"browser_action": {
|
|
||||||
"default_icon": {
|
|
||||||
"19": "icons/icon19.png",
|
|
||||||
"38": "icons/icon38.png",
|
|
||||||
"48": "icons/icon48.png"
|
|
||||||
},
|
|
||||||
"default_popup": "popup.html"
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"all_frames": true,
|
|
||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
|
||||||
"match_about_blank": true,
|
|
||||||
"exclude_matches": [
|
|
||||||
"https://plus.google.com/hangouts/*",
|
|
||||||
"https://hangouts.google.com/*",
|
|
||||||
"https://meet.google.com/*"
|
|
||||||
],
|
|
||||||
"css": ["inject.css"],
|
|
||||||
"js": ["inject.js"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"web_accessible_resources": ["inject.css", "shadow.css"]
|
|
||||||
}
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg: #f4f5f7;
|
|
||||||
--panel: #ffffff;
|
|
||||||
--panel-subtle: #fafbfc;
|
|
||||||
--border: #e2e5e9;
|
|
||||||
--border-strong: #d4d9e0;
|
|
||||||
--text: #17191c;
|
|
||||||
--muted: #626b76;
|
|
||||||
--accent: #111827;
|
|
||||||
--danger: #b42318;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 24px 16px 40px;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font: 14px/1.45 "Avenir Next", "SF Pro Text", "Segoe UI", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-shell {
|
|
||||||
width: min(880px, 100%);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-block {
|
|
||||||
max-width: 560px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Avenir Next", "SF Pro Display", "Segoe UI", sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-subtitle,
|
|
||||||
.section-intro {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 26px;
|
|
||||||
padding: 0 9px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-stack {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card {
|
|
||||||
padding: 18px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-heading {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaults-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: var(--border);
|
|
||||||
margin: 14px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaults-sub-heading {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.75em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
a:visited {
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration-color: #c6ccd5;
|
|
||||||
text-underline-offset: 0.14em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover,
|
|
||||||
a:focus {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding: 0.08em 0.38em;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--panel-subtle);
|
|
||||||
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
|
||||||
font-size: 0.92em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
appearance: none;
|
|
||||||
min-height: 36px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 120ms ease, border-color 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #f8f9fb;
|
|
||||||
border-color: #c5ccd5;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
background: #f1f3f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus-visible,
|
|
||||||
input[type="text"]:focus,
|
|
||||||
select:focus,
|
|
||||||
textarea:focus {
|
|
||||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save {
|
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save:hover {
|
|
||||||
background: #1f2937;
|
|
||||||
border-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 36px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus,
|
|
||||||
select:focus,
|
|
||||||
textarea:focus {
|
|
||||||
border-color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 2px 0 0;
|
|
||||||
accent-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
label em {
|
|
||||||
display: block;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcuts-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 120px 120px;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcuts-grid .shortcut-row:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-row.customs {
|
|
||||||
grid-template-columns: minmax(0, 1fr) 120px 120px 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-label {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customKey,
|
|
||||||
.customValue {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#addShortcutSelector {
|
|
||||||
width: min(220px, 100%);
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.removeParent,
|
|
||||||
.toggle-site-rule {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.removeParent {
|
|
||||||
color: var(--danger);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-site-rule {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 160px;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: start;
|
|
||||||
padding: 10px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card .row:first-of-type {
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row.row-controller-margin {
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controller-margin-inputs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin-pad-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin-pad-mini {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controller-margin-inputs input[type="text"] {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-option.site-rule-margin-option {
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 220px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-override-section {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-content > .site-rule-override-section:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-override-lead {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-override-lead input {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-override-section .site-override-fields,
|
|
||||||
.site-rule-override-section .site-placement-container,
|
|
||||||
.site-rule-override-section .site-visibility-container,
|
|
||||||
.site-rule-override-section .site-autohide-container,
|
|
||||||
.site-rule-override-section .site-playback-container,
|
|
||||||
.site-rule-override-section .site-opacity-container,
|
|
||||||
.site-rule-override-section .site-subtitleNudge-container {
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-editor {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-editor-disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-zone-label {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-dropzone {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 52px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 2px dashed var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--panel-subtle);
|
|
||||||
transition: border-color 150ms ease, background 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-dropzone.cb-over {
|
|
||||||
border-color: var(--accent);
|
|
||||||
background: rgba(17, 24, 39, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-active-zone:empty::after {
|
|
||||||
content: "Drag buttons here";
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-available-zone:empty::after {
|
|
||||||
content: "All buttons active";
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 7px 12px 7px 8px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--panel);
|
|
||||||
cursor: grab;
|
|
||||||
user-select: none;
|
|
||||||
transition: box-shadow 150ms ease, opacity 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-block:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-block:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-block.cb-dragging {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-grip {
|
|
||||||
width: 6px;
|
|
||||||
min-width: 6px;
|
|
||||||
height: 14px;
|
|
||||||
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--panel-subtle);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#siteRulesContainer {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--panel-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 36px minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-pattern {
|
|
||||||
min-width: 0;
|
|
||||||
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-body {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-content.disabled-rule {
|
|
||||||
opacity: 0.55;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-option {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 150px;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: start;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-body > .site-rule-option:first-child,
|
|
||||||
.site-rule-content > .site-rule-option:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-option label {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-controlbar,
|
|
||||||
.site-rule-shortcuts {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-controlbar > label,
|
|
||||||
.site-rule-shortcuts > label {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-controlbar-container,
|
|
||||||
.site-popup-controlbar-container {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-shortcuts-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-shortcuts-container .shortcut-row {
|
|
||||||
grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr);
|
|
||||||
padding: 8px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-shortcuts-container .shortcut-row:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.force-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.force-label input {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status {
|
|
||||||
min-height: 1.3em;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#faq hr {
|
|
||||||
height: 1px;
|
|
||||||
margin: 0 0 14px;
|
|
||||||
border: 0;
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.support-footer {
|
|
||||||
padding: 16px 20px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.support-footer p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.support-footer a {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.shortcut-row,
|
|
||||||
.shortcut-row.customs,
|
|
||||||
.row,
|
|
||||||
.site-rule-option,
|
|
||||||
.site-shortcuts-container .shortcut-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row button,
|
|
||||||
#addShortcutSelector {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-header {
|
|
||||||
grid-template-columns: 36px minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-site-rule {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
|
||||||
body {
|
|
||||||
padding: 16px 12px 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-rule-header {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #111315;
|
|
||||||
--panel: #171a1d;
|
|
||||||
--panel-subtle: #1b1f23;
|
|
||||||
--border: #2b3138;
|
|
||||||
--border-strong: #3a414a;
|
|
||||||
--text: #f2f4f6;
|
|
||||||
--muted: #a0a8b2;
|
|
||||||
--accent: #f2f4f6;
|
|
||||||
--danger: #ff8a80;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
a:visited {
|
|
||||||
color: #f2f4f6;
|
|
||||||
text-decoration-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save {
|
|
||||||
background: #f2f4f6;
|
|
||||||
border-color: #f2f4f6;
|
|
||||||
color: #111315;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save:hover {
|
|
||||||
background: #dfe3e8;
|
|
||||||
border-color: #dfe3e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cb-dropzone.cb-over {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus,
|
|
||||||
select:focus,
|
|
||||||
textarea:focus {
|
|
||||||
border-color: #6b7280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,616 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Speeder Settings</title>
|
|
||||||
<link rel="stylesheet" href="options.css" />
|
|
||||||
<script src="options.js"></script>
|
|
||||||
<script src="importExport.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="page-shell">
|
|
||||||
<header class="page-header">
|
|
||||||
<div class="title-row">
|
|
||||||
<div class="title-block">
|
|
||||||
<h1>Speeder settings</h1>
|
|
||||||
<p class="page-subtitle">Shortcuts, defaults, and site rules.</p>
|
|
||||||
</div>
|
|
||||||
<div class="version">v<span id="app-version"></span></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="settings-stack">
|
|
||||||
<section id="customs" class="settings-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Shortcuts</h3>
|
|
||||||
<p class="section-intro">Backspace clears a shortcut. Escape disables it.</p>
|
|
||||||
</div>
|
|
||||||
<div class="shortcuts-grid">
|
|
||||||
<div class="shortcut-row" id="display" data-action="display">
|
|
||||||
<div class="shortcut-label">Show/hide controller</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value"
|
|
||||||
value="N/A"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="move" data-action="move">
|
|
||||||
<div class="shortcut-label">Move controller</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value"
|
|
||||||
value="N/A"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="slower" data-action="slower">
|
|
||||||
<div class="shortcut-label">Decrease speed</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value (0.10)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="faster" data-action="faster">
|
|
||||||
<div class="shortcut-label">Increase speed</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value (0.10)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="rewind" data-action="rewind">
|
|
||||||
<div class="shortcut-label">Rewind</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value (10)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="advance" data-action="advance">
|
|
||||||
<div class="shortcut-label">Advance</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value (10)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="reset" data-action="reset">
|
|
||||||
<div class="shortcut-label">Reset speed</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value"
|
|
||||||
value="N/A"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-row" id="fast" data-action="fast">
|
|
||||||
<div class="shortcut-label">Preferred speed</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value (1.80)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="shortcut-row"
|
|
||||||
id="toggleSubtitleNudge"
|
|
||||||
data-action="toggleSubtitleNudge"
|
|
||||||
>
|
|
||||||
<div class="shortcut-label">Toggle subtitle nudge</div>
|
|
||||||
<input
|
|
||||||
class="customKey"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="press a key"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="customValue"
|
|
||||||
type="text"
|
|
||||||
placeholder="value"
|
|
||||||
value="N/A"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select id="addShortcutSelector">
|
|
||||||
<option value="">Add shortcut…</option>
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="generalSettings" class="settings-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Defaults</h3>
|
|
||||||
<p class="section-intro">Used unless a site rule overrides it.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="defaults-sub-heading">General</h4>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label for="enabled">Enable</label>
|
|
||||||
<input id="enabled" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="audioBoolean">Work on audio</label>
|
|
||||||
<input id="audioBoolean" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="defaults-divider"></div>
|
|
||||||
<h4 class="defaults-sub-heading">Playback</h4>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label for="rememberSpeed">Remember playback speed</label>
|
|
||||||
<input id="rememberSpeed" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="forceLastSavedSpeed"
|
|
||||||
>Force last saved speed<br />
|
|
||||||
<em
|
|
||||||
>Useful when a video player tries to override the speed you set
|
|
||||||
in Speeder.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input id="forceLastSavedSpeed" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="defaults-divider"></div>
|
|
||||||
<h4 class="defaults-sub-heading">Controller</h4>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label for="startHidden">Hide controller by default</label>
|
|
||||||
<input id="startHidden" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="controllerLocation">Default controller location</label>
|
|
||||||
<select id="controllerLocation">
|
|
||||||
<option value="top-left">Top left</option>
|
|
||||||
<option value="top-center">Top center</option>
|
|
||||||
<option value="top-right">Top right</option>
|
|
||||||
<option value="middle-right">Middle right</option>
|
|
||||||
<option value="bottom-right">Bottom right</option>
|
|
||||||
<option value="bottom-center">Bottom center</option>
|
|
||||||
<option value="bottom-left">Bottom left</option>
|
|
||||||
<option value="middle-left">Middle left</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="controllerOpacity">Controller opacity</label>
|
|
||||||
<input id="controllerOpacity" type="text" value="" />
|
|
||||||
</div>
|
|
||||||
<div class="row row-controller-margin">
|
|
||||||
<label for="controllerMarginTop"
|
|
||||||
>Controller margin (px)<br />
|
|
||||||
<em
|
|
||||||
>Shifts the whole control from its preset position (CSS
|
|
||||||
margins). Top and bottom. 0–200.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<div class="controller-margin-inputs" aria-label="Controller margin in pixels">
|
|
||||||
<div class="margin-pad-cell">
|
|
||||||
<span class="margin-pad-mini">Top</span>
|
|
||||||
<input id="controllerMarginTop" type="text" inputmode="numeric" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="margin-pad-cell">
|
|
||||||
<span class="margin-pad-mini">Bottom</span>
|
|
||||||
<input id="controllerMarginBottom" type="text" inputmode="numeric" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="hideWithControls"
|
|
||||||
>Hide with controls<br />
|
|
||||||
<em
|
|
||||||
>Fade the controller in and out with the video interface:
|
|
||||||
perfect sync on YouTube, idle-based elsewhere.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input id="hideWithControls" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="hideWithControlsTimer"
|
|
||||||
>Auto-hide timer (seconds)<br />
|
|
||||||
<em
|
|
||||||
>Seconds of inactivity before hiding: 0.1–15 for
|
|
||||||
non-YouTube sites.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="showPopupControlBar">Show popup control bar</label>
|
|
||||||
<input id="showPopupControlBar" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="defaults-divider"></div>
|
|
||||||
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label for="enableSubtitleNudge"
|
|
||||||
>Enable subtitle nudge<br /><em
|
|
||||||
>Makes tiny playback changes to help keep subtitles aligned.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input id="enableSubtitleNudge" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="subtitleNudgeInterval"
|
|
||||||
>Nudge interval (milliseconds)<br /><em
|
|
||||||
>How often to nudge: 10–1000. Smaller values are more
|
|
||||||
frequent. Default: 50.</em
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="subtitleNudgeInterval"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
placeholder="50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="controlBarSettings" class="settings-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Control bar</h3>
|
|
||||||
<p class="section-intro">
|
|
||||||
Drag blocks to reorder. Move between Active and Available to show
|
|
||||||
or hide buttons.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="cb-editor">
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Active</div>
|
|
||||||
<div
|
|
||||||
id="controlBarActive"
|
|
||||||
class="cb-dropzone cb-active-zone"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Available</div>
|
|
||||||
<div
|
|
||||||
id="controlBarAvailable"
|
|
||||||
class="cb-dropzone cb-available-zone"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="popupControlBarSettings" class="settings-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Popup control bar</h3>
|
|
||||||
<p class="section-intro">
|
|
||||||
Configure which buttons appear in the browser popup control bar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label for="popupMatchHoverControls">Match hover controls</label>
|
|
||||||
<input id="popupMatchHoverControls" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Active</div>
|
|
||||||
<div
|
|
||||||
id="popupControlBarActive"
|
|
||||||
class="cb-dropzone cb-active-zone"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Available</div>
|
|
||||||
<div
|
|
||||||
id="popupControlBarAvailable"
|
|
||||||
class="cb-dropzone cb-available-zone"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="siteRules" class="settings-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Site rules</h3>
|
|
||||||
<p class="section-intro">
|
|
||||||
Use plain domains or
|
|
||||||
<a
|
|
||||||
href="https://www.regexpal.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Regex</a
|
|
||||||
>
|
|
||||||
patterns like <code>/(.+)youtube\.com(\/*)$/gi</code>. Turn on a
|
|
||||||
row only when you want that group to override the general defaults
|
|
||||||
above.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div id="siteRulesContainer"></div>
|
|
||||||
<button id="addSiteRule" type="button">Add Site Rule</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<template id="siteRuleTemplate">
|
|
||||||
<div class="site-rule">
|
|
||||||
<div class="site-rule-header">
|
|
||||||
<button type="button" class="toggle-site-rule" title="Expand/Collapse">+</button>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="site-pattern"
|
|
||||||
placeholder="e.g., youtube.com or /regex/gi"
|
|
||||||
/>
|
|
||||||
<button type="button" class="remove-site-rule">Remove</button>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-body">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" class="site-enabled" />
|
|
||||||
Enable Speeder on this site
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-content">
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-placement" />
|
|
||||||
Override placement for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-placement-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Default controller location:</label>
|
|
||||||
<select class="site-controllerLocation">
|
|
||||||
<option value="top-left">Top left</option>
|
|
||||||
<option value="top-center">Top center</option>
|
|
||||||
<option value="top-right">Top right</option>
|
|
||||||
<option value="middle-right">Middle right</option>
|
|
||||||
<option value="bottom-right">Bottom right</option>
|
|
||||||
<option value="bottom-center">Bottom center</option>
|
|
||||||
<option value="bottom-left">Bottom left</option>
|
|
||||||
<option value="middle-left">Middle left</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-option site-rule-margin-option">
|
|
||||||
<label
|
|
||||||
>Controller margin (px):<br /><em
|
|
||||||
>Shifts the whole control. 0–200.</em
|
|
||||||
></label
|
|
||||||
>
|
|
||||||
<div class="controller-margin-inputs">
|
|
||||||
<div class="margin-pad-cell">
|
|
||||||
<span class="margin-pad-mini">T</span>
|
|
||||||
<input type="text" class="site-controllerMarginTop" inputmode="numeric" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="margin-pad-cell">
|
|
||||||
<span class="margin-pad-mini">B</span>
|
|
||||||
<input type="text" class="site-controllerMarginBottom" inputmode="numeric" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-visibility" />
|
|
||||||
Override hide-by-default for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-visibility-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Hide controller by default:</label>
|
|
||||||
<input type="checkbox" class="site-startHidden" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-autohide" />
|
|
||||||
Override auto-hide for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-autohide-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" class="site-hideWithControls" />
|
|
||||||
Hide with controls (idle-based)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Auto-hide timer (0.1–15s):</label>
|
|
||||||
<input type="text" class="site-hideWithControlsTimer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-playback" />
|
|
||||||
Override playback for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-playback-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Remember playback speed:</label>
|
|
||||||
<input type="checkbox" class="site-rememberSpeed" />
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Force last saved speed:</label>
|
|
||||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Work on audio:</label>
|
|
||||||
<input type="checkbox" class="site-audioBoolean" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-opacity" />
|
|
||||||
Override opacity for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-opacity-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Controller opacity:</label>
|
|
||||||
<input type="text" class="site-controllerOpacity" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-override-section">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-subtitleNudge" />
|
|
||||||
Override subtitle nudge for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-subtitleNudge-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Enable subtitle nudge:</label>
|
|
||||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Nudge interval (10–1000ms):</label>
|
|
||||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-controlbar">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-controlbar" />
|
|
||||||
Override in-player control bar for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-controlbar-container" style="display: none">
|
|
||||||
<div class="cb-editor">
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Active</div>
|
|
||||||
<div class="cb-dropzone cb-active-zone site-cb-active"></div>
|
|
||||||
</div>
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Available</div>
|
|
||||||
<div class="cb-dropzone cb-available-zone site-cb-available"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-controlbar">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-popup-controlbar" />
|
|
||||||
Override extension popup for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-popup-controlbar-container" style="display: none">
|
|
||||||
<div class="site-rule-option">
|
|
||||||
<label>Show popup control bar</label>
|
|
||||||
<input type="checkbox" class="site-showPopupControlBar" />
|
|
||||||
</div>
|
|
||||||
<div class="cb-editor">
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Active</div>
|
|
||||||
<div class="cb-dropzone cb-active-zone site-popup-cb-active"></div>
|
|
||||||
</div>
|
|
||||||
<div class="cb-zone">
|
|
||||||
<div class="cb-zone-label">Available</div>
|
|
||||||
<div class="cb-dropzone cb-available-zone site-popup-cb-available"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="site-rule-shortcuts">
|
|
||||||
<label class="site-override-lead">
|
|
||||||
<input type="checkbox" class="override-shortcuts" />
|
|
||||||
Override shortcuts for this site
|
|
||||||
</label>
|
|
||||||
<div class="site-shortcuts-container" style="display: none"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<section class="settings-card action-card">
|
|
||||||
<div class="section-heading">
|
|
||||||
<h3>Actions</h3>
|
|
||||||
<p class="section-intro">Save, restore, export, or import settings.</p>
|
|
||||||
</div>
|
|
||||||
<div class="action-row">
|
|
||||||
<button id="save">Save Changes</button>
|
|
||||||
<button id="restore">Restore Defaults</button>
|
|
||||||
<button id="exportSettings">Export Settings</button>
|
|
||||||
<button id="importSettings">Import Settings</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" role="status" aria-live="polite"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="faq" class="settings-card info-card">
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h4>Extension controls not appearing?</h4>
|
|
||||||
<p>
|
|
||||||
This extension only works with HTML5 audio and video. If the
|
|
||||||
controls never appear, you may be looking at Flash content instead.
|
|
||||||
Right-click the player to check: if the menu mentions Flash, that
|
|
||||||
is the issue. Most sites will fall back to HTML5 when Flash is not
|
|
||||||
available, so disabling Flash in the browser can help.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="support-footer settings-card">
|
|
||||||
<p>
|
|
||||||
If Speeder has been useful, consider supporting its development via
|
|
||||||
<a
|
|
||||||
href="https://github.com/sponsors/SoPat712"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>GitHub Sponsor</a
|
|
||||||
>
|
|
||||||
or
|
|
||||||
<a
|
|
||||||
href="https://ko-fi.com/joshpatra"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Ko-Fi</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,2128 @@
|
|||||||
|
{
|
||||||
|
"name": "speeder",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "speeder",
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.1.0",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "3.2.4",
|
||||||
|
"@vitest/utils": "3.2.4",
|
||||||
|
"chai": "^5.2.0",
|
||||||
|
"tinyrainbow": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "3.2.4",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "3.2.4",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"strip-literal": "^3.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "3.2.4",
|
||||||
|
"magic-string": "^0.30.17",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyspy": "^4.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "3.2.4",
|
||||||
|
"loupe": "^3.1.4",
|
||||||
|
"tinyrainbow": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cac": {
|
||||||
|
"version": "6.7.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"assertion-error": "^2.0.1",
|
||||||
|
"check-error": "^2.1.1",
|
||||||
|
"deep-eql": "^5.0.1",
|
||||||
|
"loupe": "^3.1.0",
|
||||||
|
"pathval": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/check-error": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/deep-eql": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.7",
|
||||||
|
"@esbuild/android-arm": "0.27.7",
|
||||||
|
"@esbuild/android-arm64": "0.27.7",
|
||||||
|
"@esbuild/android-x64": "0.27.7",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.7",
|
||||||
|
"@esbuild/darwin-x64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.7",
|
||||||
|
"@esbuild/linux-arm": "0.27.7",
|
||||||
|
"@esbuild/linux-arm64": "0.27.7",
|
||||||
|
"@esbuild/linux-ia32": "0.27.7",
|
||||||
|
"@esbuild/linux-loong64": "0.27.7",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.7",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.7",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.7",
|
||||||
|
"@esbuild/linux-s390x": "0.27.7",
|
||||||
|
"@esbuild/linux-x64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.7",
|
||||||
|
"@esbuild/sunos-x64": "0.27.7",
|
||||||
|
"@esbuild/win32-arm64": "0.27.7",
|
||||||
|
"@esbuild/win32-ia32": "0.27.7",
|
||||||
|
"@esbuild/win32-x64": "0.27.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "26.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.2.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.16",
|
||||||
|
"parse5": "^7.2.1",
|
||||||
|
"rrweb-cssom": "^0.8.0",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.1.1",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/loupe": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
|
||||||
|
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pathval": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
||||||
|
"@rollup/rollup-android-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-openbsd-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/strip-literal": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^9.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinypool": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyspy": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.86"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"rollup": "^4.43.0",
|
||||||
|
"tinyglobby": "^0.2.15"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"lightningcss": "^1.21.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-node": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"es-module-lexer": "^1.7.0",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite-node": "vite-node.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/expect": "3.2.4",
|
||||||
|
"@vitest/mocker": "3.2.4",
|
||||||
|
"@vitest/pretty-format": "^3.2.4",
|
||||||
|
"@vitest/runner": "3.2.4",
|
||||||
|
"@vitest/snapshot": "3.2.4",
|
||||||
|
"@vitest/spy": "3.2.4",
|
||||||
|
"@vitest/utils": "3.2.4",
|
||||||
|
"chai": "^5.2.0",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"expect-type": "^1.2.1",
|
||||||
|
"magic-string": "^0.30.17",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.2",
|
||||||
|
"std-env": "^3.9.0",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^0.3.2",
|
||||||
|
"tinyglobby": "^0.2.14",
|
||||||
|
"tinypool": "^1.1.1",
|
||||||
|
"tinyrainbow": "^2.0.0",
|
||||||
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||||
|
"vite-node": "3.2.4",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
|
"@vitest/browser": "3.2.4",
|
||||||
|
"@vitest/ui": "3.2.4",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/debug": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "speeder",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Speeder</title>
|
|
||||||
<link rel="stylesheet" href="popup.css" />
|
|
||||||
<script src="popup.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="popup-shell">
|
|
||||||
<div class="popup-header">
|
|
||||||
<span class="popup-title">Speeder</span>
|
|
||||||
<span class="popup-version">v<span id="app-version"></span></span>
|
|
||||||
</div>
|
|
||||||
<div class="popup-actions">
|
|
||||||
<button id="refresh">Rescan page for videos</button>
|
|
||||||
<div class="popup-divider"></div>
|
|
||||||
<div id="popupControlBar" class="popup-control-bar">
|
|
||||||
<span id="popupSpeed" class="popup-speed">1.00</span>
|
|
||||||
</div>
|
|
||||||
<div class="popup-divider"></div>
|
|
||||||
<button id="enable" class="hide">Enable</button>
|
|
||||||
<button id="disable">Disable</button>
|
|
||||||
</div>
|
|
||||||
<div id="status" class="popup-status hide"></div>
|
|
||||||
<div class="popup-links">
|
|
||||||
<button id="config">Settings</button>
|
|
||||||
<div class="popup-secondary">
|
|
||||||
<button id="feedback" class="secondary">Feedback</button>
|
|
||||||
<button id="about" class="secondary">About</button>
|
|
||||||
</div>
|
|
||||||
<div id="donateWrap" class="donate-wrap">
|
|
||||||
<button id="donate" class="secondary">Donate</button>
|
|
||||||
<div id="donateOptions" class="donate-split hide">
|
|
||||||
<button id="donateKofi" class="secondary">Ko-fi</button>
|
|
||||||
<button id="donateGithub" class="secondary">Sponsors</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Squash beta onto main, set manifest version, one release commit, push stable tag (v* without -beta).
|
||||||
|
# Does not merge dev or push to beta — promote only what is already on beta.
|
||||||
|
# Triggers .github/workflows/deploy.yml: listed AMO submission.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$ROOT"
|
||||||
|
MANIFEST_PATH="extension/manifest.json"
|
||||||
|
|
||||||
|
manifest_version() {
|
||||||
|
MANIFEST_PATH="$MANIFEST_PATH" python3 -c 'import json, os; print(json.load(open(os.environ["MANIFEST_PATH"]))["version"])'
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_manifest() {
|
||||||
|
local ver="$1"
|
||||||
|
VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
ver = os.environ["VER"]
|
||||||
|
path = os.environ["MANIFEST_PATH"]
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data["version"] = ver
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_semver() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s#"${s%%[![:space:]]*}"}"
|
||||||
|
s="${s%"${s##*[![:space:]]}"}"
|
||||||
|
s="${s#v}"
|
||||||
|
s="${s#V}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_semver() {
|
||||||
|
local s="$1"
|
||||||
|
if [[ -z "$s" ]]; then
|
||||||
|
echo "Error: empty version." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Error: invalid version (use something like 5.0.4)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout beta
|
||||||
|
git pull origin beta
|
||||||
|
|
||||||
|
echo "Current version on beta ($MANIFEST_PATH): $(manifest_version)"
|
||||||
|
read -r -p "Release version for $MANIFEST_PATH + tag (e.g. 5.0.4): " SEMVER_IN
|
||||||
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
|
validate_semver "$SEMVER"
|
||||||
|
|
||||||
|
TAG="v${SEMVER}"
|
||||||
|
if [[ "$TAG" == *-beta* ]]; then
|
||||||
|
echo "Warning: stable tags should not contain '-beta' (workflow would use unlisted + prerelease, not AMO listed)."
|
||||||
|
read -r -p "Continue anyway? [y/N] " w
|
||||||
|
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "This will:"
|
||||||
|
echo " 1. checkout main, merge --squash origin/beta (single release commit on main)"
|
||||||
|
echo " 2. set $MANIFEST_PATH to $SEMVER in that commit (if anything else changed, it is included too)"
|
||||||
|
echo " 3. push origin main, create tag $TAG, push tag (triggers listed AMO submit)"
|
||||||
|
echo " 4. checkout dev (merge main→dev yourself if you want them aligned)"
|
||||||
|
read -r -p "Continue? [y/N] " confirm
|
||||||
|
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
|
||||||
|
echo "🚀 Releasing stable $TAG to AMO (listed)"
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git merge --squash beta
|
||||||
|
bump_manifest "$SEMVER"
|
||||||
|
git add -A
|
||||||
|
git commit -m "Release $TAG"
|
||||||
|
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
|
||||||
|
echo "✅ Done: main squashed from beta, tagged $TAG (manifest $SEMVER)"
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Merge dev → beta, push beta, and push an annotated beta tag (v*-beta*).
|
||||||
|
# Triggers .github/workflows/deploy.yml: unlisted AMO sign + GitHub prerelease.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$ROOT"
|
||||||
|
MANIFEST_PATH="extension/manifest.json"
|
||||||
|
|
||||||
|
manifest_version() {
|
||||||
|
MANIFEST_PATH="$MANIFEST_PATH" python3 -c 'import json, os; print(json.load(open(os.environ["MANIFEST_PATH"]))["version"])'
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_manifest() {
|
||||||
|
local ver="$1"
|
||||||
|
VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
ver = os.environ["VER"]
|
||||||
|
path = os.environ["MANIFEST_PATH"]
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data["version"] = ver
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_semver() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s#"${s%%[![:space:]]*}"}"
|
||||||
|
s="${s%"${s##*[![:space:]]}"}"
|
||||||
|
s="${s#v}"
|
||||||
|
s="${s#V}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_semver() {
|
||||||
|
local s="$1"
|
||||||
|
if [[ -z "$s" ]]; then
|
||||||
|
echo "Error: empty version." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Error: invalid version (use something like 5.0.4 or 5.0.4-beta.1)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
echo "Current version in $MANIFEST_PATH: $(manifest_version)"
|
||||||
|
read -r -p "New version for $MANIFEST_PATH (e.g. 5.0.4): " SEMVER_IN
|
||||||
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
|
validate_semver "$SEMVER"
|
||||||
|
|
||||||
|
echo "Beta git tag will include '-beta' (required by deploy.yml)."
|
||||||
|
read -r -p "Beta tag suffix [beta.1]: " SUFFIX_IN
|
||||||
|
SUFFIX="${SUFFIX_IN#"${SUFFIX_IN%%[![:space:]]*}"}"
|
||||||
|
SUFFIX="${SUFFIX%"${SUFFIX##*[![:space:]]}"}"
|
||||||
|
SUFFIX="${SUFFIX:-beta.1}"
|
||||||
|
|
||||||
|
TAG="v${SEMVER}-${SUFFIX}"
|
||||||
|
if [[ "$TAG" != *-beta* ]]; then
|
||||||
|
echo "Error: beta tag must contain '-beta' for the workflow (got $TAG). Try suffix like beta.1." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "This will:"
|
||||||
|
echo " 1. set $MANIFEST_PATH version to $SEMVER, commit on dev, push origin dev"
|
||||||
|
echo " 2. checkout beta, merge dev (no-ff), push origin beta"
|
||||||
|
echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)"
|
||||||
|
echo " 4. checkout dev (main is not modified)"
|
||||||
|
read -r -p "Continue? [y/N] " confirm
|
||||||
|
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
|
||||||
|
echo "🚀 Releasing beta $TAG"
|
||||||
|
|
||||||
|
bump_manifest "$SEMVER"
|
||||||
|
git add "$MANIFEST_PATH"
|
||||||
|
git commit -m "Bump version to $SEMVER"
|
||||||
|
git push origin dev
|
||||||
|
|
||||||
|
git checkout beta
|
||||||
|
git pull origin beta
|
||||||
|
git merge dev --no-ff -m "$TAG"
|
||||||
|
git push origin beta
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
echo "✅ Done: beta $TAG (manifest $SEMVER; dev + beta + tag pushed)"
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
* {
|
|
||||||
line-height: 1.9em;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller:hover #controls,
|
|
||||||
#controller:focus-within #controls,
|
|
||||||
:host(:hover) #controls {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translate(0, 0);
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
cursor: default;
|
|
||||||
/* Stay above the local video surface without overtaking page-level dialogs. */
|
|
||||||
z-index: 1;
|
|
||||||
transition: top 160ms ease, left 160ms ease, transform 160ms ease,
|
|
||||||
opacity 160ms ease;
|
|
||||||
/* Insets are baked into left/top; bar must not wrap when narrow. Hover controls
|
|
||||||
extend into the inset area (past “logical” margin). */
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: visible;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller:hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller:hover > .draggable {
|
|
||||||
margin-right: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */
|
|
||||||
#controller[data-location="top-center"]:not([data-position-mode="manual"]) {
|
|
||||||
transform: translate(-50%, 0) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller[data-location="bottom-center"]:not([data-position-mode="manual"]) {
|
|
||||||
transform: translate(-50%, -100%) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controls {
|
|
||||||
display: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: visible;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controls > * + * {
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Standalone flash indicator next to speed text — hidden by default,
|
|
||||||
briefly shown when nudge is toggled via N key or click */
|
|
||||||
#nudge-flash-indicator {
|
|
||||||
display: none;
|
|
||||||
vertical-align: middle;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: 0.3em;
|
|
||||||
padding: 3px 6px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: "Lucida Console", Monaco, monospace;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-flash-indicator.visible {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide flash indicator when hovering — the one in #controls is visible instead */
|
|
||||||
#controller:hover #nudge-flash-indicator,
|
|
||||||
:host(:hover) #nudge-flash-indicator {
|
|
||||||
display: none !important;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-flash-indicator[data-enabled="true"] {
|
|
||||||
color: #fff;
|
|
||||||
background: #4b9135;
|
|
||||||
border: 1px solid #6ec754;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-flash-indicator[data-enabled="false"] {
|
|
||||||
color: #fff;
|
|
||||||
background: #943e3e;
|
|
||||||
border: 1px solid #c06060;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-indicator {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 3px 6px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: "Lucida Console", Monaco, monospace;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-indicator[data-enabled="true"] {
|
|
||||||
color: #fff;
|
|
||||||
background: #4b9135;
|
|
||||||
border: 1px solid #6ec754;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-indicator[data-enabled="false"] {
|
|
||||||
color: #fff;
|
|
||||||
background: #943e3e;
|
|
||||||
border: 1px solid #c06060;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nudge-indicator[data-supported="false"] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller.dragging {
|
|
||||||
cursor: -webkit-grabbing;
|
|
||||||
cursor: -moz-grabbing;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controller.dragging #controls {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable {
|
|
||||||
cursor: -webkit-grab;
|
|
||||||
cursor: -moz-grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable:active {
|
|
||||||
cursor: -webkit-grabbing;
|
|
||||||
cursor: -moz-grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
color: black;
|
|
||||||
background: white;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 3px 6px 3px 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
border: 1px solid white;
|
|
||||||
font-family: "Lucida Console", Monaco, monospace;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
background: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.rw {
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.hideButton {
|
|
||||||
opacity: 0.65;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import { applyJSDOMWindow } from "./jsdom-globals.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function readRepoFile(relPath) {
|
||||||
|
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse HTML into a fresh JSDOM document so tests can reload scripts without
|
||||||
|
* top-level `const` redeclaration errors (avoids document.write).
|
||||||
|
*/
|
||||||
|
export function loadHtmlString(html) {
|
||||||
|
const dom = new JSDOM(html, {
|
||||||
|
url: "https://example.org/",
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
runScripts: "dangerously"
|
||||||
|
});
|
||||||
|
applyJSDOMWindow(dom.window);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadHtml(relPath) {
|
||||||
|
loadHtmlString(readRepoFile(relPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOW_GLOBAL_SKIP = new Set([
|
||||||
|
"alert",
|
||||||
|
"atob",
|
||||||
|
"blur",
|
||||||
|
"btoa",
|
||||||
|
"cancelAnimationFrame",
|
||||||
|
"captureEvents",
|
||||||
|
"clearInterval",
|
||||||
|
"clearTimeout",
|
||||||
|
"close",
|
||||||
|
"confirm",
|
||||||
|
"fetch",
|
||||||
|
"focus",
|
||||||
|
"getComputedStyle",
|
||||||
|
"matchMedia",
|
||||||
|
"open",
|
||||||
|
"prompt",
|
||||||
|
"queueMicrotask",
|
||||||
|
"releaseEvents",
|
||||||
|
"requestAnimationFrame",
|
||||||
|
"setInterval",
|
||||||
|
"setTimeout",
|
||||||
|
"stop"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function mirrorExtensionGlobalsFromWindow(win) {
|
||||||
|
if (!win) return;
|
||||||
|
if (win.tc) {
|
||||||
|
globalThis.tc = win.tc;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(win)) {
|
||||||
|
if (WINDOW_GLOBAL_SKIP.has(key)) continue;
|
||||||
|
if (/^[A-Z]/.test(key)) continue;
|
||||||
|
const val = win[key];
|
||||||
|
if (typeof val === "function") {
|
||||||
|
globalThis[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadScript(relPath) {
|
||||||
|
const source =
|
||||||
|
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||||
|
readRepoFile(relPath) +
|
||||||
|
"\n//# sourceURL=" +
|
||||||
|
relPath;
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.textContent = source;
|
||||||
|
document.head.appendChild(el);
|
||||||
|
mirrorExtensionGlobalsFromWindow(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushAsyncWork() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerDomContentLoaded() {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new window.Event("DOMContentLoaded", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEvent() {
|
||||||
|
const listeners = [];
|
||||||
|
return {
|
||||||
|
addListener(listener) {
|
||||||
|
listeners.push(listener);
|
||||||
|
},
|
||||||
|
trigger(...args) {
|
||||||
|
listeners.forEach((listener) => listener(...args));
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorageArea(initialState = {}) {
|
||||||
|
const state = { ...initialState };
|
||||||
|
|
||||||
|
function resolveGet(keys) {
|
||||||
|
if (keys == null) return { ...state };
|
||||||
|
if (Array.isArray(keys)) {
|
||||||
|
return keys.reduce((acc, key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
||||||
|
acc[key] = state[key];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
if (typeof keys === "string") {
|
||||||
|
return Object.prototype.hasOwnProperty.call(state, keys)
|
||||||
|
? { [keys]: state[keys] }
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
if (typeof keys === "object") {
|
||||||
|
const result = { ...keys };
|
||||||
|
Object.keys(state).forEach((key) => {
|
||||||
|
result[key] = state[key];
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__state: state,
|
||||||
|
get: vi.fn((keys, callback) => {
|
||||||
|
callback(resolveGet(keys));
|
||||||
|
}),
|
||||||
|
set: vi.fn((items, callback) => {
|
||||||
|
Object.assign(state, items);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
remove: vi.fn((keys, callback) => {
|
||||||
|
const list = Array.isArray(keys) ? keys : [keys];
|
||||||
|
list.forEach((key) => {
|
||||||
|
delete state[key];
|
||||||
|
});
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
clear: vi.fn((callback) => {
|
||||||
|
Object.keys(state).forEach((key) => delete state[key]);
|
||||||
|
if (callback) callback();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChromeMock(options = {}) {
|
||||||
|
const syncArea = createStorageArea(options.sync ?? {});
|
||||||
|
const localArea = createStorageArea(options.local ?? {});
|
||||||
|
const tabsOnActivated = createEvent();
|
||||||
|
const tabsOnUpdated = createEvent();
|
||||||
|
const storageOnChanged = createEvent();
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
runtime: {
|
||||||
|
lastError: null,
|
||||||
|
getManifest: vi.fn(() => ({
|
||||||
|
version: options.manifestVersion || "9.9.9"
|
||||||
|
})),
|
||||||
|
getURL: vi.fn((url) => "moz-extension://speeder/" + url)
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
sync: syncArea,
|
||||||
|
local: localArea,
|
||||||
|
onChanged: storageOnChanged
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: vi.fn((queryInfo, callback) => {
|
||||||
|
callback(
|
||||||
|
options.tabs ??
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
active: true,
|
||||||
|
url: "https://example.com/watch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
sendMessage: vi.fn((tabId, message, callback) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(options.sendMessageResponse ?? { speed: 1.25 });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
executeScript: vi.fn((tabId, details, callback) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(
|
||||||
|
options.executeScriptResponse ?? [
|
||||||
|
{ speed: 1.25, preferred: true }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
create: vi.fn(),
|
||||||
|
onActivated: tabsOnActivated,
|
||||||
|
onUpdated: tabsOnUpdated
|
||||||
|
},
|
||||||
|
browserAction: {
|
||||||
|
setIcon: vi.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { JSDOM } = require("jsdom");
|
||||||
|
const vi = globalThis.vi;
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspacePath(relPath) {
|
||||||
|
return path.join(ROOT, relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readWorkspaceFile(relPath) {
|
||||||
|
return fs.readFileSync(workspacePath(relPath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHtmlFile(relPath) {
|
||||||
|
loadHtmlString(readWorkspaceFile(relPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyJSDOMWindow(win) {
|
||||||
|
globalThis.window = win;
|
||||||
|
globalThis.document = win.document;
|
||||||
|
globalThis.navigator = win.navigator;
|
||||||
|
globalThis.customElements = win.customElements;
|
||||||
|
globalThis.HTMLElement = win.HTMLElement;
|
||||||
|
globalThis.Element = win.Element;
|
||||||
|
globalThis.Node = win.Node;
|
||||||
|
globalThis.Text = win.Text;
|
||||||
|
globalThis.DocumentFragment = win.DocumentFragment;
|
||||||
|
globalThis.Event = win.Event;
|
||||||
|
globalThis.MouseEvent = win.MouseEvent;
|
||||||
|
globalThis.KeyboardEvent = win.KeyboardEvent;
|
||||||
|
globalThis.DOMParser = win.DOMParser;
|
||||||
|
globalThis.URL = win.URL;
|
||||||
|
globalThis.Blob = win.Blob;
|
||||||
|
globalThis.FileReader = win.FileReader;
|
||||||
|
win.Date = globalThis.Date;
|
||||||
|
win.open = vi.fn();
|
||||||
|
win.close = vi.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHtmlString(html) {
|
||||||
|
const dom = new JSDOM(html, {
|
||||||
|
url: "https://example.org/",
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
runScripts: "dangerously"
|
||||||
|
});
|
||||||
|
applyJSDOMWindow(dom.window);
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOW_GLOBAL_SKIP = new Set([
|
||||||
|
"alert",
|
||||||
|
"atob",
|
||||||
|
"blur",
|
||||||
|
"btoa",
|
||||||
|
"cancelAnimationFrame",
|
||||||
|
"captureEvents",
|
||||||
|
"clearInterval",
|
||||||
|
"clearTimeout",
|
||||||
|
"close",
|
||||||
|
"confirm",
|
||||||
|
"fetch",
|
||||||
|
"focus",
|
||||||
|
"getComputedStyle",
|
||||||
|
"matchMedia",
|
||||||
|
"open",
|
||||||
|
"prompt",
|
||||||
|
"queueMicrotask",
|
||||||
|
"releaseEvents",
|
||||||
|
"requestAnimationFrame",
|
||||||
|
"setInterval",
|
||||||
|
"setTimeout",
|
||||||
|
"stop"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function mirrorExtensionGlobalsFromWindow(win) {
|
||||||
|
if (!win) return;
|
||||||
|
if (win.tc) {
|
||||||
|
globalThis.tc = win.tc;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(win)) {
|
||||||
|
if (WINDOW_GLOBAL_SKIP.has(key)) continue;
|
||||||
|
if (/^[A-Z]/.test(key)) continue;
|
||||||
|
const val = win[key];
|
||||||
|
if (typeof val === "function") {
|
||||||
|
globalThis[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateScript(relPath) {
|
||||||
|
const absPath = workspacePath(relPath);
|
||||||
|
const source =
|
||||||
|
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||||
|
readWorkspaceFile(relPath) +
|
||||||
|
`\n//# sourceURL=${absPath.replace(/\\/g, "/")}`;
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.textContent = source;
|
||||||
|
document.head.appendChild(el);
|
||||||
|
mirrorExtensionGlobalsFromWindow(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireDOMContentLoaded() {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new window.Event("DOMContentLoaded", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushAsyncWork(turns) {
|
||||||
|
const count = turns || 2;
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
if (vi && typeof vi.isFakeTimers === "function" && vi.isFakeTimers()) {
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
} else {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStorageValues(data, keys) {
|
||||||
|
if (keys == null) return clone(data || {});
|
||||||
|
|
||||||
|
if (typeof keys === "string") {
|
||||||
|
return { [keys]: clone(data ? data[keys] : undefined) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(keys)) {
|
||||||
|
const result = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
result[key] = clone(data ? data[key] : undefined);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof keys === "object") {
|
||||||
|
const result = clone(keys) || {};
|
||||||
|
Object.keys(keys).forEach((key) => {
|
||||||
|
if (data && Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
result[key] = clone(data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChromeEvent() {
|
||||||
|
const listeners = [];
|
||||||
|
return {
|
||||||
|
addListener(listener) {
|
||||||
|
listeners.push(listener);
|
||||||
|
},
|
||||||
|
removeListener(listener) {
|
||||||
|
const index = listeners.indexOf(listener);
|
||||||
|
if (index >= 0) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasListener(listener) {
|
||||||
|
return listeners.includes(listener);
|
||||||
|
},
|
||||||
|
emit(...args) {
|
||||||
|
listeners.slice().forEach((listener) => listener(...args));
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorageArea(areaName, initialData, onChangedEvent) {
|
||||||
|
let data = clone(initialData) || {};
|
||||||
|
|
||||||
|
function emitChanges(changes) {
|
||||||
|
if (changes && Object.keys(changes).length > 0) {
|
||||||
|
onChangedEvent.emit(changes, areaName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: vi.fn((keys, callback) => {
|
||||||
|
if (callback) callback(pickStorageValues(data, keys));
|
||||||
|
}),
|
||||||
|
set: vi.fn((items, callback) => {
|
||||||
|
const nextItems = items || {};
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
Object.keys(nextItems).forEach((key) => {
|
||||||
|
const oldValue = clone(data[key]);
|
||||||
|
const newValue = clone(nextItems[key]);
|
||||||
|
data[key] = newValue;
|
||||||
|
changes[key] = { oldValue, newValue };
|
||||||
|
});
|
||||||
|
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
remove: vi.fn((keys, callback) => {
|
||||||
|
const list = Array.isArray(keys) ? keys : [keys];
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
list.forEach((key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
changes[key] = {
|
||||||
|
oldValue: clone(data[key]),
|
||||||
|
newValue: undefined
|
||||||
|
};
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
clear: vi.fn((callback) => {
|
||||||
|
const changes = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
changes[key] = {
|
||||||
|
oldValue: clone(data[key]),
|
||||||
|
newValue: undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
data = {};
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
_dump() {
|
||||||
|
return clone(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChromeMock(options) {
|
||||||
|
const config = options || {};
|
||||||
|
const storageOnChanged = createChromeEvent();
|
||||||
|
const tabsOnActivated = createChromeEvent();
|
||||||
|
const tabsOnUpdated = createChromeEvent();
|
||||||
|
const runtimeOnMessage = createChromeEvent();
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
runtime: {
|
||||||
|
lastError: null,
|
||||||
|
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||||
|
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
||||||
|
onMessage: runtimeOnMessage
|
||||||
|
},
|
||||||
|
browserAction: {
|
||||||
|
setIcon: vi.fn()
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: vi.fn((queryInfo, callback) => {
|
||||||
|
const tabs = clone(config.tabsQueryResult) || [
|
||||||
|
{ id: 1, active: true, url: "https://example.com/" }
|
||||||
|
];
|
||||||
|
if (callback) callback(tabs);
|
||||||
|
}),
|
||||||
|
sendMessage: vi.fn((tabId, message, callback) => {
|
||||||
|
if (callback) callback(null);
|
||||||
|
}),
|
||||||
|
executeScript: vi.fn((tabId, details, callback) => {
|
||||||
|
if (callback) callback([]);
|
||||||
|
}),
|
||||||
|
create: vi.fn(),
|
||||||
|
onActivated: tabsOnActivated,
|
||||||
|
onUpdated: tabsOnUpdated
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
onChanged: storageOnChanged,
|
||||||
|
sync: null,
|
||||||
|
local: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.storage.sync = createStorageArea(
|
||||||
|
"sync",
|
||||||
|
config.syncData,
|
||||||
|
storageOnChanged
|
||||||
|
);
|
||||||
|
chrome.storage.local = createStorageArea(
|
||||||
|
"local",
|
||||||
|
config.localData,
|
||||||
|
storageOnChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installCommonWindowMocks() {
|
||||||
|
window.open = vi.fn();
|
||||||
|
window.close = vi.fn();
|
||||||
|
window.requestAnimationFrame = vi.fn((callback) => setTimeout(callback, 0));
|
||||||
|
window.cancelAnimationFrame = vi.fn((id) => clearTimeout(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile,
|
||||||
|
loadHtmlString,
|
||||||
|
readWorkspaceFile,
|
||||||
|
workspacePath
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Point Vitest/jsdom test globals at a new JSDOM window (no document.write).
|
||||||
|
* Call after creating `new JSDOM(html, options).window`.
|
||||||
|
*/
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
export function applyJSDOMWindow(win) {
|
||||||
|
globalThis.window = win;
|
||||||
|
globalThis.document = win.document;
|
||||||
|
globalThis.navigator = win.navigator;
|
||||||
|
globalThis.customElements = win.customElements;
|
||||||
|
globalThis.HTMLElement = win.HTMLElement;
|
||||||
|
globalThis.Element = win.Element;
|
||||||
|
globalThis.Node = win.Node;
|
||||||
|
globalThis.Text = win.Text;
|
||||||
|
globalThis.DocumentFragment = win.DocumentFragment;
|
||||||
|
globalThis.Event = win.Event;
|
||||||
|
globalThis.MouseEvent = win.MouseEvent;
|
||||||
|
globalThis.KeyboardEvent = win.KeyboardEvent;
|
||||||
|
globalThis.DOMParser = win.DOMParser;
|
||||||
|
globalThis.URL = win.URL;
|
||||||
|
globalThis.Blob = win.Blob;
|
||||||
|
globalThis.FileReader = win.FileReader;
|
||||||
|
|
||||||
|
// Vitest fake timers patch host `Date`; jsdom’s window keeps its own otherwise.
|
||||||
|
win.Date = globalThis.Date;
|
||||||
|
|
||||||
|
win.open = vi.fn();
|
||||||
|
win.close = vi.fn();
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupImportExport(overrides = {}) {
|
||||||
|
loadHtml("extension/options/options.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
const restoreSpy = vi.fn();
|
||||||
|
globalThis.restore_options = restoreSpy;
|
||||||
|
window.restore_options = restoreSpy;
|
||||||
|
loadScript("extension/shared/import-export.js");
|
||||||
|
loadScript("extension/options/import-export.js");
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("import/export flows", () => {
|
||||||
|
it("exports sync and local settings as a JSON download", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
sync: { rememberSpeed: true },
|
||||||
|
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
const OriginalBlob = window.Blob;
|
||||||
|
class TestBlob {
|
||||||
|
constructor(parts, options) {
|
||||||
|
this.parts = parts;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text() {
|
||||||
|
return this.parts.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.Blob = TestBlob;
|
||||||
|
window.Blob = TestBlob;
|
||||||
|
|
||||||
|
let capturedBlob = null;
|
||||||
|
let clickedDownload = null;
|
||||||
|
Object.defineProperty(window.URL, "createObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn((blob) => {
|
||||||
|
capturedBlob = blob;
|
||||||
|
return "blob:test";
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn(() => {})
|
||||||
|
});
|
||||||
|
vi.spyOn(window.HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||||
|
function () {
|
||||||
|
clickedDownload = this.download;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById("exportSettings").click();
|
||||||
|
|
||||||
|
expect(clickedDownload).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||||
|
expect(capturedBlob).not.toBeNull();
|
||||||
|
const blobText = await capturedBlob.text();
|
||||||
|
expect(JSON.parse(blobText)).toEqual({
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: "2026-04-04T12:09:10.000Z",
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Settings exported successfully"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.get).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.get).toHaveBeenCalled();
|
||||||
|
globalThis.Blob = OriginalBlob;
|
||||||
|
window.Blob = OriginalBlob;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("export strips lucideTagsCacheV1 from localSettings", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
||||||
|
await setupImportExport({
|
||||||
|
sync: { rememberSpeed: true },
|
||||||
|
local: {
|
||||||
|
customButtonIcons: { faster: { slug: "rocket", svg: "<svg/>" } },
|
||||||
|
lucideTagsCacheV1: { "a-arrow-down": ["letter"] },
|
||||||
|
lucideTagsCacheV1At: 42
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const OriginalBlob = window.Blob;
|
||||||
|
class TestBlob {
|
||||||
|
constructor(parts) {
|
||||||
|
this.parts = parts;
|
||||||
|
}
|
||||||
|
async text() {
|
||||||
|
return this.parts.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.Blob = TestBlob;
|
||||||
|
window.Blob = TestBlob;
|
||||||
|
let capturedBlob = null;
|
||||||
|
Object.defineProperty(window.URL, "createObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn((blob) => {
|
||||||
|
capturedBlob = blob;
|
||||||
|
return "blob:test";
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn(() => {})
|
||||||
|
});
|
||||||
|
vi.spyOn(window.HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById("exportSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(JSON.parse(await capturedBlob.text()).localSettings).toEqual({
|
||||||
|
customButtonIcons: { faster: { slug: "rocket", svg: "<svg/>" } }
|
||||||
|
});
|
||||||
|
globalThis.Blob = OriginalBlob;
|
||||||
|
window.Blob = OriginalBlob;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports wrapped backup payloads and refreshes options", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport();
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.FileReader = MockFileReader;
|
||||||
|
window.FileReader = MockFileReader;
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||||
|
{ customButtonIcons: { faster: { slug: "rocket" } } },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Settings imported successfully. Reloading..."
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(globalThis.restore_options).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports raw settings objects without touching local storage", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.FileReader = MockFileReader;
|
||||||
|
window.FileReader = MockFileReader;
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.clear).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
},
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears stale local data when a wrapped backup has empty local settings", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
local: {
|
||||||
|
customButtonIcons: { faster: { slug: "rocket" } },
|
||||||
|
lucideTagsCacheV1: { stale: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.FileReader = MockFileReader;
|
||||||
|
window.FileReader = MockFileReader;
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error for invalid backup files", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport();
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.FileReader = MockFileReader;
|
||||||
|
window.FileReader = MockFileReader;
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({ wat: true })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Error: Invalid backup file format"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
flushAsyncWork,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootImportExport(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlString(`<!doctype html><html><body>
|
||||||
|
<button id="exportSettings">Export</button>
|
||||||
|
<button id="importSettings">Import</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
</body></html>`);
|
||||||
|
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
class TestBlob {
|
||||||
|
constructor(parts, options) {
|
||||||
|
this.parts = parts;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text() {
|
||||||
|
return this.parts.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.Blob = TestBlob;
|
||||||
|
window.Blob = TestBlob;
|
||||||
|
|
||||||
|
const createObjectURL = vi.fn(() => "blob:test");
|
||||||
|
const revokeObjectURL = vi.fn();
|
||||||
|
Object.defineProperty(window.URL, "createObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: createObjectURL
|
||||||
|
});
|
||||||
|
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: revokeObjectURL
|
||||||
|
});
|
||||||
|
global.URL = window.URL;
|
||||||
|
|
||||||
|
evaluateScript("extension/shared/import-export.js");
|
||||||
|
evaluateScript("extension/options/import-export.js");
|
||||||
|
fireDOMContentLoaded();
|
||||||
|
return { chrome, createObjectURL, revokeObjectURL };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("options/import-export.js", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates timestamped backup filenames", () => {
|
||||||
|
vi.setSystemTime(new Date("2026-04-04T13:14:15Z"));
|
||||||
|
bootImportExport();
|
||||||
|
|
||||||
|
expect(window.generateBackupFilename()).toBe(
|
||||||
|
"speeder-backup_2026-04-04_09.14.15.json"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exports sync and local settings into a downloadable backup", async () => {
|
||||||
|
Object.defineProperty(window.HTMLAnchorElement.prototype, "click", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn()
|
||||||
|
});
|
||||||
|
const { createObjectURL, revokeObjectURL } = bootImportExport({
|
||||||
|
syncData: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
keyBindings: [{ action: "faster", code: "KeyD", value: 0.1 }]
|
||||||
|
},
|
||||||
|
localData: {
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#exportSettings").click();
|
||||||
|
|
||||||
|
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||||
|
const blob = createObjectURL.mock.calls[0][0];
|
||||||
|
const backup = JSON.parse(await blob.text());
|
||||||
|
|
||||||
|
expect(backup.settings.rememberSpeed).toBe(true);
|
||||||
|
expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("exported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits Lucide tags cache from exported localSettings", async () => {
|
||||||
|
Object.defineProperty(window.HTMLAnchorElement.prototype, "click", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn()
|
||||||
|
});
|
||||||
|
const { createObjectURL } = bootImportExport({
|
||||||
|
syncData: { rememberSpeed: true },
|
||||||
|
localData: {
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
},
|
||||||
|
lucideTagsCacheV1: { "a-arrow-down": ["letter", "text"] },
|
||||||
|
lucideTagsCacheV1At: 999
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#exportSettings").click();
|
||||||
|
|
||||||
|
const blob = createObjectURL.mock.calls[0][0];
|
||||||
|
const backup = JSON.parse(await blob.text());
|
||||||
|
|
||||||
|
expect(backup.localSettings).toEqual({
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(backup.localSettings.lucideTagsCacheV1).toBeUndefined();
|
||||||
|
expect(backup.localSettings.lucideTagsCacheV1At).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports wrapped backups, restores local data, and refreshes the options page", async () => {
|
||||||
|
const { chrome } = bootImportExport();
|
||||||
|
window.restore_options = vi.fn();
|
||||||
|
|
||||||
|
const realCreateElement = document.createElement.bind(document);
|
||||||
|
const fakeInput = realCreateElement("input");
|
||||||
|
Object.defineProperty(fakeInput, "files", {
|
||||||
|
configurable: true,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
__contents: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
localSettings: {
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
fakeInput.click = vi.fn(() => {
|
||||||
|
fakeInput.onchange({ target: fakeInput });
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
if (String(tagName).toLowerCase() === "input") {
|
||||||
|
return fakeInput;
|
||||||
|
}
|
||||||
|
return realCreateElement(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
|
class FakeFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({ target: { result: file.__contents } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal("FileReader", FakeFileReader);
|
||||||
|
window.FileReader = FakeFileReader;
|
||||||
|
|
||||||
|
document.querySelector("#importSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true, enabled: false },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.restore_options).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error for malformed backups", async () => {
|
||||||
|
bootImportExport();
|
||||||
|
|
||||||
|
const realCreateElement = document.createElement.bind(document);
|
||||||
|
const fakeInput = realCreateElement("input");
|
||||||
|
Object.defineProperty(fakeInput, "files", {
|
||||||
|
configurable: true,
|
||||||
|
value: [{ __contents: "{bad json" }]
|
||||||
|
});
|
||||||
|
fakeInput.click = vi.fn(() => {
|
||||||
|
fakeInput.onchange({ target: fakeInput });
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
if (String(tagName).toLowerCase() === "input") {
|
||||||
|
return fakeInput;
|
||||||
|
}
|
||||||
|
return realCreateElement(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
|
class FakeFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({ target: { result: file.__contents } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal("FileReader", FakeFileReader);
|
||||||
|
window.FileReader = FakeFileReader;
|
||||||
|
|
||||||
|
document.querySelector("#importSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"Failed to parse backup file"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootInject(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
window.requestIdleCallback = (callback, opts) =>
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
callback({
|
||||||
|
didTimeout: false,
|
||||||
|
timeRemaining() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(opts && opts.timeout) || 0
|
||||||
|
);
|
||||||
|
window.cancelIdleCallback = (id) => clearTimeout(id);
|
||||||
|
|
||||||
|
evaluateScript("extension/shared/controller-utils.js");
|
||||||
|
evaluateScript("extension/shared/key-bindings.js");
|
||||||
|
evaluateScript("extension/shared/site-rules.js");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/content/inject.js");
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("inject.js helper logic", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes bindings from legacy formats", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
window.normalizeStoredBinding({
|
||||||
|
action: "faster",
|
||||||
|
key: "g",
|
||||||
|
value: 1.8,
|
||||||
|
force: false
|
||||||
|
}).code
|
||||||
|
).toBe("KeyG");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
window.normalizeStoredBinding({
|
||||||
|
action: "pause",
|
||||||
|
code: null,
|
||||||
|
key: null,
|
||||||
|
keyCode: null,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
action: "pause",
|
||||||
|
code: null,
|
||||||
|
disabled: true,
|
||||||
|
value: 0,
|
||||||
|
force: "false",
|
||||||
|
predefined: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.defaultKeyBindings({ speedStep: 0.25, rewindTime: 5 })[0]).toEqual(
|
||||||
|
{
|
||||||
|
action: "slower",
|
||||||
|
code: "KeyS",
|
||||||
|
value: 0.25,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps controller margins and ignores stale source-specific target speeds", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(window.normalizeControllerMarginPx(250, 0)).toBe(200);
|
||||||
|
expect(window.normalizeControllerMarginPx(-5, 65)).toBe(0);
|
||||||
|
expect(window.normalizeControllerMarginPx("bad", 65)).toBe(65);
|
||||||
|
|
||||||
|
const staleVideo = {
|
||||||
|
currentSrc: "fresh.mp4",
|
||||||
|
vsc: {
|
||||||
|
targetSpeed: 1.75,
|
||||||
|
targetSpeedSourceKey: "old.mp4"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(window.getControllerTargetSpeed(staleVideo)).toBeNull();
|
||||||
|
|
||||||
|
window.tc.settings.rememberSpeed = true;
|
||||||
|
window.tc.settings.forceLastSavedSpeed = false;
|
||||||
|
window.tc.settings.lastSpeed = 1.3;
|
||||||
|
window.tc.settings.speeds = { "fresh.mp4": 1.6 };
|
||||||
|
|
||||||
|
expect(window.getRememberedSpeed({ currentSrc: "fresh.mp4" })).toBe(1.6);
|
||||||
|
expect(window.getDesiredSpeed(staleVideo)).toBe(1.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies site rule overrides and detects disabled sites", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
window.tc.settings.siteRules = [{ pattern: "example.org", enabled: false }];
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
expect(window.applySiteRuleOverrides()).toBe(true);
|
||||||
|
|
||||||
|
window.resetSettingsFromSiteRuleBase();
|
||||||
|
window.tc.settings.siteRules = [
|
||||||
|
{
|
||||||
|
pattern: "example.org",
|
||||||
|
controllerLocation: "bottom-left",
|
||||||
|
controllerMarginTop: 300,
|
||||||
|
controllerMarginBottom: -10,
|
||||||
|
rememberSpeed: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
|
||||||
|
expect(window.applySiteRuleOverrides()).toBe(false);
|
||||||
|
expect(window.tc.settings.controllerLocation).toBe("bottom-left");
|
||||||
|
expect(window.tc.settings.controllerMarginTop).toBe(200);
|
||||||
|
expect(window.tc.settings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(window.tc.settings.rememberSpeed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sizes and positions the controller host to the video bounds", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
const mount = document.createElement("div");
|
||||||
|
const video = document.createElement("video");
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "vsc-controller";
|
||||||
|
mount.append(video, wrapper);
|
||||||
|
document.body.appendChild(mount);
|
||||||
|
|
||||||
|
Object.defineProperties(mount, {
|
||||||
|
offsetWidth: { value: 400 },
|
||||||
|
offsetHeight: { value: 240 },
|
||||||
|
clientLeft: { value: 2 },
|
||||||
|
clientTop: { value: 2 }
|
||||||
|
});
|
||||||
|
mount.getBoundingClientRect = () => ({
|
||||||
|
left: 100,
|
||||||
|
top: 50,
|
||||||
|
right: 500,
|
||||||
|
bottom: 290,
|
||||||
|
width: 400,
|
||||||
|
height: 240
|
||||||
|
});
|
||||||
|
video.getBoundingClientRect = () => ({
|
||||||
|
left: 140,
|
||||||
|
top: 70,
|
||||||
|
right: 460,
|
||||||
|
bottom: 250,
|
||||||
|
width: 320,
|
||||||
|
height: 180
|
||||||
|
});
|
||||||
|
|
||||||
|
window.positionControllerHost(wrapper, video, mount);
|
||||||
|
|
||||||
|
expect(wrapper.style.getPropertyValue("left")).toBe("38px");
|
||||||
|
expect(wrapper.style.getPropertyValue("top")).toBe("18px");
|
||||||
|
expect(wrapper.style.getPropertyValue("width")).toBe("320px");
|
||||||
|
expect(wrapper.style.getPropertyValue("height")).toBe("180px");
|
||||||
|
expect(wrapper.style.getPropertyPriority("width")).toBe("important");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtmlString,
|
||||||
|
loadScript
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
function loadBlankDocument() {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootInject({ sync = {}, local = {} } = {}) {
|
||||||
|
loadBlankDocument();
|
||||||
|
globalThis.chrome = createChromeMock({ sync, local });
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.chrome.runtime.onMessage = {
|
||||||
|
addListener: vi.fn()
|
||||||
|
};
|
||||||
|
const originalSyncGet = globalThis.chrome.storage.sync.get;
|
||||||
|
const originalLocalGet = globalThis.chrome.storage.local.get;
|
||||||
|
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalSyncGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalLocalGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.requestIdleCallback = (callback, options) =>
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
callback({
|
||||||
|
didTimeout: false,
|
||||||
|
timeRemaining() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(options && options.timeout) || 0
|
||||||
|
);
|
||||||
|
globalThis.cancelIdleCallback = (id) => clearTimeout(id);
|
||||||
|
|
||||||
|
loadScript("extension/shared/controller-utils.js");
|
||||||
|
loadScript("extension/shared/key-bindings.js");
|
||||||
|
loadScript("extension/shared/site-rules.js");
|
||||||
|
loadScript("extension/shared/ui-icons.js");
|
||||||
|
loadScript("extension/content/inject.js");
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
await flushAsyncWork();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("inject runtime", () => {
|
||||||
|
it("treats a matching site rule with site enabled as active when global enable is off", async () => {
|
||||||
|
await bootInject({
|
||||||
|
sync: {
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.org", enabled: true }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.tc.settings.enabled).toBe(false);
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
window.applySiteRuleOverrides();
|
||||||
|
expect(window.tc.activeSiteRule).toEqual(
|
||||||
|
expect.objectContaining({ pattern: "example.org", enabled: true })
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
window.SpeederShared.siteRules.isSpeederActiveForSite(
|
||||||
|
window.tc.settings.enabled,
|
||||||
|
window.tc.activeSiteRule
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps subtitle nudge disabled when the effective setting is off", async () => {
|
||||||
|
await bootInject({
|
||||||
|
sync: {
|
||||||
|
enableSubtitleNudge: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopSubtitleNudge = vi.fn();
|
||||||
|
const startSubtitleNudge = vi.fn();
|
||||||
|
const flashEl = document.createElement("span");
|
||||||
|
const video = {
|
||||||
|
paused: false,
|
||||||
|
playbackRate: 1.5,
|
||||||
|
vsc: {
|
||||||
|
stopSubtitleNudge,
|
||||||
|
startSubtitleNudge,
|
||||||
|
subtitleNudgeEnabledOverride: null,
|
||||||
|
subtitleNudgeIndicator: null,
|
||||||
|
nudgeFlashIndicator: flashEl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(window.tc.settings.enableSubtitleNudge).toBe(false);
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||||
|
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(false);
|
||||||
|
expect(video.vsc.subtitleNudgeEnabledOverride).toBeNull();
|
||||||
|
expect(stopSubtitleNudge).toHaveBeenCalledTimes(1);
|
||||||
|
expect(startSubtitleNudge).not.toHaveBeenCalled();
|
||||||
|
expect(flashEl.classList.contains("visible")).toBe(false);
|
||||||
|
|
||||||
|
window.tc.settings.enableSubtitleNudge = true;
|
||||||
|
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
|
||||||
|
|
||||||
|
window.tc.settings.enableSubtitleNudge = false;
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the configured subtitle nudge default until a video is toggled", async () => {
|
||||||
|
await bootInject({
|
||||||
|
sync: {
|
||||||
|
enableSubtitleNudge: true,
|
||||||
|
subtitleNudgeEnabledByDefault: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const startSubtitleNudge = vi.fn();
|
||||||
|
const video = {
|
||||||
|
paused: false,
|
||||||
|
playbackRate: 1.5,
|
||||||
|
vsc: {
|
||||||
|
startSubtitleNudge,
|
||||||
|
stopSubtitleNudge: vi.fn(),
|
||||||
|
subtitleNudgeEnabledOverride: null,
|
||||||
|
subtitleNudgeIndicator: null,
|
||||||
|
nudgeFlashIndicator: document.createElement("span")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||||
|
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
|
||||||
|
expect(video.vsc.subtitleNudgeEnabledOverride).toBe(true);
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
|
||||||
|
expect(startSubtitleNudge).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies subtitle nudge default state from matching site rules", async () => {
|
||||||
|
await bootInject();
|
||||||
|
|
||||||
|
window.tc.settings.enableSubtitleNudge = true;
|
||||||
|
window.tc.settings.subtitleNudgeEnabledByDefault = true;
|
||||||
|
window.tc.settings.siteRules = [
|
||||||
|
{
|
||||||
|
pattern: "example.org",
|
||||||
|
subtitleNudgeEnabledByDefault: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
|
||||||
|
expect(window.applySiteRuleOverrides()).toBe(false);
|
||||||
|
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(false);
|
||||||
|
|
||||||
|
window.resetSettingsFromSiteRuleBase();
|
||||||
|
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects media inside dynamically added shadow DOMs", async () => {
|
||||||
|
await bootInject();
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
expect(window.vscAttachShadowPatched).toBe(true);
|
||||||
|
|
||||||
|
const host = document.createElement("custom-player");
|
||||||
|
const shadow = host.attachShadow({ mode: "open" });
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = "https://example.org/dynamic.mp4";
|
||||||
|
shadow.appendChild(video);
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// Flush MutationObserver microtasks so that the observer callback runs
|
||||||
|
// and schedules requestIdleCallback's setTimeout.
|
||||||
|
await flushAsyncWork();
|
||||||
|
// Run the scheduled timers (requestIdleCallback)
|
||||||
|
vi.runAllTimers();
|
||||||
|
// Flush any remaining microtasks/promises
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(video.vsc).toBeDefined();
|
||||||
|
expect(video.vsc.div).toBeDefined();
|
||||||
|
expect(video.vsc.div.classList.contains("vsc-non-youtube")).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects media in pre-existing shadow DOMs via delayed rescan", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
|
||||||
|
const host = document.createElement("custom-player");
|
||||||
|
const shadow = host.attachShadow({ mode: "open" });
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = "https://example.org/pre-existing.mp4";
|
||||||
|
shadow.appendChild(video);
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
globalThis.chrome = createChromeMock({ sync: {}, local: {} });
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.chrome.runtime.onMessage = {
|
||||||
|
addListener: vi.fn()
|
||||||
|
};
|
||||||
|
const originalSyncGet = globalThis.chrome.storage.sync.get;
|
||||||
|
const originalLocalGet = globalThis.chrome.storage.local.get;
|
||||||
|
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalSyncGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalLocalGet(keys, callback));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadScript("extension/shared/controller-utils.js");
|
||||||
|
loadScript("extension/shared/key-bindings.js");
|
||||||
|
loadScript("extension/shared/site-rules.js");
|
||||||
|
loadScript("extension/shared/ui-icons.js");
|
||||||
|
loadScript("extension/content/inject.js");
|
||||||
|
|
||||||
|
// Fast-forward 3000ms for delayed rescan to trigger
|
||||||
|
vi.advanceTimersByTime(3000);
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
await flushAsyncWork();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(video.vsc).toBeDefined();
|
||||||
|
expect(video.vsc.div).toBeDefined();
|
||||||
|
expect(video.vsc.div.classList.contains("vsc-non-youtube")).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const {
|
||||||
|
evaluateScript,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
describe("lucide-client.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds icon URLs and rejects invalid slugs", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/options/lucide-client.js");
|
||||||
|
|
||||||
|
expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
|
||||||
|
"/icons/alarm-clock.svg"
|
||||||
|
);
|
||||||
|
expect(window.lucideIconSvgUrl("bad slug!!")).toBe("");
|
||||||
|
expect(window.lucideTagsJsonUrl()).toContain("/tags.json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes SVG before persisting a Lucide icon", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/options/lucide-client.js");
|
||||||
|
|
||||||
|
const sanitized = window.sanitizeLucideSvg(`
|
||||||
|
<svg width="10" height="10" onclick="evil()">
|
||||||
|
<script>alert(1)</script>
|
||||||
|
<foreignObject>bad</foreignObject>
|
||||||
|
<path d="M0 0h10v10"></path>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(sanitized).toContain("<svg");
|
||||||
|
expect(sanitized).not.toContain("onclick");
|
||||||
|
expect(sanitized).not.toContain("<script");
|
||||||
|
expect(sanitized).not.toContain("foreignObject");
|
||||||
|
expect(sanitized).toContain('width="100%"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches and ranks icon slugs by query", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/options/lucide-client.js");
|
||||||
|
|
||||||
|
const results = window.searchLucideSlugs(
|
||||||
|
{
|
||||||
|
alarm: ["clock", "time"],
|
||||||
|
"badge-alert": ["alert", "warning"],
|
||||||
|
calendar: ["date", "time"]
|
||||||
|
},
|
||||||
|
"al",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results).toEqual(["alarm", "badge-alert", "calendar"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript,
|
||||||
|
triggerDomContentLoaded
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupOptions(overrides = {}) {
|
||||||
|
loadHtml("extension/options/options.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.fetch = vi.fn();
|
||||||
|
loadScript("extension/shared/controller-utils.js");
|
||||||
|
loadScript("extension/shared/key-bindings.js");
|
||||||
|
loadScript("extension/shared/popup-controls.js");
|
||||||
|
loadScript("extension/shared/ui-icons.js");
|
||||||
|
loadScript("extension/options/lucide-client.js");
|
||||||
|
loadScript("extension/options/options.js");
|
||||||
|
triggerDomContentLoaded();
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("options page", () => {
|
||||||
|
it("restores stored settings, custom shortcuts, and site rules", async () => {
|
||||||
|
await setupOptions({
|
||||||
|
manifestVersion: "5.1.7.0",
|
||||||
|
sync: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
enabled: false,
|
||||||
|
subtitleNudgeEnabledByDefault: false,
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
popupControllerButtons: ["rewind", "settings", "advance", "advance"],
|
||||||
|
keyBindings: [
|
||||||
|
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||||
|
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: true,
|
||||||
|
subtitleNudgeEnabledByDefault: false,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance", "settings", "advance"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("enabled").checked).toBe(false);
|
||||||
|
expect(document.getElementById("subtitleNudgeEnabledByDefault").checked).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(document.querySelector('.shortcut-row[data-action="pause"]')).not.toBe(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
expect(document.getElementById("siteRulesContainer").children.length).toBe(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(document.querySelector(".site-rule .override-subtitleNudge").checked).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document.querySelector(".site-rule .site-subtitleNudgeEnabledByDefault")
|
||||||
|
.checked
|
||||||
|
).toBe(false);
|
||||||
|
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates site rule regexes before saving", async () => {
|
||||||
|
const chrome = await setupOptions();
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
globalThis.createSiteRule(null);
|
||||||
|
const rule = document.querySelector(".site-rule");
|
||||||
|
rule.querySelector(".site-pattern").value = "/(/";
|
||||||
|
|
||||||
|
globalThis.save_options();
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toContain(
|
||||||
|
"Invalid site rule regex"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
|
||||||
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
|
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
|
||||||
|
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||||
|
const toggle = rule.querySelector(".toggle-site-rule");
|
||||||
|
const body = rule.querySelector(".site-rule-body");
|
||||||
|
|
||||||
|
expect(rule.classList.contains("collapsed")).toBe(true);
|
||||||
|
expect(body.style.display).toBe("none");
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
expect(toggle.getAttribute("aria-label")).toBe("Expand site rule");
|
||||||
|
expect(toggle.querySelector("svg")).not.toBeNull();
|
||||||
|
|
||||||
|
globalThis.setSiteRuleExpandedState(rule, true);
|
||||||
|
|
||||||
|
expect(rule.classList.contains("collapsed")).toBe(false);
|
||||||
|
expect(body.style.display).toBe("block");
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||||
|
expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("site rule shortcut override shows no rows by default and adds via selector", async () => {
|
||||||
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
|
globalThis.createSiteRule({ pattern: "example.com" });
|
||||||
|
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||||
|
const rows = rule.querySelector(".site-shortcuts-rows");
|
||||||
|
const selector = rule.querySelector(".site-add-shortcut-selector");
|
||||||
|
|
||||||
|
expect(rows.querySelectorAll(".shortcut-row").length).toBe(0);
|
||||||
|
expect(selector).not.toBeNull();
|
||||||
|
expect(selector.disabled).toBe(true);
|
||||||
|
|
||||||
|
rule.querySelector(".override-shortcuts").checked = true;
|
||||||
|
rule.querySelector(".override-shortcuts").dispatchEvent(
|
||||||
|
new Event("change", { bubbles: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selector.disabled).toBe(false);
|
||||||
|
expect(selector.options.length).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
selector.value = "pause";
|
||||||
|
selector.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(rows.querySelectorAll('.shortcut-row[data-action="pause"]').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps site override settings visible but disabled until enabled", async () => {
|
||||||
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
|
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
|
||||||
|
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||||
|
const playbackOverride = rule.querySelector(".override-playback");
|
||||||
|
const playbackContainer = rule.querySelector(".site-playback-container");
|
||||||
|
const rememberSpeed = rule.querySelector(".site-rememberSpeed");
|
||||||
|
|
||||||
|
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(rememberSpeed.disabled).toBe(true);
|
||||||
|
|
||||||
|
playbackOverride.checked = true;
|
||||||
|
playbackOverride.dispatchEvent(
|
||||||
|
new Event("change", {
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(rememberSpeed.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves normalized settings and sanitized popup/site-rule controls", async () => {
|
||||||
|
const chrome = await setupOptions();
|
||||||
|
|
||||||
|
document.getElementById("rememberSpeed").checked = true;
|
||||||
|
document.getElementById("hideWithControlsTimer").value = "20";
|
||||||
|
document.getElementById("controllerOpacity").value = "0";
|
||||||
|
document.getElementById("controllerMarginTop").value = "250";
|
||||||
|
document.getElementById("controllerMarginBottom").value = "-4";
|
||||||
|
document.getElementById("enableSubtitleNudge").checked = true;
|
||||||
|
document.getElementById("subtitleNudgeEnabledByDefault").checked = false;
|
||||||
|
document.getElementById("subtitleNudgeInterval").value = "5";
|
||||||
|
document.getElementById("popupMatchHoverControls").checked = false;
|
||||||
|
document.getElementById("showPopupControlBar").checked = false;
|
||||||
|
|
||||||
|
globalThis.populatePopupControlBarEditor([
|
||||||
|
"rewind",
|
||||||
|
"settings",
|
||||||
|
"faster",
|
||||||
|
"faster"
|
||||||
|
]);
|
||||||
|
|
||||||
|
globalThis.createSiteRule(null);
|
||||||
|
const rule = document.querySelector(".site-rule");
|
||||||
|
rule.querySelector(".site-pattern").value = "youtube.com";
|
||||||
|
rule.querySelector(".override-playback").checked = true;
|
||||||
|
rule.querySelector(".site-rememberSpeed").checked = true;
|
||||||
|
rule.querySelector(".override-opacity").checked = true;
|
||||||
|
rule.querySelector(".site-controllerOpacity").value = "0";
|
||||||
|
rule.querySelector(".override-subtitleNudge").checked = true;
|
||||||
|
rule.querySelector(".site-enableSubtitleNudge").checked = true;
|
||||||
|
rule.querySelector(".site-subtitleNudgeEnabledByDefault").checked = false;
|
||||||
|
rule.querySelector(".site-subtitleNudgeInterval").value = "75";
|
||||||
|
rule.querySelector(".override-popup-controlbar").checked = true;
|
||||||
|
rule.querySelector(".site-showPopupControlBar").checked = false;
|
||||||
|
globalThis.populateControlBarZones(
|
||||||
|
rule.querySelector(".site-popup-cb-active"),
|
||||||
|
rule.querySelector(".site-popup-cb-available"),
|
||||||
|
["advance", "settings", "advance"],
|
||||||
|
function (id) {
|
||||||
|
return id !== "settings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.save_options();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.remove).toHaveBeenCalled();
|
||||||
|
const savedSettings =
|
||||||
|
chrome.storage.sync.set.mock.calls[
|
||||||
|
chrome.storage.sync.set.mock.calls.length - 1
|
||||||
|
][0];
|
||||||
|
|
||||||
|
expect(savedSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||||
|
expect(savedSettings.controllerOpacity).toBe(0);
|
||||||
|
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||||
|
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(savedSettings.subtitleNudgeEnabledByDefault).toBe(false);
|
||||||
|
expect(savedSettings.subtitleNudgeInterval).toBe(10);
|
||||||
|
expect(savedSettings.showPopupControlBar).toBe(false);
|
||||||
|
expect(savedSettings.popupMatchHoverControls).toBe(false);
|
||||||
|
expect(savedSettings.popupControllerButtons).toEqual(["rewind", "faster"]);
|
||||||
|
expect(savedSettings.siteRules).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
pattern: "youtube.com",
|
||||||
|
rememberSpeed: true,
|
||||||
|
controllerOpacity: 0,
|
||||||
|
enableSubtitleNudge: true,
|
||||||
|
subtitleNudgeEnabledByDefault: false,
|
||||||
|
subtitleNudgeInterval: 75,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance"]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootOptions(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlFile("extension/options/options.html");
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
manifest: { version: "5.1.7.0" },
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
text: () => Promise.resolve("<svg></svg>")
|
||||||
|
})
|
||||||
|
);
|
||||||
|
window.fetch = global.fetch;
|
||||||
|
|
||||||
|
evaluateScript("extension/shared/controller-utils.js");
|
||||||
|
evaluateScript("extension/shared/key-bindings.js");
|
||||||
|
evaluateScript("extension/shared/popup-controls.js");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/options/lucide-client.js");
|
||||||
|
evaluateScript("extension/options/options.js");
|
||||||
|
fireDOMContentLoaded();
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("options.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
delete global.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores saved settings, bindings, site rules, and popup bar order", async () => {
|
||||||
|
bootOptions({
|
||||||
|
syncData: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
forceLastSavedSpeed: true,
|
||||||
|
controllerLocation: "middle-right",
|
||||||
|
controllerOpacity: 0.75,
|
||||||
|
controllerMarginTop: 22,
|
||||||
|
controllerMarginBottom: 14,
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
controllerButtons: ["rewind", "fast", "display"],
|
||||||
|
popupControllerButtons: ["advance", "settings", "rewind", "advance"],
|
||||||
|
keyBindings: [
|
||||||
|
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||||
|
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: false,
|
||||||
|
controllerMarginTop: 12,
|
||||||
|
popupControllerButtons: ["advance", "settings", "rewind"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("forceLastSavedSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("controllerLocation").value).toBe(
|
||||||
|
"middle-right"
|
||||||
|
);
|
||||||
|
expect(document.getElementById("controllerOpacity").value).toBe("0.75");
|
||||||
|
expect(document.getElementById("controllerMarginTop").value).toBe("22");
|
||||||
|
expect(document.getElementById("popupMatchHoverControls").checked).toBe(false);
|
||||||
|
expect(
|
||||||
|
document.getElementById("popupCbEditorWrap").classList.contains(
|
||||||
|
"cb-editor-disabled"
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
const popupButtons = Array.from(
|
||||||
|
document.querySelectorAll("#popupControlBarActive .cb-block")
|
||||||
|
).map((block) => block.dataset.buttonId);
|
||||||
|
expect(popupButtons).toEqual(["advance", "rewind"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('.shortcut-row.customs[data-action="pause"]')
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(document.querySelectorAll(".site-rule")).toHaveLength(1);
|
||||||
|
expect(document.querySelector(".site-rule .site-enabled").checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves normalized settings and site rule overrides", async () => {
|
||||||
|
const chrome = bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
document.getElementById("rememberSpeed").checked = true;
|
||||||
|
document.getElementById("hideWithControlsTimer").value = "99";
|
||||||
|
document.getElementById("controllerLocation").value = "bottom-left";
|
||||||
|
document.getElementById("controllerOpacity").value = "0.65";
|
||||||
|
document.getElementById("controllerMarginTop").value = "250";
|
||||||
|
document.getElementById("controllerMarginBottom").value = "-5";
|
||||||
|
document.getElementById("popupMatchHoverControls").checked = false;
|
||||||
|
document.getElementById("showPopupControlBar").checked = true;
|
||||||
|
|
||||||
|
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
|
||||||
|
|
||||||
|
window.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
const siteRuleEls = document.querySelectorAll(".site-rule");
|
||||||
|
const ruleEl = siteRuleEls[siteRuleEls.length - 1];
|
||||||
|
ruleEl.querySelector(".override-placement").checked = true;
|
||||||
|
ruleEl.querySelector(".site-controllerLocation").value = "top-right";
|
||||||
|
ruleEl.querySelector(".site-controllerMarginTop").value = "300";
|
||||||
|
ruleEl.querySelector(".site-controllerMarginBottom").value = "-10";
|
||||||
|
|
||||||
|
ruleEl.querySelector(".override-autohide").checked = true;
|
||||||
|
ruleEl.querySelector(".site-hideWithControls").checked = true;
|
||||||
|
ruleEl.querySelector(".site-hideWithControlsTimer").value = "0";
|
||||||
|
|
||||||
|
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||||
|
ruleEl.querySelector(".site-showPopupControlBar").checked = false;
|
||||||
|
window.populateControlBarZones(
|
||||||
|
ruleEl.querySelector(".site-popup-cb-active"),
|
||||||
|
ruleEl.querySelector(".site-popup-cb-available"),
|
||||||
|
["advance", "settings", "rewind"],
|
||||||
|
function (id) {
|
||||||
|
return id !== "settings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.save_options();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.remove).toHaveBeenCalledWith(
|
||||||
|
[
|
||||||
|
"resetSpeed",
|
||||||
|
"speedStep",
|
||||||
|
"fastSpeed",
|
||||||
|
"rewindTime",
|
||||||
|
"advanceTime",
|
||||||
|
"resetKeyCode",
|
||||||
|
"slowerKeyCode",
|
||||||
|
"fasterKeyCode",
|
||||||
|
"rewindKeyCode",
|
||||||
|
"advanceKeyCode",
|
||||||
|
"fastKeyCode",
|
||||||
|
"blacklist"
|
||||||
|
],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
const savedSettings = chrome.storage.sync.set.mock.calls.at(-1)[0];
|
||||||
|
expect(savedSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||||
|
expect(savedSettings.controllerLocation).toBe("bottom-left");
|
||||||
|
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||||
|
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
|
||||||
|
expect(savedSettings.siteRules).toHaveLength(3);
|
||||||
|
expect(savedSettings.siteRules).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: true,
|
||||||
|
controllerLocation: "top-right",
|
||||||
|
controllerMarginTop: 200,
|
||||||
|
controllerMarginBottom: 0,
|
||||||
|
hideWithControls: true,
|
||||||
|
hideWithControlsTimer: 0.1,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance", "rewind"]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks save when a site rule regex is invalid", async () => {
|
||||||
|
const chrome = bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
window.createSiteRule({ pattern: "/[abc/" });
|
||||||
|
window.save_options();
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toContain(
|
||||||
|
"Invalid site rule regex"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds shortcuts from the selector and records key input states", async () => {
|
||||||
|
bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
const selector = document.getElementById("addShortcutSelector");
|
||||||
|
selector.value = "pause";
|
||||||
|
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
const row = document.querySelector('.shortcut-row.customs[data-action="pause"]');
|
||||||
|
expect(row).not.toBeNull();
|
||||||
|
|
||||||
|
const keyInput = row.querySelector(".customKey");
|
||||||
|
keyInput.dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "q",
|
||||||
|
code: "KeyQ",
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(keyInput.vscBinding.code).toBe("KeyQ");
|
||||||
|
expect(keyInput.value).toBe("Q");
|
||||||
|
|
||||||
|
keyInput.dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "Escape",
|
||||||
|
code: "Escape",
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(keyInput.vscBinding.disabled).toBe(true);
|
||||||
|
expect(selector.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript,
|
||||||
|
triggerDomContentLoaded
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupPopup(overrides = {}) {
|
||||||
|
loadHtml("extension/popup/popup.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
loadScript("extension/shared/site-rules.js");
|
||||||
|
loadScript("extension/shared/popup-controls.js");
|
||||||
|
loadScript("extension/shared/ui-icons.js");
|
||||||
|
loadScript("extension/popup/popup.js");
|
||||||
|
triggerDomContentLoaded();
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("popup UI", () => {
|
||||||
|
it("renders version, builds controls, and prefers the active frame speed", async () => {
|
||||||
|
await setupPopup({
|
||||||
|
manifestVersion: "5.1.7.0",
|
||||||
|
executeScriptResponse: [
|
||||||
|
{ speed: 1.1, preferred: false },
|
||||||
|
{ speed: 1.75, preferred: true }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("popupSpeed").textContent).toBe("1.75");
|
||||||
|
expect(
|
||||||
|
document.querySelectorAll("#popupControlBar button").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows controls when globally disabled but a whitelist site rule matches", async () => {
|
||||||
|
await setupPopup({
|
||||||
|
sync: {
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: true }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("status").classList.contains("hide")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(document.getElementById("popupControlBar").style.display).not.toBe(
|
||||||
|
"none"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows disabled state for a matching site rule", async () => {
|
||||||
|
await setupPopup({
|
||||||
|
sync: {
|
||||||
|
enabled: true,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Speeder is disabled for this site."
|
||||||
|
);
|
||||||
|
expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
|
||||||
|
expect(document.getElementById("popupControlBar").style.display).toBe(
|
||||||
|
"none"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles enabled state and updates the browser action icons", async () => {
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
chrome.browserAction.setIcon.mockClear();
|
||||||
|
|
||||||
|
document.getElementById("disable").click();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ enabled: false },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.getElementById("enable").classList.contains("hide")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "assets/icons/icon19_disabled.png",
|
||||||
|
38: "assets/icons/icon38_disabled.png",
|
||||||
|
48: "assets/icons/icon48_disabled.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles refresh responses for unsupported and successful pages", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
let response = null;
|
||||||
|
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||||
|
if (message.action === "rescan_page") {
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.25 });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("refresh").click();
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Cannot run on this page."
|
||||||
|
);
|
||||||
|
|
||||||
|
response = { status: "complete" };
|
||||||
|
document.getElementById("refresh").click();
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Scan complete. Closing..."
|
||||||
|
);
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches popup control bar actions back to the active tab", async () => {
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
chrome.tabs.sendMessage.mockClear();
|
||||||
|
chrome.tabs.executeScript.mockClear();
|
||||||
|
|
||||||
|
document.querySelector("#popupControlBar button").click();
|
||||||
|
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "run_action"
|
||||||
|
}),
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.tabs.executeScript).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles force last saved speed and applies it to the active page", async () => {
|
||||||
|
const chrome = await setupPopup({
|
||||||
|
sync: { lastSpeed: 1.8, forceLastSavedSpeed: false }
|
||||||
|
});
|
||||||
|
chrome.tabs.sendMessage.mockClear();
|
||||||
|
|
||||||
|
document.getElementById("forceLastSavedSpeed").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.__state.forceLastSavedSpeed).toBe(true);
|
||||||
|
expect(
|
||||||
|
document.getElementById("forceLastSavedSpeed").getAttribute("aria-pressed")
|
||||||
|
).toBe("true");
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
action: "set_force_last_saved_speed",
|
||||||
|
enabled: true,
|
||||||
|
speed: 1.8
|
||||||
|
},
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootPopup(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlFile("extension/popup/popup.html");
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
manifest: { version: "9.9.9-test" },
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData,
|
||||||
|
tabsQueryResult: [
|
||||||
|
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.executeScript.mockImplementation(
|
||||||
|
config.executeScriptImpl ||
|
||||||
|
((tabId, details, callback) => {
|
||||||
|
if (callback) callback([{ speed: 1.0, preferred: true }]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
chrome.tabs.sendMessage.mockImplementation(
|
||||||
|
config.sendMessageImpl ||
|
||||||
|
((tabId, message, callback) => {
|
||||||
|
if (message.action === "get_speed") {
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.action === "rescan_page") {
|
||||||
|
callback({ status: "complete" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
evaluateScript("extension/shared/site-rules.js");
|
||||||
|
evaluateScript("extension/shared/popup-controls.js");
|
||||||
|
evaluateScript("extension/shared/ui-icons.js");
|
||||||
|
evaluateScript("extension/popup/popup.js");
|
||||||
|
fireDOMContentLoaded();
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("popup.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the popup disabled state when a site rule disables Speeder", async () => {
|
||||||
|
bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: true,
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
id: 10,
|
||||||
|
active: true,
|
||||||
|
url: "https://www.youtube.com/watch?v=abc123"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(document.querySelector("#app-version").textContent).toBe("9.9.9-test");
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"disabled for this site"
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.00");
|
||||||
|
expect(document.querySelector("#popupControlBar").style.display).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds sanitized popup buttons and refreshes speed after an action", async () => {
|
||||||
|
let speedQueryCount = 0;
|
||||||
|
const chrome = bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: true,
|
||||||
|
controllerButtons: ["faster", "settings", "rewind", "faster"],
|
||||||
|
popupMatchHoverControls: true
|
||||||
|
},
|
||||||
|
executeScriptImpl: (tabId, details, callback) => {
|
||||||
|
speedQueryCount += 1;
|
||||||
|
callback(
|
||||||
|
speedQueryCount <= 2
|
||||||
|
? [
|
||||||
|
{ speed: 1.25, preferred: false },
|
||||||
|
{ speed: 1.5, preferred: true }
|
||||||
|
]
|
||||||
|
: [{ speed: 1.75, preferred: true }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||||
|
if (message.action === "run_action") {
|
||||||
|
callback({ speed: 1.75 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
const buttons = Array.from(
|
||||||
|
document.querySelectorAll("#popupControlBar button[data-action]")
|
||||||
|
).map((button) => button.dataset.action);
|
||||||
|
expect(buttons).toEqual(["faster", "rewind"]);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.50");
|
||||||
|
|
||||||
|
document.querySelector('#popupControlBar button[data-action="faster"]').click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
99,
|
||||||
|
{ action: "run_action", actionName: "faster" },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles enablement and closes after a successful refresh", async () => {
|
||||||
|
const chrome = bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
|
||||||
|
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
|
||||||
|
|
||||||
|
document.querySelector("#enable").click();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ enabled: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "assets/icons/icon19.png",
|
||||||
|
38: "assets/icons/icon38.png",
|
||||||
|
48: "assets/icons/icon48.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#refresh").click();
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("Closing");
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.open = vi.fn();
|
||||||
|
window.close = vi.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0);
|
||||||
|
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
delete globalThis.SpeederShared;
|
||||||
|
try {
|
||||||
|
delete globalThis.restore_options;
|
||||||
|
} catch {
|
||||||
|
globalThis.restore_options = undefined;
|
||||||
|
}
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.head.innerHTML = "";
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
}
|
||||||
|
delete globalThis.chrome;
|
||||||
|
});
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import controllerUtils from "../extension/shared/controller-utils.js";
|
||||||
|
import importExportUtils from "../extension/shared/import-export.js";
|
||||||
|
import keyBindingUtils from "../extension/shared/key-bindings.js";
|
||||||
|
import popupControls from "../extension/shared/popup-controls.js";
|
||||||
|
import siteRules from "../extension/shared/site-rules.js";
|
||||||
|
|
||||||
|
describe("shared helpers", () => {
|
||||||
|
it("matches site rules and skips invalid regex patterns", () => {
|
||||||
|
const literalRule = { pattern: "example.com/watch" };
|
||||||
|
const regexRule = { pattern: "/youtube\\.com\\/watch/i" };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://example.com/watch?v=1", [literalRule])
|
||||||
|
).toBe(literalRule);
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://www.youtube.com/watch?v=2", [regexRule])
|
||||||
|
).toBe(regexRule);
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://www.youtube.com/shorts/3", [
|
||||||
|
{ pattern: "/(/" },
|
||||||
|
regexRule
|
||||||
|
])
|
||||||
|
).toBeNull();
|
||||||
|
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines global enabled with matched site rules (whitelist / blacklist)", () => {
|
||||||
|
const allowSite = { pattern: "good.test", enabled: true };
|
||||||
|
const blockSite = { pattern: "bad.test", enabled: false };
|
||||||
|
|
||||||
|
expect(siteRules.isSpeederActiveForSite(true, null)).toBe(true);
|
||||||
|
expect(siteRules.isSpeederActiveForSite(false, null)).toBe(false);
|
||||||
|
|
||||||
|
expect(siteRules.isSpeederActiveForSite(true, blockSite)).toBe(false);
|
||||||
|
expect(siteRules.isSpeederActiveForSite(false, blockSite)).toBe(false);
|
||||||
|
|
||||||
|
expect(siteRules.isSpeederActiveForSite(true, allowSite)).toBe(true);
|
||||||
|
expect(siteRules.isSpeederActiveForSite(false, allowSite)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes and resolves popup button orders", () => {
|
||||||
|
const controllerButtonDefs = {
|
||||||
|
rewind: {},
|
||||||
|
faster: {},
|
||||||
|
advance: {},
|
||||||
|
display: {},
|
||||||
|
settings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.sanitizeButtonOrder(
|
||||||
|
["rewind", "settings", "rewind", "faster", "missing"],
|
||||||
|
controllerButtonDefs,
|
||||||
|
new Set(["settings"])
|
||||||
|
)
|
||||||
|
).toEqual(["rewind", "faster"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.resolvePopupButtons(
|
||||||
|
{
|
||||||
|
popupMatchHoverControls: true,
|
||||||
|
controllerButtons: ["advance", "display"],
|
||||||
|
popupControllerButtons: ["rewind"]
|
||||||
|
},
|
||||||
|
{ controllerButtons: ["faster", "advance"] },
|
||||||
|
{
|
||||||
|
controllerButtonDefs,
|
||||||
|
defaultButtons: ["rewind", "display"],
|
||||||
|
excludedIds: ["settings"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual(["faster", "advance"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.resolvePopupButtons(
|
||||||
|
{
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
popupControllerButtons: ["rewind", "display"]
|
||||||
|
},
|
||||||
|
{ popupControllerButtons: ["advance", "settings", "advance"] },
|
||||||
|
{
|
||||||
|
controllerButtonDefs,
|
||||||
|
defaultButtons: ["rewind", "display"],
|
||||||
|
excludedIds: ["settings"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual(["advance"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes controller locations and margins", () => {
|
||||||
|
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
|
||||||
|
"top-right"
|
||||||
|
);
|
||||||
|
expect(controllerUtils.normalizeControllerLocation("bogus")).toBe(
|
||||||
|
controllerUtils.defaultControllerLocation
|
||||||
|
);
|
||||||
|
expect(controllerUtils.clampControllerMarginPx(300, 65)).toBe(200);
|
||||||
|
expect(controllerUtils.clampControllerMarginPx(-5, 65)).toBe(0);
|
||||||
|
expect(controllerUtils.getNextControllerLocation("top-left")).toBe(
|
||||||
|
"top-center"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("infers key binding codes from legacy formats", () => {
|
||||||
|
expect(keyBindingUtils.normalizeBindingKey("a")).toBe("A");
|
||||||
|
expect(keyBindingUtils.normalizeBindingKey("Esc")).toBe("Escape");
|
||||||
|
expect(keyBindingUtils.legacyBindingKeyToCode(" ")).toBe("Space");
|
||||||
|
expect(keyBindingUtils.legacyKeyCodeToCode(90)).toBe("KeyZ");
|
||||||
|
expect(keyBindingUtils.inferBindingCode({ key: "x" }, null)).toBe("KeyX");
|
||||||
|
expect(keyBindingUtils.inferBindingCode({ keyCode: 107 }, null)).toBe(
|
||||||
|
"NumpadAdd"
|
||||||
|
);
|
||||||
|
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds and parses import/export payloads", () => {
|
||||||
|
expect(
|
||||||
|
importExportUtils.generateBackupFilename(new Date(2026, 3, 4, 8, 9, 10))
|
||||||
|
).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.buildBackupPayload(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
{ customButtonIcons: {} },
|
||||||
|
"2026-04-04T08:09:10Z"
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: "2026-04-04T08:09:10.000Z",
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: true,
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.parseImportText(
|
||||||
|
JSON.stringify({ rememberSpeed: true, keyBindings: [] })
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { rememberSpeed: true, keyBindings: [] },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({
|
||||||
|
subtitleNudgeEnabledByDefault: false
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { subtitleNudgeEnabledByDefault: false },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({ enabled: true })
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { enabled: true },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.filterLocalSettingsForExport({
|
||||||
|
customButtonIcons: { faster: { slug: "zap" } },
|
||||||
|
lucideTagsCacheV1: { "a-arrow-down": ["letter"] },
|
||||||
|
lucideTagsCacheV1At: 123
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
customButtonIcons: { faster: { slug: "zap" } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const { defineConfig } = require("vitest/config");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
clearMocks: true,
|
||||||
|
globals: true,
|
||||||
|
restoreMocks: true,
|
||||||
|
include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
|
||||||
|
setupFiles: ["./tests/setup.js"]
|
||||||
|
}
|
||||||
|
});
|
||||||