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, 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, darkMode: boolean, showCategoryGuidelines: boolean, chaptersAvailable: 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, 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, 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, darkMode: true, showCategoryGuidelines: true, chaptersAvailable: 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: "]"}, previousChapterKeybind: {key: "["}, 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, 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;