import * as CompileConfig from "../config.json"; import * as invidiousList from "../ci/invidiouslist.json"; import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types"; import { keybindEquals } from "./utils/configUtils"; export interface Permission { canSubmit: boolean; } interface SBConfig { userID: string; isVip: boolean; permissions: Record; /* Contains unsubmitted segments that the user has created. */ unsubmittedSegments: Record; defaultCategory: Category; renderSegmentsAsChapters: boolean; whitelistedChannels: string[]; forceChannelCheck: boolean; minutesSaved: number; skipCount: number; sponsorTimesContributed: number; submissionCountSinceCategories: number; // New count used to show the "Read The Guidelines!!" message showTimeWithSkips: boolean; disableSkipping: boolean; muteSegments: boolean; fullVideoSegments: boolean; manualSkipOnFullVideo: boolean; trackViewCount: boolean; trackViewCountInPrivate: boolean; trackDownvotes: boolean; dontShowNotice: boolean; noticeVisibilityMode: NoticeVisbilityMode; hideVideoPlayerControls: boolean; hideInfoButtonPlayerControls: boolean; hideDeleteButtonPlayerControls: boolean; hideUploadButtonPlayerControls: boolean; hideSkipButtonPlayerControls: boolean; hideDiscordLaunches: number; hideDiscordLink: boolean; invidiousInstances: string[]; supportInvidious: boolean; serverAddress: string; minDuration: number; skipNoticeDuration: number; audioNotificationOnSkip: boolean; checkForUnlistedVideos: boolean; testingServer: boolean; refetchWhenNotFound: boolean; ytInfoPermissionGranted: boolean; allowExpirements: boolean; showDonationLink: boolean; showPopupDonationCount: number; showUpsells: boolean; donateClicked: number; autoHideInfoButton: boolean; autoSkipOnMusicVideos: boolean; colorPalette: { red: string; white: string; locked: string; }; scrollToEditTimeUpdate: boolean; categoryPillUpdate: boolean; showChapterInfoMessage: boolean; darkMode: boolean; showCategoryGuidelines: boolean; showCategoryWithoutPermission: boolean; showSegmentNameInChapterBar: boolean; useVirtualTime: boolean; // Used to cache calculated text color info categoryPillColors: { [key in Category]: { lastColor: string; textColor: string; } }; skipKeybind: Keybind; startSponsorKeybind: Keybind; submitKeybind: Keybind; nextChapterKeybind: Keybind; previousChapterKeybind: Keybind; // What categories should be skipped categorySelections: CategorySelection[]; payments: { licenseKey: string; lastCheck: number; lastFreeCheck: number; freeAccess: boolean; chaptersAllowed: boolean; }; // Preview bar barTypes: { "preview-chooseACategory": PreviewBarOption; "sponsor": PreviewBarOption; "preview-sponsor": PreviewBarOption; "selfpromo": PreviewBarOption; "preview-selfpromo": PreviewBarOption; "exclusive_access": PreviewBarOption; "interaction": PreviewBarOption; "preview-interaction": PreviewBarOption; "intro": PreviewBarOption; "preview-intro": PreviewBarOption; "outro": PreviewBarOption; "preview-outro": PreviewBarOption; "preview": PreviewBarOption; "preview-preview": PreviewBarOption; "music_offtopic": PreviewBarOption; "preview-music_offtopic": PreviewBarOption; "poi_highlight": PreviewBarOption; "preview-poi_highlight": PreviewBarOption; "filler": PreviewBarOption; "preview-filler": PreviewBarOption; }; } export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[] ; lastAccess: number }; interface SBStorage { /* VideoID prefixes to UUID prefixes */ downvotedSegments: Record; navigationApiAvailable: boolean; } export interface SBObject { configLocalListeners: Array<(changes: StorageChangesObject) => unknown>; configSyncListeners: Array<(changes: StorageChangesObject) => unknown>; syncDefaults: SBConfig; localDefaults: SBStorage; cachedSyncConfig: SBConfig; cachedLocalStorage: SBStorage; config: SBConfig; local: SBStorage; forceSyncUpdate(prop: string): void; forceLocalUpdate(prop: string): void; resetToDefault(): void; } const Config: SBObject = { /** * Callback function when an option is updated */ configLocalListeners: [], configSyncListeners: [], syncDefaults: { userID: null, isVip: false, permissions: {}, unsubmittedSegments: {}, defaultCategory: "chooseACategory" as Category, renderSegmentsAsChapters: false, whitelistedChannels: [], forceChannelCheck: false, minutesSaved: 0, skipCount: 0, sponsorTimesContributed: 0, submissionCountSinceCategories: 0, showTimeWithSkips: true, disableSkipping: false, muteSegments: true, fullVideoSegments: true, manualSkipOnFullVideo: false, trackViewCount: true, trackViewCountInPrivate: true, trackDownvotes: true, dontShowNotice: false, noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip, hideVideoPlayerControls: false, hideInfoButtonPlayerControls: false, hideDeleteButtonPlayerControls: false, hideUploadButtonPlayerControls: false, hideSkipButtonPlayerControls: false, hideDiscordLaunches: 0, hideDiscordLink: false, invidiousInstances: ["invidious.snopyta.org"], // leave as default supportInvidious: false, serverAddress: CompileConfig.serverAddress, minDuration: 0, skipNoticeDuration: 4, audioNotificationOnSkip: false, checkForUnlistedVideos: false, testingServer: false, refetchWhenNotFound: true, ytInfoPermissionGranted: false, allowExpirements: true, showDonationLink: true, showPopupDonationCount: 0, showUpsells: true, donateClicked: 0, autoHideInfoButton: true, autoSkipOnMusicVideos: false, scrollToEditTimeUpdate: false, // false means the tooltip will be shown categoryPillUpdate: false, showChapterInfoMessage: true, darkMode: true, showCategoryGuidelines: true, showCategoryWithoutPermission: false, showSegmentNameInChapterBar: true, useVirtualTime: true, categoryPillColors: {}, /** * Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none). * Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales. * The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't. * Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"! * TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map? */ skipKeybind: {key: "Enter"}, startSponsorKeybind: {key: ";"}, submitKeybind: {key: "'"}, nextChapterKeybind: {key: "]", ctrl: true}, previousChapterKeybind: {key: "[", ctrl: true}, categorySelections: [{ name: "sponsor" as Category, option: CategorySkipOption.AutoSkip }, { name: "poi_highlight" as Category, option: CategorySkipOption.ManualSkip }, { name: "exclusive_access" as Category, option: CategorySkipOption.ShowOverlay }], payments: { licenseKey: null, lastCheck: 0, lastFreeCheck: 0, freeAccess: false, chaptersAllowed: false }, colorPalette: { red: "#780303", white: "#ffffff", locked: "#ffc83d" }, // Preview bar barTypes: { "preview-chooseACategory": { color: "#ffffff", opacity: "0.7" }, "sponsor": { color: "#00d400", opacity: "0.7" }, "preview-sponsor": { color: "#007800", opacity: "0.7" }, "selfpromo": { color: "#ffff00", opacity: "0.7" }, "preview-selfpromo": { color: "#bfbf35", opacity: "0.7" }, "exclusive_access": { color: "#008a5c", opacity: "0.7" }, "interaction": { color: "#cc00ff", opacity: "0.7" }, "preview-interaction": { color: "#6c0087", opacity: "0.7" }, "intro": { color: "#00ffff", opacity: "0.7" }, "preview-intro": { color: "#008080", opacity: "0.7" }, "outro": { color: "#0202ed", opacity: "0.7" }, "preview-outro": { color: "#000070", opacity: "0.7" }, "preview": { color: "#008fd6", opacity: "0.7" }, "preview-preview": { color: "#005799", opacity: "0.7" }, "music_offtopic": { color: "#ff9900", opacity: "0.7" }, "preview-music_offtopic": { color: "#a6634a", opacity: "0.7" }, "poi_highlight": { color: "#ff1684", opacity: "0.7" }, "preview-poi_highlight": { color: "#9b044c", opacity: "0.7" }, "filler": { color: "#7300FF", opacity: "0.9" }, "preview-filler": { color: "#2E0066", opacity: "0.7" } } }, localDefaults: { downvotedSegments: {}, navigationApiAvailable: null }, cachedSyncConfig: null, cachedLocalStorage: null, config: null, local: null, forceSyncUpdate, forceLocalUpdate, resetToDefault }; // Function setup function configProxy(): { sync: SBConfig; local: SBStorage } { chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}, areaName) => { if (areaName === "sync") { for (const key in changes) { Config.cachedSyncConfig[key] = changes[key].newValue; } for (const callback of Config.configSyncListeners) { callback(changes); } } else if (areaName === "local") { for (const key in changes) { Config.cachedLocalStorage[key] = changes[key].newValue; } for (const callback of Config.configLocalListeners) { callback(changes); } } }); const syncHandler: ProxyHandler = { set(obj: SBConfig, prop: K, value: SBConfig[K]) { Config.cachedSyncConfig[prop] = value; chrome.storage.sync.set({ [prop]: value }); return true; }, get(obj: SBConfig, prop: K): SBConfig[K] { const data = Config.cachedSyncConfig[prop]; return obj[prop] || data; }, deleteProperty(obj: SBConfig, prop: keyof SBConfig) { chrome.storage.sync.remove( prop); return true; } }; const localHandler: ProxyHandler = { set(obj: SBStorage, prop: K, value: SBStorage[K]) { Config.cachedLocalStorage[prop] = value; chrome.storage.local.set({ [prop]: value }); return true; }, get(obj: SBStorage, prop: K): SBStorage[K] { const data = Config.cachedLocalStorage[prop]; return obj[prop] || data; }, deleteProperty(obj: SBStorage, prop: keyof SBStorage) { chrome.storage.local.remove( prop); return true; } }; return { sync: new Proxy({ handler: syncHandler } as unknown as SBConfig, syncHandler), local: new Proxy({ handler: localHandler } as unknown as SBStorage, localHandler) }; } function forceSyncUpdate(prop: string): void { const value = Config.cachedSyncConfig[prop]; if (prop === "unsubmittedSegments") { // Early to be safe if (JSON.stringify(value).length + prop.length > 8000) { for (const key in value) { if (!value[key] || value[key].length <= 0) { delete value[key]; } } } } chrome.storage.sync.set({ [prop]: value }); } function forceLocalUpdate(prop: string): void { chrome.storage.local.set({ [prop]: Config.cachedLocalStorage[prop] }); } async function fetchConfig(): Promise { await Promise.all([new Promise((resolve) => { chrome.storage.sync.get(null, function(items) { Config.cachedSyncConfig = items; resolve(); }); }), new Promise((resolve) => { chrome.storage.local.get(null, function(items) { Config.cachedLocalStorage = items; resolve(); }); })]); } function migrateOldSyncFormats(config: SBConfig) { if (config["segmentTimes"]) { const unsubmittedSegments = {}; for (const item of config["segmentTimes"]) { unsubmittedSegments[item[0]] = item[1]; } chrome.storage.sync.remove("segmentTimes", () => config.unsubmittedSegments = unsubmittedSegments); } if (!config["exclusive_accessCategoryAdded"] && !config.categorySelections.some((s) => s.name === "exclusive_access")) { config["exclusive_accessCategoryAdded"] = true; config.categorySelections.push({ name: "exclusive_access" as Category, option: CategorySkipOption.ShowOverlay }); config.categorySelections = config.categorySelections; } if (config["fillerUpdate"] !== undefined) { chrome.storage.sync.remove("fillerUpdate"); } if (config["highlightCategoryAdded"] !== undefined) { chrome.storage.sync.remove("highlightCategoryAdded"); } if (config["highlightCategoryUpdate"] !== undefined) { chrome.storage.sync.remove("highlightCategoryUpdate"); } if (config["askAboutUnlistedVideos"]) { chrome.storage.sync.remove("askAboutUnlistedVideos"); } if (!config["autoSkipOnMusicVideosUpdate"]) { config["autoSkipOnMusicVideosUpdate"] = true; for (const selection of config.categorySelections) { if (selection.name === "music_offtopic" && selection.option === CategorySkipOption.AutoSkip) { config.autoSkipOnMusicVideos = true; break; } } } if (config["disableAutoSkip"]) { for (const selection of config.categorySelections) { if (selection.name === "sponsor") { selection.option = CategorySkipOption.ManualSkip; chrome.storage.sync.remove("disableAutoSkip"); } } } if (typeof config["skipKeybind"] == "string") { config["skipKeybind"] = {key: config["skipKeybind"]}; } if (typeof config["startSponsorKeybind"] == "string") { config["startSponsorKeybind"] = {key: config["startSponsorKeybind"]}; } if (typeof config["submitKeybind"] == "string") { config["submitKeybind"] = {key: config["submitKeybind"]}; } // Unbind key if it matches a previous one set by the user (should be ordered oldest to newest) const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"]; for (let i = keybinds.length-1; i >= 0; i--) { for (let j = 0; j < keybinds.length; j++) { if (i == j) continue; if (keybindEquals(config[keybinds[i]], config[keybinds[j]])) config[keybinds[i]] = null; } } // Remove some old unused options if (config["sponsorVideoID"] !== undefined) { chrome.storage.sync.remove("sponsorVideoID"); } if (config["previousVideoID"] !== undefined) { chrome.storage.sync.remove("previousVideoID"); } // populate invidiousInstances with new instances if 3p support is **DISABLED** if (!config["supportInvidious"] && config["invidiousInstances"].length !== invidiousList.length) { config["invidiousInstances"] = invidiousList; } if (config["lastIsVipUpdate"]) { chrome.storage.sync.remove("lastIsVipUpdate"); } } async function setupConfig() { if (typeof(chrome) === "undefined") return; await fetchConfig(); addDefaults(); const config = configProxy(); migrateOldSyncFormats(config.sync); Config.config = config.sync; Config.local = config.local; } // Add defaults function addDefaults() { for (const key in Config.syncDefaults) { if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig, key)) { Config.cachedSyncConfig[key] = Config.syncDefaults[key]; } else if (key === "barTypes") { for (const key2 in Config.syncDefaults[key]) { if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig[key], key2)) { Config.cachedSyncConfig[key][key2] = Config.syncDefaults[key][key2]; } } } } for (const key in Config.localDefaults) { if(!Object.prototype.hasOwnProperty.call(Config.cachedLocalStorage, key)) { Config.cachedLocalStorage[key] = Config.localDefaults[key]; } } } function resetToDefault() { chrome.storage.sync.set({ ...Config.syncDefaults, userID: Config.config.userID, minutesSaved: Config.config.minutesSaved, skipCount: Config.config.skipCount, sponsorTimesContributed: Config.config.sponsorTimesContributed }); } // Sync config setupConfig(); export default Config;