mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-10-08 19:05:02 -04:00
fix(extension): harden controls, rules, and speed persistence
This commit is contained in:
29 files changed
+6154
-842
No files matched your search
@@ -7,11 +7,87 @@ function generateBackupFilename() {
|
||||
return importExportUtils.generateBackupFilename(new Date());
|
||||
}
|
||||
|
||||
function normalizedSettingsForExport(rawStorage) {
|
||||
var raw = rawStorage && typeof rawStorage === "object" ? rawStorage : {};
|
||||
var expanded = vscExpandStoredSettings(raw);
|
||||
var normalized = vscBuildStoredSettingsDiff(expanded);
|
||||
|
||||
// Backups should remain readable outside the extension too. Storage stays
|
||||
// sparse, but explicitly include built-in rule titles in the exported v2
|
||||
// patches. Custom rule titles are already part of their normal patches.
|
||||
var titledRulePatches = Array.isArray(normalized.siteRules)
|
||||
? normalized.siteRules.slice()
|
||||
: [];
|
||||
var orderedRulePatches = [];
|
||||
var claimedRulePatches = new Set();
|
||||
vscGetSettingsDefaults().siteRules.forEach(function(defaultRule) {
|
||||
var expandedRule = expanded.siteRules.find(function(rule) {
|
||||
return rule && rule.pattern === defaultRule.pattern;
|
||||
});
|
||||
if (!expandedRule) return;
|
||||
var patch = titledRulePatches.find(function(candidate) {
|
||||
return (
|
||||
candidate &&
|
||||
candidate.pattern === defaultRule.pattern &&
|
||||
!claimedRulePatches.has(candidate)
|
||||
);
|
||||
});
|
||||
if (!patch && typeof expandedRule.title === "string") {
|
||||
patch = { pattern: defaultRule.pattern };
|
||||
titledRulePatches.push(patch);
|
||||
}
|
||||
if (!patch) return;
|
||||
if (typeof expandedRule.title === "string") {
|
||||
patch.title = expandedRule.title;
|
||||
}
|
||||
claimedRulePatches.add(patch);
|
||||
orderedRulePatches.push(patch);
|
||||
});
|
||||
titledRulePatches.forEach(function(patch) {
|
||||
if (!claimedRulePatches.has(patch)) orderedRulePatches.push(patch);
|
||||
});
|
||||
if (orderedRulePatches.length > 0) {
|
||||
normalized.siteRules = orderedRulePatches;
|
||||
normalized.siteRulesFormat = vscGetSiteRulesDiffFormat();
|
||||
}
|
||||
|
||||
// lastSpeed is useful user data, but it is intentionally outside the
|
||||
// managed options diff because content scripts update it at runtime.
|
||||
if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) {
|
||||
if (
|
||||
Number.isFinite(expanded.lastSpeed) &&
|
||||
expanded.lastSpeed >= 0.0625 &&
|
||||
expanded.lastSpeed <= 16
|
||||
) {
|
||||
normalized.lastSpeed = expanded.lastSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function exportSettings() {
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to read settings - " + chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.get(null, function (localStorage) {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to read local extension data - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const backup = importExportUtils.buildBackupPayload(
|
||||
storage,
|
||||
normalizedSettingsForExport(storage),
|
||||
importExportUtils.filterLocalSettingsForExport(localStorage),
|
||||
new Date()
|
||||
);
|
||||
@@ -33,6 +109,70 @@ function exportSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
function persistImportedSyncSettings(currentRaw, importedRaw, callback) {
|
||||
var expanded = vscExpandStoredSettings(importedRaw || {});
|
||||
var mutation = vscBuildManagedStorageMutation(currentRaw || {}, expanded);
|
||||
|
||||
if (
|
||||
importedRaw &&
|
||||
Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed")
|
||||
) {
|
||||
if (
|
||||
Number.isFinite(expanded.lastSpeed) &&
|
||||
expanded.lastSpeed >= 0.0625 &&
|
||||
expanded.lastSpeed <= 16
|
||||
) {
|
||||
mutation.set.lastSpeed = expanded.lastSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
function removeStaleSettings() {
|
||||
if (!mutation.remove.length) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
chrome.storage.sync.remove(mutation.remove, function () {
|
||||
callback(chrome.runtime.lastError || null);
|
||||
});
|
||||
}
|
||||
|
||||
if (!Object.keys(mutation.set).length) {
|
||||
removeStaleSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the replacement first. A failed write must not leave the user with
|
||||
// their old managed settings already deleted.
|
||||
chrome.storage.sync.set(mutation.set, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
callback(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
removeStaleSettings();
|
||||
});
|
||||
}
|
||||
|
||||
function importLocalSettings(localToImport, callback) {
|
||||
if (localToImport === null) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var icons = localToImport.customButtonIcons;
|
||||
if (icons && typeof icons === "object" && !Array.isArray(icons)) {
|
||||
chrome.storage.local.set({ customButtonIcons: icons }, function () {
|
||||
callback(chrome.runtime.lastError || null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Backups only own customButtonIcons. Keep Lucide caches, remembered speed
|
||||
// history, and any future local runtime data private to the current install.
|
||||
chrome.storage.local.remove("customButtonIcons", function () {
|
||||
callback(chrome.runtime.lastError || null);
|
||||
});
|
||||
}
|
||||
|
||||
function importSettings() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
@@ -55,65 +195,53 @@ function importSettings() {
|
||||
var settingsToImport = parsedBackup.settings;
|
||||
var localToImport = parsedBackup.localSettings;
|
||||
|
||||
function importLocalSettings(callback) {
|
||||
if (parsedBackup.isWrappedBackup !== true) {
|
||||
callback();
|
||||
chrome.storage.sync.get(null, function (currentRaw) {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to read current settings - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
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) {
|
||||
persistImportedSyncSettings(
|
||||
currentRaw || {},
|
||||
settingsToImport,
|
||||
function (syncError) {
|
||||
if (syncError) {
|
||||
showStatus(
|
||||
"Error: Failed to save imported settings - " +
|
||||
chrome.runtime.lastError.message,
|
||||
syncError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
showStatus("Settings imported successfully. Reloading...");
|
||||
setTimeout(function () {
|
||||
if (typeof restore_options === "function") {
|
||||
restore_options();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
importLocalSettings(afterLocalImport);
|
||||
var scopedLocalSettings =
|
||||
parsedBackup.isWrappedBackup === true ? localToImport : null;
|
||||
importLocalSettings(scopedLocalSettings, function (localError) {
|
||||
if (localError) {
|
||||
showStatus(
|
||||
"Error: Failed to save local extension data - " +
|
||||
localError.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);
|
||||
}
|
||||
|
||||
@@ -905,8 +905,19 @@ button.lucide-result-tile.lucide-picked {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-rule-identity {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, 0.45fr) minmax(220px, 1fr);
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-title,
|
||||
.site-pattern {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-pattern {
|
||||
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
@@ -1173,6 +1184,10 @@ button.lucide-result-tile.lucide-picked {
|
||||
grid-template-columns: 36px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.site-rule-identity {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.remove-site-rule {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -1198,6 +1213,10 @@ button.lucide-result-tile.lucide-picked {
|
||||
.site-rule-header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.site-rule-identity {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
<script src="../shared/controller-utils.js"></script>
|
||||
<script src="../shared/key-bindings.js"></script>
|
||||
<script src="../shared/settings-core.js"></script>
|
||||
<script src="../shared/popup-controls.js"></script>
|
||||
<script src="../shared/ui-icons.js"></script>
|
||||
<script src="lucide-client.js"></script>
|
||||
@@ -289,9 +290,9 @@
|
||||
<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
|
||||
>Stores speeds you set with Speeder per source so revisiting the
|
||||
same media can restore them. Separate from “Force last
|
||||
saved speed,” which fights players that reset rate.</em
|
||||
>
|
||||
</label>
|
||||
<input id="rememberSpeed" type="checkbox" />
|
||||
@@ -306,6 +307,20 @@
|
||||
</label>
|
||||
<input id="forceLastSavedSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="shortcutTargetMode"
|
||||
>Keyboard shortcut target<br />
|
||||
<em
|
||||
>Closest changes the video nearest the pointer (falling back to
|
||||
the most visible active player). Change all keeps the legacy
|
||||
behavior on pages with multiple players.</em
|
||||
>
|
||||
</label>
|
||||
<select id="shortcutTargetMode">
|
||||
<option value="closest">Closest active video</option>
|
||||
<option value="all">All videos on the page</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Controller</h4>
|
||||
@@ -607,7 +622,9 @@
|
||||
>
|
||||
patterns like <code>/(.+)youtube\.com(\/*)$/gi</code>. Turn on a
|
||||
row only when you want that group to override the general defaults
|
||||
above.
|
||||
above. Regex and plain-path rules test both the literal URL and a
|
||||
safely decoded path, query, and hash, so readable text can match
|
||||
<code>%xx</code>-encoded URLs too.
|
||||
</p>
|
||||
</div>
|
||||
<div id="siteRulesContainer"></div>
|
||||
@@ -626,11 +643,20 @@
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<div class="site-rule-identity">
|
||||
<input
|
||||
type="text"
|
||||
class="site-title"
|
||||
placeholder="Rule title (optional)"
|
||||
aria-label="Site rule title"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="site-pattern"
|
||||
placeholder="e.g., youtube.com or /regex/gi"
|
||||
aria-label="Site rule URL pattern"
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="remove-site-rule">Remove</button>
|
||||
</div>
|
||||
<div class="site-rule-body">
|
||||
@@ -697,6 +723,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span
|
||||
>Override shortcut target for this site<br /><em
|
||||
>Choose whether keyboard and popup control-bar actions
|
||||
affect the closest active video or every video.</em
|
||||
></span
|
||||
>
|
||||
<input type="checkbox" class="override-shortcut-target" />
|
||||
</label>
|
||||
<div class="site-shortcut-target-container">
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Shortcut target:</label>
|
||||
<select class="site-shortcutTargetMode">
|
||||
<option value="closest">Closest active video</option>
|
||||
<option value="all">All videos on the page</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span
|
||||
@@ -767,10 +813,10 @@
|
||||
<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
|
||||
>Stores speeds you set with Speeder per source so
|
||||
revisiting the same media can restore them. Separate
|
||||
from “Force last saved speed,” which fights
|
||||
players that reset rate.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-rememberSpeed" />
|
||||
@@ -784,6 +830,21 @@
|
||||
>
|
||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label
|
||||
>Preferred speed (0.0625–16):<br /><em
|
||||
>Overrides the Preferred speed action for matching
|
||||
URLs. It does not force every video to start at this
|
||||
speed.</em
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
class="site-preferredSpeed"
|
||||
placeholder="1.80"
|
||||
/>
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label
|
||||
>Work on audio:<br /><em
|
||||
|
||||
+310
-107
@@ -162,58 +162,72 @@ function createDefaultBinding(action, code, value) {
|
||||
};
|
||||
}
|
||||
|
||||
var tcDefaults = {
|
||||
speed: 1.0,
|
||||
lastSpeed: 1.0,
|
||||
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: [
|
||||
createDefaultBinding("display", "KeyV", 0),
|
||||
createDefaultBinding("move", "KeyP", 0),
|
||||
createDefaultBinding("slower", "KeyS", 0.1),
|
||||
createDefaultBinding("faster", "KeyD", 0.1),
|
||||
createDefaultBinding("rewind", "KeyZ", 10),
|
||||
createDefaultBinding("advance", "KeyX", 10),
|
||||
createDefaultBinding("reset", "KeyR", 1),
|
||||
createDefaultBinding("fast", "KeyG", 1.8),
|
||||
createDefaultBinding("toggleSubtitleNudge", "KeyN", 0)
|
||||
],
|
||||
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
|
||||
var tcDefaults = vscGetSettingsDefaults();
|
||||
var optionsSyncSettingsLoaded = false;
|
||||
|
||||
function setOptionsSyncSettingsLoaded(loaded) {
|
||||
optionsSyncSettingsLoaded = loaded === true;
|
||||
var saveButton = document.getElementById("save");
|
||||
if (saveButton) {
|
||||
saveButton.disabled = !optionsSyncSettingsLoaded;
|
||||
saveButton.setAttribute(
|
||||
"aria-busy",
|
||||
optionsSyncSettingsLoaded ? "false" : "true"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function persistManagedSyncSettings(settings, callback, existingStorage) {
|
||||
function persistFromRaw(rawStorage) {
|
||||
// Preserve canonical values that this version of the Options UI does not
|
||||
// expose (for example legacy left/right placement margins). A caller can
|
||||
// still reset every managed value by passing the complete defaults object.
|
||||
var completeSettings = vscDeepMergeDefaults(
|
||||
vscExpandStoredSettings(rawStorage || {}),
|
||||
settings || {}
|
||||
);
|
||||
var mutation = vscBuildManagedStorageMutation(
|
||||
rawStorage,
|
||||
completeSettings
|
||||
);
|
||||
|
||||
function removeStaleKeys() {
|
||||
if (!mutation.remove.length) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
chrome.storage.sync.remove(mutation.remove, function () {
|
||||
callback(chrome.runtime.lastError || null);
|
||||
});
|
||||
}
|
||||
],
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
showPopupControlBar: true,
|
||||
popupMatchHoverControls: true,
|
||||
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
enableSubtitleNudge: false,
|
||||
subtitleNudgeEnabledByDefault: true,
|
||||
subtitleNudgeInterval: 50,
|
||||
subtitleNudgeAmount: 0.001
|
||||
};
|
||||
|
||||
if (Object.keys(mutation.set).length === 0) {
|
||||
removeStaleKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(mutation.set, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
callback(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
removeStaleKeys();
|
||||
});
|
||||
}
|
||||
|
||||
if (existingStorage && typeof existingStorage === "object") {
|
||||
persistFromRaw(existingStorage);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(null, function (rawStorage) {
|
||||
if (chrome.runtime.lastError) {
|
||||
callback(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
persistFromRaw(rawStorage || {});
|
||||
});
|
||||
}
|
||||
|
||||
const actionLabels = {
|
||||
display: "Show/hide controller",
|
||||
@@ -261,14 +275,11 @@ function getDefaultShortcutValue(action) {
|
||||
}
|
||||
|
||||
function resolveShortcutValue(action, value) {
|
||||
if (value === undefined || value === null) {
|
||||
return getDefaultShortcutValue(action);
|
||||
}
|
||||
var numericValue = Number(value);
|
||||
if (Number.isFinite(numericValue)) {
|
||||
return numericValue;
|
||||
}
|
||||
return 0;
|
||||
return keyBindingUtils.sanitizeActionValue(
|
||||
action,
|
||||
value,
|
||||
getDefaultShortcutValue(action)
|
||||
);
|
||||
}
|
||||
|
||||
const customActionsNoValues = [
|
||||
@@ -406,6 +417,16 @@ function parseFiniteNumberOrFallback(value, fallback) {
|
||||
return Number.isFinite(numericValue) ? numericValue : fallback;
|
||||
}
|
||||
|
||||
function readOptionalPreferredSpeedInput(input) {
|
||||
var rawValue = input && typeof input.value === "string"
|
||||
? input.value.trim()
|
||||
: "";
|
||||
if (!rawValue) return undefined;
|
||||
var parsed = Number(rawValue);
|
||||
if (!Number.isFinite(parsed)) return undefined;
|
||||
return Math.min(16, Math.max(0.0625, parsed));
|
||||
}
|
||||
|
||||
function updateSiteRuleToggleIcon(toggleButton, action) {
|
||||
if (!toggleButton) return;
|
||||
var iconEl = toggleButton.querySelector(".site-rule-toggle-icon");
|
||||
@@ -726,13 +747,31 @@ function createKeyBindings(item) {
|
||||
};
|
||||
}
|
||||
|
||||
var bindingValue = 0;
|
||||
if (!customActionsNoValues.includes(action)) {
|
||||
var valueError = keyBindingUtils.getActionValueError(
|
||||
action,
|
||||
valueInput.value
|
||||
);
|
||||
if (valueError) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
"Error: Value for " +
|
||||
(actionLabels[action] || action) +
|
||||
" " +
|
||||
valueError +
|
||||
". Unable to save"
|
||||
};
|
||||
}
|
||||
bindingValue = Number(valueInput.value);
|
||||
}
|
||||
|
||||
keyBindings.push({
|
||||
action: action,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
? 0
|
||||
: Number(valueInput.value),
|
||||
value: bindingValue,
|
||||
force: false,
|
||||
predefined: predefined
|
||||
});
|
||||
@@ -768,10 +807,15 @@ function validate() {
|
||||
}
|
||||
|
||||
function save_options() {
|
||||
var status = document.getElementById("status");
|
||||
if (!optionsSyncSettingsLoaded) {
|
||||
status.textContent =
|
||||
"Error: Settings have not loaded. Retry before saving.";
|
||||
return;
|
||||
}
|
||||
if (validate() === false) return;
|
||||
|
||||
keyBindings = [];
|
||||
var status = document.getElementById("status");
|
||||
var saveError = null;
|
||||
|
||||
// Collect shortcuts from the main shortcuts section (both default and custom)
|
||||
@@ -793,6 +837,10 @@ function save_options() {
|
||||
settings.audioBoolean = document.getElementById("audioBoolean").checked;
|
||||
settings.enabled = document.getElementById("enabled").checked;
|
||||
settings.startHidden = document.getElementById("startHidden").checked;
|
||||
settings.shortcutTargetMode =
|
||||
document.getElementById("shortcutTargetMode").value === "all"
|
||||
? "all"
|
||||
: "closest";
|
||||
settings.hideWithControls = document.getElementById("hideWithControls").checked;
|
||||
settings.hideWithControlsTimer =
|
||||
Math.min(15, Math.max(0.1, parseFloat(document.getElementById("hideWithControlsTimer").value) || tcDefaults.hideWithControlsTimer));
|
||||
@@ -829,7 +877,6 @@ function save_options() {
|
||||
settings.subtitleNudgeInterval =
|
||||
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
|
||||
tcDefaults.subtitleNudgeInterval;
|
||||
settings.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
|
||||
|
||||
if (settings.subtitleNudgeInterval < 10) {
|
||||
settings.subtitleNudgeInterval = 10;
|
||||
@@ -851,11 +898,25 @@ function save_options() {
|
||||
var pattern = ruleEl.querySelector(".site-pattern").value.trim();
|
||||
if (pattern.length === 0) return;
|
||||
|
||||
var rule = { pattern: pattern };
|
||||
var rule = vscClonePlainData(ruleEl.vscOriginalRule) || {};
|
||||
rule.pattern = pattern;
|
||||
var title = ruleEl.querySelector(".site-title").value.trim();
|
||||
if (title) rule.title = title;
|
||||
else delete rule.title;
|
||||
delete rule.disableExtension;
|
||||
|
||||
// Handle Enable toggle
|
||||
rule.enabled = ruleEl.querySelector(".site-enabled").checked;
|
||||
|
||||
if (ruleEl.querySelector(".override-shortcut-target").checked) {
|
||||
rule.shortcutTargetMode =
|
||||
ruleEl.querySelector(".site-shortcutTargetMode").value === "all"
|
||||
? "all"
|
||||
: "closest";
|
||||
} else {
|
||||
delete rule.shortcutTargetMode;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-placement").checked) {
|
||||
rule.controllerLocation = normalizeControllerLocation(
|
||||
ruleEl.querySelector(".site-controllerLocation").value
|
||||
@@ -874,10 +935,16 @@ function save_options() {
|
||||
tcDefaults.controllerMarginBottom
|
||||
)
|
||||
);
|
||||
} else {
|
||||
delete rule.controllerLocation;
|
||||
delete rule.controllerMarginTop;
|
||||
delete rule.controllerMarginBottom;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-visibility").checked) {
|
||||
rule.startHidden = ruleEl.querySelector(".site-startHidden").checked;
|
||||
} else {
|
||||
delete rule.startHidden;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-autohide").checked) {
|
||||
@@ -889,13 +956,29 @@ function save_options() {
|
||||
15,
|
||||
Math.max(0.1, Number.isFinite(st) ? st : settings.hideWithControlsTimer)
|
||||
);
|
||||
} else {
|
||||
delete rule.hideWithControls;
|
||||
delete rule.hideWithControlsTimer;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-playback").checked) {
|
||||
rule.rememberSpeed = ruleEl.querySelector(".site-rememberSpeed").checked;
|
||||
rule.forceLastSavedSpeed =
|
||||
ruleEl.querySelector(".site-forceLastSavedSpeed").checked;
|
||||
var preferredSpeed = readOptionalPreferredSpeedInput(
|
||||
ruleEl.querySelector(".site-preferredSpeed")
|
||||
);
|
||||
if (preferredSpeed === undefined) {
|
||||
delete rule.preferredSpeed;
|
||||
} else {
|
||||
rule.preferredSpeed = preferredSpeed;
|
||||
}
|
||||
rule.audioBoolean = ruleEl.querySelector(".site-audioBoolean").checked;
|
||||
} else {
|
||||
delete rule.rememberSpeed;
|
||||
delete rule.forceLastSavedSpeed;
|
||||
delete rule.preferredSpeed;
|
||||
delete rule.audioBoolean;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-opacity").checked) {
|
||||
@@ -904,6 +987,8 @@ function save_options() {
|
||||
ruleEl.querySelector(".site-controllerOpacity").value,
|
||||
settings.controllerOpacity
|
||||
);
|
||||
} else {
|
||||
delete rule.controllerOpacity;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
||||
@@ -922,6 +1007,10 @@ function save_options() {
|
||||
Number.isFinite(nudgeIv) ? nudgeIv : settings.subtitleNudgeInterval
|
||||
)
|
||||
);
|
||||
} else {
|
||||
delete rule.enableSubtitleNudge;
|
||||
delete rule.subtitleNudgeEnabledByDefault;
|
||||
delete rule.subtitleNudgeInterval;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-controlbar").checked) {
|
||||
@@ -929,6 +1018,8 @@ function save_options() {
|
||||
if (activeZone) {
|
||||
rule.controllerButtons = readControlBarOrder(activeZone);
|
||||
}
|
||||
} else {
|
||||
delete rule.controllerButtons;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-popup-controlbar").checked) {
|
||||
@@ -940,6 +1031,9 @@ function save_options() {
|
||||
readControlBarOrder(popupActiveZone)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
delete rule.showPopupControlBar;
|
||||
delete rule.popupControllerButtons;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
||||
@@ -971,37 +1065,56 @@ function save_options() {
|
||||
return;
|
||||
}
|
||||
|
||||
var shortcutValue = 0;
|
||||
if (!customActionsNoValues.includes(action)) {
|
||||
var shortcutValueError = keyBindingUtils.getActionValueError(
|
||||
action,
|
||||
valueInput.value
|
||||
);
|
||||
if (shortcutValueError) {
|
||||
saveError =
|
||||
"Error: Site rule value for " +
|
||||
(actionLabels[action] || action) +
|
||||
" " +
|
||||
shortcutValueError +
|
||||
". Unable to save";
|
||||
return;
|
||||
}
|
||||
shortcutValue = Number(valueInput.value);
|
||||
}
|
||||
|
||||
shortcuts.push({
|
||||
action: action,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
? 0
|
||||
: Number(valueInput.value),
|
||||
value: shortcutValue,
|
||||
force: forceCheckbox ? forceCheckbox.checked : false
|
||||
});
|
||||
});
|
||||
if (saveError) return;
|
||||
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
||||
else delete rule.shortcuts;
|
||||
} else {
|
||||
delete rule.shortcuts;
|
||||
}
|
||||
|
||||
settings.siteRules.push(rule);
|
||||
});
|
||||
|
||||
// Legacy keys to remove
|
||||
const legacyKeys = [
|
||||
"resetSpeed", "speedStep", "fastSpeed", "rewindTime", "advanceTime",
|
||||
"resetKeyCode", "slowerKeyCode", "fasterKeyCode", "rewindKeyCode",
|
||||
"advanceKeyCode", "fastKeyCode", "blacklist"
|
||||
];
|
||||
if (saveError) {
|
||||
status.textContent = saveError;
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.remove(legacyKeys, function () {
|
||||
chrome.storage.sync.set(settings, function () {
|
||||
status.textContent = "Options saved";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
persistManagedSyncSettings(settings, function (error) {
|
||||
if (error) {
|
||||
status.textContent = "Error: Unable to save options - " + error.message;
|
||||
return;
|
||||
}
|
||||
status.textContent = "Options saved";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1084,9 +1197,12 @@ function createSiteRule(rule) {
|
||||
var template = document.getElementById("siteRuleTemplate");
|
||||
var clone = template.content.cloneNode(true);
|
||||
var ruleEl = clone.querySelector(".site-rule");
|
||||
ruleEl.vscOriginalRule = vscClonePlainData(rule) || {};
|
||||
|
||||
var pattern = rule && rule.pattern ? rule.pattern : "";
|
||||
ruleEl.querySelector(".site-pattern").value = pattern;
|
||||
ruleEl.querySelector(".site-title").value =
|
||||
rule && typeof rule.title === "string" ? rule.title : "";
|
||||
|
||||
// Make the rule body collapsed by default
|
||||
setSiteRuleExpandedState(ruleEl, false);
|
||||
@@ -1117,6 +1233,17 @@ function createSiteRule(rule) {
|
||||
}
|
||||
updateDisabledState();
|
||||
|
||||
ruleEl.querySelector(".override-shortcut-target").checked = Boolean(
|
||||
rule && rule.shortcutTargetMode !== undefined
|
||||
);
|
||||
ruleEl.querySelector(".site-shortcutTargetMode").value =
|
||||
rule && rule.shortcutTargetMode === "all" ? "all" : "closest";
|
||||
applySiteRuleOverrideState(
|
||||
ruleEl,
|
||||
"override-shortcut-target",
|
||||
"site-shortcut-target-container"
|
||||
);
|
||||
|
||||
var placementKeys = [
|
||||
"controllerLocation",
|
||||
"controllerMarginTop",
|
||||
@@ -1150,11 +1277,16 @@ function createSiteRule(rule) {
|
||||
rule &&
|
||||
(rule.rememberSpeed !== undefined ||
|
||||
rule.forceLastSavedSpeed !== undefined ||
|
||||
rule.preferredSpeed !== undefined ||
|
||||
rule.audioBoolean !== undefined)
|
||||
);
|
||||
ruleEl.querySelector(".override-playback").checked = hasPlaybackOverride;
|
||||
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
||||
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
||||
ruleEl.querySelector(".site-preferredSpeed").value =
|
||||
rule && Number.isFinite(Number(rule.preferredSpeed))
|
||||
? String(rule.preferredSpeed)
|
||||
: "";
|
||||
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
||||
applySiteRuleOverrideState(ruleEl, "override-playback", "site-playback-container");
|
||||
|
||||
@@ -1621,11 +1753,26 @@ function initLucideButtonIconsUI() {
|
||||
}
|
||||
}
|
||||
|
||||
function restore_options() {
|
||||
chrome.storage.sync.get(tcDefaults, function (storage) {
|
||||
function restore_options(callback) {
|
||||
var done = typeof callback === "function" ? callback : function () {};
|
||||
var status = document.getElementById("status");
|
||||
setOptionsSyncSettingsLoaded(false);
|
||||
chrome.storage.sync.get(null, function (rawStorage) {
|
||||
var syncError = chrome.runtime.lastError;
|
||||
if (syncError) {
|
||||
status.textContent =
|
||||
"Error: Unable to load options - " + syncError.message;
|
||||
done(syncError);
|
||||
return;
|
||||
}
|
||||
var storage = vscExpandStoredSettings(rawStorage || {});
|
||||
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||
var localError = chrome.runtime.lastError;
|
||||
customButtonIconsLive =
|
||||
loc && loc.customButtonIcons && typeof loc.customButtonIcons === "object"
|
||||
!localError &&
|
||||
loc &&
|
||||
loc.customButtonIcons &&
|
||||
typeof loc.customButtonIcons === "object"
|
||||
? loc.customButtonIcons
|
||||
: {};
|
||||
|
||||
@@ -1635,13 +1782,11 @@ function restore_options() {
|
||||
document.getElementById("audioBoolean").checked = storage.audioBoolean;
|
||||
document.getElementById("enabled").checked = storage.enabled;
|
||||
document.getElementById("startHidden").checked = storage.startHidden;
|
||||
document.getElementById("shortcutTargetMode").value =
|
||||
storage.shortcutTargetMode === "all" ? "all" : "closest";
|
||||
|
||||
// Migration/Normalization for hideWithControls
|
||||
const hideWithControls = typeof storage.hideWithControls !== "undefined"
|
||||
? storage.hideWithControls
|
||||
: storage.hideWithYouTubeControls;
|
||||
|
||||
document.getElementById("hideWithControls").checked = hideWithControls;
|
||||
document.getElementById("hideWithControls").checked =
|
||||
storage.hideWithControls;
|
||||
document.getElementById("hideWithControlsTimer").value =
|
||||
storage.hideWithControlsTimer || tcDefaults.hideWithControlsTimer;
|
||||
|
||||
@@ -1699,11 +1844,7 @@ function restore_options() {
|
||||
|
||||
refreshAddShortcutSelector();
|
||||
|
||||
// Load site rules (use defaults if none in storage or empty array)
|
||||
var siteRules =
|
||||
Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
||||
? storage.siteRules
|
||||
: tcDefaults.siteRules || [];
|
||||
var siteRules = Array.isArray(storage.siteRules) ? storage.siteRules : [];
|
||||
|
||||
vscClearElement(document.getElementById("siteRulesContainer"));
|
||||
if (siteRules.length > 0) {
|
||||
@@ -1729,25 +1870,87 @@ function restore_options() {
|
||||
updatePopupEditorDisabledState();
|
||||
|
||||
initLucideButtonIconsUI();
|
||||
setOptionsSyncSettingsLoaded(true);
|
||||
if (localError) {
|
||||
status.textContent =
|
||||
"Warning: Options loaded, but custom icons could not be read - " +
|
||||
localError.message;
|
||||
}
|
||||
done(localError || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restore_defaults() {
|
||||
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
||||
var status = document.getElementById("status");
|
||||
var restoreButton = document.getElementById("restore");
|
||||
setOptionsSyncSettingsLoaded(false);
|
||||
if (restoreButton) restoreButton.disabled = true;
|
||||
|
||||
chrome.storage.local.remove(
|
||||
["customButtonIcons", "lucideTagsCacheV1", "lucideTagsCacheV1At"],
|
||||
function () {}
|
||||
);
|
||||
function finishRestore(error) {
|
||||
// Leave the storage callback before rereading. Besides avoiding nested API
|
||||
// calls, this prevents a callback-scoped runtime.lastError from being
|
||||
// mistaken for an error from the fresh read.
|
||||
Promise.resolve().then(function () {
|
||||
restore_options(function (reloadError) {
|
||||
if (restoreButton) restoreButton.disabled = false;
|
||||
if (error) {
|
||||
status.textContent =
|
||||
"Error: Unable to restore all defaults - " + error.message;
|
||||
return;
|
||||
}
|
||||
if (reloadError) {
|
||||
status.textContent =
|
||||
"Defaults restored, but the options page could not reload - " +
|
||||
reloadError.message;
|
||||
return;
|
||||
}
|
||||
status.textContent = "Default options restored";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(tcDefaults, function () {
|
||||
restore_options();
|
||||
var status = document.getElementById("status");
|
||||
status.textContent = "Default options restored";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
persistManagedSyncSettings(tcDefaults, function (error) {
|
||||
if (error) {
|
||||
finishRestore(error);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set({ lastSpeed: tcDefaults.lastSpeed }, function () {
|
||||
var lastSpeedError = chrome.runtime.lastError;
|
||||
if (lastSpeedError) {
|
||||
finishRestore(lastSpeedError);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set(
|
||||
{
|
||||
rememberedSpeeds: {},
|
||||
rememberedSpeedsResetAt: Date.now()
|
||||
},
|
||||
function () {
|
||||
var speedResetError = chrome.runtime.lastError;
|
||||
if (speedResetError) {
|
||||
finishRestore(speedResetError);
|
||||
return;
|
||||
}
|
||||
chrome.storage.local.remove(
|
||||
[
|
||||
"customButtonIcons",
|
||||
"lucideTagsCacheV1",
|
||||
"lucideTagsCacheV1At",
|
||||
"rememberedSpeeds"
|
||||
],
|
||||
function () {
|
||||
finishRestore(chrome.runtime.lastError || null);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user