Files
SponsorBlock/src/config.ts

609 lines
19 KiB
TypeScript

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<Category, Permission>;
/* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>;
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;
showSegmentFailedToFetchWarning: boolean;
allowScrollingToEdit: 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<VideoID & HashedValue, VideoDownvotes>;
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,
showSegmentFailedToFetchWarning: true,
allowScrollingToEdit: 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: "ArrowRight", ctrl: true},
previousChapterKeybind: {key: "ArrowLeft", 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<SBConfig> = {
set<K extends keyof SBConfig>(obj: SBConfig, prop: K, value: SBConfig[K]) {
Config.cachedSyncConfig[prop] = value;
chrome.storage.sync.set({
[prop]: value
});
return true;
},
get<K extends keyof SBConfig>(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(<string> prop);
return true;
}
};
const localHandler: ProxyHandler<SBStorage> = {
set<K extends keyof SBStorage>(obj: SBStorage, prop: K, value: SBStorage[K]) {
Config.cachedLocalStorage[prop] = value;
chrome.storage.local.set({
[prop]: value
});
return true;
},
get<K extends keyof SBStorage>(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(<string> prop);
return true;
}
};
return {
sync: new Proxy<SBConfig>({ handler: syncHandler } as unknown as SBConfig, syncHandler),
local: new Proxy<SBStorage>({ 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<void> {
await Promise.all([new Promise<void>((resolve) => {
chrome.storage.sync.get(null, function(items) {
Config.cachedSyncConfig = <SBConfig> <unknown> items;
resolve();
});
}), new Promise<void>((resolve) => {
chrome.storage.local.get(null, function(items) {
Config.cachedLocalStorage = <SBStorage> <unknown> 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;