diff --git a/settings-core.js b/settings-core.js new file mode 100644 index 0000000..62a8195 --- /dev/null +++ b/settings-core.js @@ -0,0 +1,644 @@ +(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", + "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", + "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, + 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);