37 |
38 |
39 | Blocked Channels
40 | Blocked Titles
41 | Blocked Names
42 |
43 | Excluded Channels
44 |
45 | Appearance
46 | Import/Export
47 |
48 | About Channel Blocker
49 | FAQ
50 |
51 |
52 |
53 |
Blocked users/channels
54 |
55 |
56 |
57 | add
58 |
59 |
60 | case-insensitive
61 |
62 |
63 |
64 |
65 |
66 |
67 | test
68 | test1
69 |
70 |
71 | remove
72 |
73 |
74 |
75 |
76 |
Design
77 |
78 |
79 | Configuration page design
80 |
81 | Device
82 | Dark
83 | Light
84 |
85 |
86 |
87 |
88 | Color of block-buttons
89 |
90 |
91 |
92 |
93 | Size of block-buttons
94 |
95 |
96 |
97 |
98 | Open quick popup when pressing toolbar-button
99 |
100 |
101 |
102 |
103 |
104 |
105 |
Visibility
106 |
107 |
108 | Show block-buttons
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Speed of block-animation
117 |
119 |
120 |
121 |
122 | reset
123 |
124 |
125 |
126 |
Import/Export Your Configuration
127 |
128 |
129 | Import
130 |
131 |
132 |
133 | Export
134 |
135 |
136 |
137 |
138 |
139 |
About Channel Blocker
140 |
141 |
142 |
143 | Channel Blocker empowers you to block channels effortlessly with just one
144 | click.
145 | It also supports regular expressions for unparalleled customization, allowing you to tailor your
146 | YouTube™ experience to your preferences.
147 | And another thing: Our commitment to privacy means no user information is collected.
148 |
149 |
150 |
151 |
152 |
Support Us and Our Work:
153 |
154 | Do you like what we do? Do you want to support us and our work? A subscription on Patreon would
155 | help
156 | a lot at developing this tool as well as realizing our future projects.
157 |
158 |
159 |
160 |
169 |
170 |
171 |
172 |
FAQ
173 |
174 |
175 |
176 |
177 |
Why does this add-on require permissions?
178 |
179 | ⯅
180 | ⯆
181 |
182 |
183 |
184 |
185 |
186 |
Access to Your Data on www.youtube.com:
187 |
188 | - This permission is necessary for functionalities such as removing users/channels
189 | you
190 | have
191 | blocked
192 | on YouTube™ and adding the 'x'-button.
193 | It allows the add-on to interact with and modify your YouTube™ data as needed.
194 |
195 |
Access to Browser Tabs:
196 |
197 | - This permission is essential for keeping track of your active YouTube™ tabs and
198 | managing
199 | the
200 | Add-on setting page tab.
201 |
202 |
Storage of Unlimited Client-Side Data:
203 |
204 | - This permission is required to store an unlimited amount of client-side data,
205 | enabling
206 | the
207 | add-on
208 | to effectively block any number of users or channels.
209 |
210 |
211 |
212 |
213 |
214 |
215 |
245 |
246 |
247 |
248 |
Do you collect any data?
249 |
250 | ⯅
251 | ⯆
252 |
253 |
254 |
255 |
256 |
257 |
258 | We want to assure you that we do not collect any data
259 | from
260 | you
261 | in
262 | any form.
263 | Your privacy is important to us, and our commitment is to ensure that your usage
264 | remains
265 | secure
266 | and confidential.
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
Troubleshooting Guide: Add-on Not Functioning
276 |
277 | ⯅
278 | ⯆
279 |
280 |
281 |
282 |
283 |
284 |
Check for Updates
285 |
286 | Ensure that you have the latest version of Channel Blocker installed. If not, update
287 | to
288 | the
289 | newest version for optimal performance.
290 |
291 |
292 |
Mobile Compatibility
293 |
294 | If you are using a phone, we are sorry but this add-on doesn't work on mobile
295 | browsers.
296 |
297 |
298 |
Reporting a Bug
299 |
300 | If you encounter any issues, even after updating to the latest version, please help
301 | us improve by reporting them. You can submit bug reports through our
302 |
304 | GitHub Issues page
305 | .
306 |
307 | Before opening a new issue, we kindly ask you to check if a similar issue has
308 | already been reported. Duplicate issues can clutter the tracker and delay
309 | resolution.
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
--------------------------------------------------------------------------------
/ui/settings/scripts/donate.ts:
--------------------------------------------------------------------------------
1 | const donateButton = document.getElementById("donate-btn") as HTMLButtonElement;
2 | const donateButtonPaypal = document.getElementById("donate-btn-paypal") as HTMLButtonElement;
3 | const donateButtonPatreon = document.getElementById("donate-btn-patreon") as HTMLButtonElement;
4 | const closeDonationButton = document.getElementById("close-donation-btn") as HTMLButtonElement;
5 | const donationDialog = document.getElementById("donation-dialog") as HTMLDialogElement;
6 |
7 | const paypalDonationUrl = "https://www.paypal.com/donate/?hosted_button_id=KYJWDCH3ZJ4U2";
8 | const patreonUrl = "https://www.patreon.com/time_machine_development";
9 |
10 | export function initDonation() {
11 | donateButton.addEventListener("click", () => {
12 | donationDialog.showModal();
13 | //donationContainer.classList.add("open");
14 | });
15 | closeDonationButton.addEventListener("click", () => {
16 | donationDialog.close();
17 | //donationDialog.classList.remove("open");
18 | });
19 | donateButtonPaypal.addEventListener("click", () => {
20 | window.open(paypalDonationUrl, "_blank");
21 | });
22 | donateButtonPatreon.addEventListener("click", () => {
23 | window.open(patreonUrl, "_blank");
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/ui/settings/scripts/enums.ts:
--------------------------------------------------------------------------------
1 | export enum MessageType {
2 | ADD_BLOCKING_RULE,
3 | REMOVE_BLOCKING_RULE,
4 | IS_BLOCKED,
5 | STORAGE_CHANGED,
6 | REQUEST_SETTINGS,
7 | SETTINGS_CHANGED,
8 | }
9 |
10 | export enum CommunicationRole {
11 | SERVICE_WORKER,
12 | CONTENT_SCRIPT,
13 | SETTINGS,
14 | }
15 |
16 | export enum SettingsDesign {
17 | DETECT,
18 | DARK,
19 | LICHT,
20 | }
21 |
22 | export enum SettingsState {
23 | BLOCKED_CHANNELS,
24 | BLOCKED_TITLES,
25 | BLOCKED_NAMES,
26 | BLOCKED_COMMENTS,
27 | EXCLUDED_CHANNELS,
28 | APPEARANCE,
29 | IMPORT_EXPORT,
30 | ABOUT,
31 | FAQ,
32 | }
33 |
--------------------------------------------------------------------------------
/ui/settings/scripts/faq.ts:
--------------------------------------------------------------------------------
1 | export function initFaq() {
2 | const faqPermissionHeading = document.getElementById("faq-permission-heading") as HTMLDivElement;
3 | const faqContactHeading = document.getElementById("faq-contact-heading") as HTMLDivElement;
4 | const faqPrivacyHeading = document.getElementById("faq-privacy-heading") as HTMLDivElement;
5 | const faqTroubleshootingHeading = document.getElementById("faq-troubleshooting-heading") as HTMLDivElement;
6 |
7 | const faqPermissionSection = document.getElementById("faq-permission-section") as HTMLDivElement;
8 | const faqContactSection = document.getElementById("faq-contact-section") as HTMLDivElement;
9 | const faqPrivacySection = document.getElementById("faq-privacy-section") as HTMLDivElement;
10 | const faqTroubleshootingSection = document.getElementById("faq-troubleshooting-section") as HTMLDivElement;
11 |
12 | faqPermissionHeading.addEventListener("click", () => {
13 | faqPermissionSection.classList.toggle("active");
14 | faqContactSection.classList.toggle("active", false);
15 | faqPrivacySection.classList.toggle("active", false);
16 | faqTroubleshootingSection.classList.toggle("active", false);
17 | });
18 | faqContactHeading.addEventListener("click", () => {
19 | faqPermissionSection.classList.toggle("active", false);
20 | faqContactSection.classList.toggle("active");
21 | faqPrivacySection.classList.toggle("active", false);
22 | faqTroubleshootingSection.classList.toggle("active", false);
23 | });
24 | faqPrivacyHeading.addEventListener("click", () => {
25 | faqPermissionSection.classList.toggle("active", false);
26 | faqContactSection.classList.toggle("active", false);
27 | faqPrivacySection.classList.toggle("active");
28 | faqTroubleshootingSection.classList.toggle("active", false);
29 | });
30 | faqTroubleshootingHeading.addEventListener("click", () => {
31 | faqPermissionSection.classList.toggle("active", false);
32 | faqContactSection.classList.toggle("active", false);
33 | faqPrivacySection.classList.toggle("active", false);
34 | faqTroubleshootingSection.classList.toggle("active");
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/ui/settings/scripts/helper.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Limits a value to a range between a minimum and a maximum value.
3 | * @param min The minimum value.
4 | * @param max The maximum value.
5 | * @param value The value to be clamped.
6 | * @returns
7 | */
8 | export function clamp(min: number, max: number, value: number): number {
9 | return Math.max(min, Math.min(max, value));
10 | }
11 |
--------------------------------------------------------------------------------
/ui/settings/scripts/importExport.ts:
--------------------------------------------------------------------------------
1 | import { CommunicationRole, MessageType, SettingsDesign, SettingsState } from "./enums.js";
2 | import { clamp } from "./helper.js";
3 | import { loadDataFromStorage, setSettingsState } from "./index.js";
4 | import { StorageChangedMessage } from "./interfaces/interfaces.js";
5 | import { CombinedStorageObject, OldStorageObject } from "./interfaces/storage.js";
6 | import { loadSettingsDataFromStorage } from "./settings.js";
7 |
8 | const STORAGE_VERSION = "1.0";
9 |
10 | /**
11 | * Initialize the import and export settings section.
12 | */
13 | export function initImportExport() {
14 | const importBtn = document.getElementById("import-btn") as HTMLButtonElement;
15 | const exportBtn = document.getElementById("export-btn") as HTMLButtonElement;
16 | const fileLoaderInput = document.getElementById("file-loader-input") as HTMLInputElement;
17 |
18 | fileLoaderInput.addEventListener("change", (event) => {
19 | let file = fileLoaderInput?.files?.item(0);
20 | if (file) {
21 | const fileReader = new FileReader();
22 | fileReader.addEventListener(
23 | "load",
24 | () => {
25 | console.log("FileReader loaded");
26 |
27 | try {
28 | const fileContent = fileReader.result;
29 | if (typeof fileContent === "string") {
30 | const fileContentJson = JSON.parse(fileContent);
31 | if (fileContentJson.version === undefined) {
32 | loadOldFormat(fileContentJson);
33 | } else {
34 | loadNewFormat(fileContentJson);
35 | }
36 | sendStorageChangeMsg();
37 | } else {
38 | throw new Error("Could not read file.");
39 | }
40 | } catch (error) {
41 | console.error(`Error: ${error}`);
42 |
43 | alert(`Error: ${error}`);
44 | }
45 | },
46 | false
47 | );
48 | fileReader.readAsText(file, "UTF-8");
49 | }
50 | });
51 |
52 | importBtn.addEventListener(
53 | "click",
54 | () => {
55 | // Check if the file-APIs are supported.
56 | if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
57 | // The file-APIs are not supported.
58 | alert("The file-APIs are not supported. You are not able to import.");
59 | return;
60 | }
61 | fileLoaderInput.click();
62 | },
63 | false
64 | );
65 |
66 | exportBtn.addEventListener("click", () => {
67 | const defaultCombinedStorageObject: CombinedStorageObject = {
68 | version: STORAGE_VERSION,
69 | settings: {
70 | design: SettingsDesign.DETECT,
71 | advancedView: false,
72 | openPopup: false,
73 | buttonVisible: true,
74 | buttonColor: "#717171",
75 | buttonSize: 142,
76 | animationSpeed: 200,
77 | },
78 | blockedChannels: [],
79 | excludedChannels: [],
80 | blockedVideoTitles: {},
81 | blockedChannelsRegExp: {},
82 | blockedComments: {},
83 | };
84 |
85 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => {
86 | const combinedStorageObject = result as CombinedStorageObject;
87 | const currentDate = new Date();
88 | download(
89 | JSON.stringify(combinedStorageObject),
90 | `ChannelBlocker ${currentDate.getFullYear()}-${currentDate.getMonth() + 1}-${currentDate.getDate()}.save`,
91 | ".save"
92 | );
93 | });
94 | });
95 | }
96 |
97 | /**
98 | * Load the date from the old format and add them to currently saved data.
99 | * @param oldStorageObject The loaded JSON data.
100 | */
101 | function loadOldFormat(oldStorageObject: OldStorageObject) {
102 | const defaultCombinedStorageObject: CombinedStorageObject = {
103 | version: STORAGE_VERSION,
104 | settings: {
105 | design: SettingsDesign.DETECT,
106 | advancedView: false,
107 | openPopup: false,
108 | buttonVisible: true,
109 | buttonColor: "#717171",
110 | buttonSize: 142,
111 | animationSpeed: 200,
112 | },
113 | blockedChannels: [],
114 | excludedChannels: [],
115 | blockedVideoTitles: {},
116 | blockedChannelsRegExp: {},
117 | blockedComments: {},
118 | };
119 |
120 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => {
121 | const combinedStorageObject = result as CombinedStorageObject;
122 |
123 | // Load blocking rules
124 | if (oldStorageObject[0] !== undefined) {
125 | combinedStorageObject.blockedChannels.push(...Object.keys(oldStorageObject[0]));
126 | }
127 | if (oldStorageObject[1] !== undefined) {
128 | for (const key in oldStorageObject[1]) {
129 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[1], key)) {
130 | combinedStorageObject.blockedVideoTitles[key] = oldStorageObject[1][key] === 0 ? "i" : "";
131 | }
132 | }
133 | }
134 | if (oldStorageObject[2] !== undefined) {
135 | for (const key in oldStorageObject[2]) {
136 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[2], key)) {
137 | combinedStorageObject.blockedChannelsRegExp[key] = oldStorageObject[2][key] === 0 ? "i" : "";
138 | }
139 | }
140 | }
141 | if (oldStorageObject[3] !== undefined) {
142 | for (const key in oldStorageObject[3]) {
143 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[3], key)) {
144 | combinedStorageObject.blockedComments[key] = oldStorageObject[3][key] === 0 ? "i" : "";
145 | }
146 | }
147 | }
148 | if (oldStorageObject[4] !== undefined) {
149 | combinedStorageObject.excludedChannels.push(...Object.keys(oldStorageObject[4]));
150 | }
151 |
152 | // Load settings
153 | if (oldStorageObject?.settings_ui?.[0] !== undefined) {
154 | // The old format only had two designs. Dark: 0 and Light: 1.
155 | // Currently Device: 0 is the default, therefore adding 1 adjusts this.
156 | combinedStorageObject.settings.design = clamp(0, 2, oldStorageObject.settings_ui[0] + 1);
157 | }
158 | if (oldStorageObject?.settings_ui?.[1] !== undefined) {
159 | // No longer in use
160 | combinedStorageObject.settings.advancedView = oldStorageObject.settings_ui[1];
161 | }
162 | if (oldStorageObject?.settings_ui?.[2] !== undefined) {
163 | combinedStorageObject.settings.openPopup = oldStorageObject.settings_ui[2];
164 | }
165 | if (oldStorageObject?.content_ui?.[0] !== undefined) {
166 | combinedStorageObject.settings.buttonVisible = oldStorageObject.content_ui[0];
167 | }
168 | if (oldStorageObject?.content_ui?.[1] !== undefined) {
169 | combinedStorageObject.settings.buttonColor = oldStorageObject.content_ui[1];
170 | }
171 | if (oldStorageObject?.content_ui?.[2] !== undefined) {
172 | // The old default was 106, but in the new implementation this is pretty small so add 36 to adjust.
173 | combinedStorageObject.settings.buttonSize = clamp(100, 200, oldStorageObject.content_ui[2] + 36);
174 | }
175 | if (oldStorageObject?.content_ui?.[3] !== undefined) {
176 | combinedStorageObject.settings.animationSpeed = clamp(100, 200, oldStorageObject.content_ui[3]);
177 | }
178 |
179 | // Save data
180 | chrome.storage.local.set(combinedStorageObject);
181 | setSettingsState(SettingsState.BLOCKED_CHANNELS);
182 | loadDataFromStorage();
183 | loadSettingsDataFromStorage();
184 | });
185 | }
186 |
187 | /**
188 | * Load the date from the new format and add them to currently saved data.
189 | * @param oldStorageObject The loaded JSON data.
190 | */
191 | function loadNewFormat(loadedStorageObject: CombinedStorageObject) {
192 | const defaultCombinedStorageObject: CombinedStorageObject = {
193 | version: STORAGE_VERSION,
194 | settings: {
195 | design: SettingsDesign.DETECT,
196 | advancedView: false,
197 | openPopup: false,
198 | buttonVisible: true,
199 | buttonColor: "#717171",
200 | buttonSize: 142,
201 | animationSpeed: 200,
202 | },
203 | blockedChannels: [],
204 | excludedChannels: [],
205 | blockedVideoTitles: {},
206 | blockedChannelsRegExp: {},
207 | blockedComments: {},
208 | };
209 |
210 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => {
211 | const storageObject = result as CombinedStorageObject;
212 |
213 | console.log("NEW FORMAT", storageObject);
214 | console.log("NEW FORMAT", loadedStorageObject);
215 | console.log(
216 | "filter",
217 | loadedStorageObject.blockedChannels.filter((channel) => {
218 | return !storageObject.blockedChannels.includes(channel);
219 | })
220 | );
221 |
222 | storageObject.blockedChannels.push(
223 | ...loadedStorageObject.blockedChannels.filter((channel) => {
224 | return !storageObject.blockedChannels.includes(channel);
225 | })
226 | );
227 | storageObject.blockedVideoTitles = { ...storageObject.blockedVideoTitles, ...loadedStorageObject.blockedVideoTitles };
228 | storageObject.blockedChannelsRegExp = { ...storageObject.blockedChannelsRegExp, ...loadedStorageObject.blockedChannelsRegExp };
229 | storageObject.blockedComments = { ...storageObject.blockedComments, ...loadedStorageObject.blockedComments };
230 | storageObject.excludedChannels.push(
231 | ...loadedStorageObject.excludedChannels.filter((channel) => {
232 | return !storageObject.excludedChannels.includes(channel);
233 | })
234 | );
235 | storageObject.settings = { ...storageObject.settings, ...loadedStorageObject.settings };
236 |
237 | // Save data
238 | chrome.storage.local.set(storageObject);
239 | setSettingsState(SettingsState.BLOCKED_CHANNELS);
240 | loadDataFromStorage();
241 | loadSettingsDataFromStorage();
242 | });
243 | }
244 |
245 | /**
246 | * Creates a file with the given data and downloads it.
247 | * @param data The content of the file.
248 | * @param filename The name of the file.
249 | * @param type The type of the file.
250 | */
251 | function download(data: BlobPart, filename: string, type: string) {
252 | const file = new Blob([data], { type: type });
253 | const a = document.createElement("a");
254 | const url = URL.createObjectURL(file);
255 |
256 | a.href = url;
257 | a.download = filename;
258 | document.body.appendChild(a);
259 | a.click();
260 | setTimeout(() => {
261 | document.body.removeChild(a);
262 | window.URL.revokeObjectURL(url);
263 | }, 0);
264 | }
265 |
266 | /**
267 | * Send a message to the service worker, that informs them that the storage was changed
268 | */
269 | function sendStorageChangeMsg() {
270 | const message: StorageChangedMessage = {
271 | sender: CommunicationRole.SETTINGS,
272 | receiver: CommunicationRole.SERVICE_WORKER,
273 | type: MessageType.STORAGE_CHANGED,
274 | content: undefined,
275 | };
276 | chrome.runtime.sendMessage(message);
277 | console.log("sendStorageChangeMsg");
278 | }
279 |
--------------------------------------------------------------------------------
/ui/settings/scripts/index.ts:
--------------------------------------------------------------------------------
1 | import { CommunicationRole, MessageType, SettingsState } from "./enums.js";
2 | import { AddBlockingRuleMessage, Message, RemoveBlockingRuleMessage } from "./interfaces/interfaces.js";
3 | import { KeyValueMap, StorageObject } from "./interfaces/storage.js";
4 | import { initFaq } from "./faq.js";
5 | import { initNavigation } from "./navigation.js";
6 | import { initAppearanceUI } from "./settings.js";
7 | import { initImportExport } from "./importExport.js";
8 | import { initDonation } from "./donate.js";
9 |
10 | const blockedChannelsSelect: HTMLSelectElement = document.getElementById("blocked-channels") as HTMLSelectElement;
11 | const blockedChannelsInput: HTMLInputElement = document.getElementById("blocked-channels-input") as HTMLInputElement;
12 | const blockedChannelsAddBtn: HTMLButtonElement = document.getElementById("blocked-channels-add-btn") as HTMLButtonElement;
13 | const blockedChannelsRemoveBtn: HTMLButtonElement = document.getElementById("blocked-channels-remove-btn") as HTMLButtonElement;
14 | const caseInsensitiveRow = document.getElementById("case-insensitive-row") as HTMLDivElement;
15 | const caseInsensitiveCheckbox = document.getElementById("case-insensitive-checkbox") as HTMLInputElement;
16 | const nav = document.getElementById("main-nav") as HTMLElement;
17 |
18 | const headingElement: HTMLHeadingElement = document.getElementById("heading") as HTMLHeadingElement;
19 | const rulesSection: HTMLDivElement = document.getElementById("rules-section") as HTMLDivElement;
20 | const appearanceSection: HTMLDivElement = document.getElementById("appearance-section") as HTMLDivElement;
21 | const importExportSection: HTMLDivElement = document.getElementById("import-export-section") as HTMLDivElement;
22 | const aboutSection: HTMLDivElement = document.getElementById("about-section") as HTMLDivElement;
23 | const faqSection: HTMLDivElement = document.getElementById("faq-section") as HTMLDivElement;
24 |
25 | const blockedChannelsNav: HTMLLIElement = document.getElementById("blocked-channels-nav") as HTMLLIElement;
26 | const blockedTitlesNav: HTMLLIElement = document.getElementById("blocked-titles-nav") as HTMLLIElement;
27 | const blockedNamesNav: HTMLLIElement = document.getElementById("blocked-names-nav") as HTMLLIElement;
28 | const blockedCommentsNav: HTMLLIElement = document.getElementById("blocked-comments-nav") as HTMLLIElement;
29 | const excludedChannelsNav: HTMLLIElement = document.getElementById("excluded-channels-nav") as HTMLLIElement;
30 | const appearanceNav: HTMLLIElement = document.getElementById("appearance-nav") as HTMLLIElement;
31 | const importExportNav: HTMLLIElement = document.getElementById("import-export-nav") as HTMLLIElement;
32 | const aboutNav: HTMLLIElement = document.getElementById("about-nav") as HTMLLIElement;
33 | const faqNav: HTMLLIElement = document.getElementById("faq-nav") as HTMLLIElement;
34 |
35 | const STORAGE_VERSION = "1.0";
36 |
37 | let settingsState: SettingsState = SettingsState.BLOCKED_CHANNELS;
38 |
39 | let defaultStorage: StorageObject = {
40 | version: STORAGE_VERSION,
41 | blockedChannels: [],
42 | blockedChannelsRegExp: {},
43 | blockedComments: {},
44 | blockedVideoTitles: {},
45 | excludedChannels: [],
46 | };
47 |
48 | let blockedChannelsSet = new Set
();
49 | let excludedChannels = new Set();
50 |
51 | let blockedChannelsRegExp: KeyValueMap = {};
52 | let blockedComments: KeyValueMap = {};
53 | let blockedVideoTitles: KeyValueMap = {};
54 |
55 | loadDataFromStorage();
56 |
57 | export function loadDataFromStorage() {
58 | chrome.storage.local.get(defaultStorage).then((result) => {
59 | const storageObject = result as StorageObject;
60 | console.log("Loaded stored data", storageObject);
61 |
62 | blockedChannelsSet = new Set();
63 | excludedChannels = new Set();
64 |
65 | blockedChannelsRegExp = {};
66 | blockedComments = {};
67 | blockedVideoTitles = {};
68 |
69 | if (storageObject.version === "0") {
70 | // This should not happen, because the service worker is converting the old storage / filling it with default values.
71 | } else {
72 | for (let index = 0; index < storageObject.blockedChannels.length; index++) {
73 | blockedChannelsSet.add(storageObject.blockedChannels[index]);
74 | }
75 | for (let index = 0; index < storageObject.excludedChannels.length; index++) {
76 | excludedChannels.add(storageObject.excludedChannels[index]);
77 | }
78 |
79 | blockedChannelsRegExp = storageObject.blockedChannelsRegExp;
80 | blockedComments = storageObject.blockedComments;
81 | blockedVideoTitles = storageObject.blockedVideoTitles;
82 | }
83 |
84 | updateUI();
85 | });
86 | }
87 |
88 | export function setSettingsState(pSettingsState: SettingsState) {
89 | settingsState = pSettingsState;
90 | blockedChannelsRemoveBtn.classList.add("outlined");
91 | }
92 |
93 | function updateUI() {
94 | nav.classList.remove("open");
95 | const showRulesSection =
96 | settingsState === SettingsState.BLOCKED_CHANNELS ||
97 | settingsState === SettingsState.BLOCKED_TITLES ||
98 | settingsState === SettingsState.BLOCKED_NAMES ||
99 | settingsState === SettingsState.BLOCKED_COMMENTS ||
100 | settingsState === SettingsState.EXCLUDED_CHANNELS;
101 | rulesSection.style.display = showRulesSection ? "" : "none";
102 | blockedChannelsNav.classList.remove("active");
103 | blockedTitlesNav.classList.remove("active");
104 | blockedNamesNav.classList.remove("active");
105 | blockedCommentsNav.classList.remove("active");
106 | excludedChannelsNav.classList.remove("active");
107 | if (showRulesSection) {
108 | updateRulesUI();
109 | }
110 |
111 | const showAppearanceSection = settingsState === SettingsState.APPEARANCE;
112 | appearanceSection.style.display = showAppearanceSection ? "" : "none";
113 | appearanceNav.classList.toggle("active", showAppearanceSection);
114 |
115 | const showImportExportSection = settingsState === SettingsState.IMPORT_EXPORT;
116 | importExportSection.style.display = showImportExportSection ? "" : "none";
117 | importExportNav.classList.toggle("active", showImportExportSection);
118 |
119 | const showAboutSection = settingsState === SettingsState.ABOUT;
120 | aboutSection.style.display = showAboutSection ? "" : "none";
121 | aboutNav.classList.toggle("active", showAboutSection);
122 |
123 | const showFaqSection = settingsState === SettingsState.FAQ;
124 | faqSection.style.display = showFaqSection ? "" : "none";
125 | faqNav.classList.toggle("active", showFaqSection);
126 | }
127 |
128 | function updateRulesUI() {
129 | while (blockedChannelsSelect.firstChild !== null) {
130 | blockedChannelsSelect.removeChild(blockedChannelsSelect.firstChild);
131 | }
132 |
133 | switch (settingsState) {
134 | case SettingsState.BLOCKED_CHANNELS:
135 | blockedChannelsNav.classList.add("active");
136 | caseInsensitiveRow.style.display = "none";
137 | headingElement.innerText = "Blocked Users/Channels";
138 | blockedChannelsInput.placeholder = "User/Channel Name";
139 | blockedChannelsSet.forEach((channelName) => {
140 | insertOption(channelName);
141 | });
142 | break;
143 | case SettingsState.BLOCKED_TITLES:
144 | blockedTitlesNav.classList.add("active");
145 | caseInsensitiveRow.style.display = "";
146 | headingElement.innerText = "Blocked Video Titles by Regular Expressions";
147 | blockedChannelsInput.placeholder = "Video Title Regular Expression";
148 | for (const key in blockedVideoTitles) {
149 | if (Object.prototype.hasOwnProperty.call(blockedVideoTitles, key)) {
150 | insertOption(key, blockedVideoTitles[key] !== "");
151 | }
152 | }
153 | break;
154 | case SettingsState.BLOCKED_NAMES:
155 | blockedNamesNav.classList.add("active");
156 | caseInsensitiveRow.style.display = "";
157 | headingElement.innerText = "Blocked User/Channel Names by Regular Expressions";
158 | blockedChannelsInput.placeholder = "User/Channel Name Regular Expression";
159 | for (const key in blockedChannelsRegExp) {
160 | if (Object.prototype.hasOwnProperty.call(blockedChannelsRegExp, key)) {
161 | insertOption(key, blockedChannelsRegExp[key] !== "");
162 | }
163 | }
164 | break;
165 | case SettingsState.BLOCKED_COMMENTS:
166 | blockedCommentsNav.classList.add("active");
167 | caseInsensitiveRow.style.display = "";
168 | headingElement.innerText = "Blocked Comments by Regular Expressions";
169 | blockedChannelsInput.placeholder = "Comment Regular Expression";
170 | for (const key in blockedComments) {
171 | if (Object.prototype.hasOwnProperty.call(blockedComments, key)) {
172 | insertOption(key, blockedComments[key] !== "");
173 | }
174 | }
175 | break;
176 | case SettingsState.EXCLUDED_CHANNELS:
177 | excludedChannelsNav.classList.add("active");
178 | caseInsensitiveRow.style.display = "none";
179 | headingElement.innerText = "Excluded Users/Channels from Regular Expressions";
180 | blockedChannelsInput.placeholder = "User/Channel Name";
181 | excludedChannels.forEach((channelName) => {
182 | insertOption(channelName);
183 | });
184 | break;
185 | }
186 |
187 | blockedChannelsSelect.classList.toggle("larger", blockedChannelsSelect.childElementCount > 4);
188 | blockedChannelsSelect.classList.toggle("largest", blockedChannelsSelect.childElementCount > 8);
189 | }
190 |
191 | function insertOption(value: string, isCaseInsensitive: boolean = false) {
192 | let option = document.createElement("option");
193 | option.value = value;
194 | option.innerText = value;
195 | option.classList.toggle("case-insensitive", isCaseInsensitive);
196 | blockedChannelsSelect.insertAdjacentElement("afterbegin", option);
197 | }
198 |
199 | function addNewRule() {
200 | const rule = blockedChannelsInput.value;
201 | if (rule.trim().length === 0) return;
202 |
203 | const message: AddBlockingRuleMessage = {
204 | sender: CommunicationRole.SETTINGS,
205 | receiver: CommunicationRole.SERVICE_WORKER,
206 | type: MessageType.ADD_BLOCKING_RULE,
207 | content: {
208 | blockedChannel: settingsState === SettingsState.BLOCKED_CHANNELS ? rule : undefined,
209 | blockingVideoTitleRegExp: settingsState === SettingsState.BLOCKED_TITLES ? rule : undefined,
210 | blockingChannelRegExp: settingsState === SettingsState.BLOCKED_NAMES ? rule : undefined,
211 | blockingCommentRegExp: settingsState === SettingsState.BLOCKED_COMMENTS ? rule : undefined,
212 | excludedChannel: settingsState === SettingsState.EXCLUDED_CHANNELS ? rule : undefined,
213 | caseInsensitive: caseInsensitiveCheckbox.checked,
214 | },
215 | };
216 | chrome.runtime.sendMessage(message);
217 | blockedChannelsInput.value = "";
218 | }
219 | function removeRule() {
220 | let selectedOptions = [];
221 | for (let index = 0; index < blockedChannelsSelect.options.length; index++) {
222 | const option = blockedChannelsSelect.options[index];
223 | if (option.selected) selectedOptions.push(option.value);
224 | }
225 | const message: RemoveBlockingRuleMessage = {
226 | sender: CommunicationRole.SETTINGS,
227 | receiver: CommunicationRole.SERVICE_WORKER,
228 | type: MessageType.REMOVE_BLOCKING_RULE,
229 | content: {
230 | blockedChannel: settingsState === SettingsState.BLOCKED_CHANNELS ? selectedOptions : undefined,
231 | blockingVideoTitleRegExp: settingsState === SettingsState.BLOCKED_TITLES ? selectedOptions : undefined,
232 | blockingChannelRegExp: settingsState === SettingsState.BLOCKED_NAMES ? selectedOptions : undefined,
233 | blockingCommentRegExp: settingsState === SettingsState.BLOCKED_COMMENTS ? selectedOptions : undefined,
234 | excludedChannel: settingsState === SettingsState.EXCLUDED_CHANNELS ? selectedOptions : undefined,
235 | },
236 | };
237 | chrome.runtime.sendMessage(message);
238 | }
239 |
240 | (function initUI() {
241 | initFaq();
242 | initNavigation();
243 | initAppearanceUI();
244 | initImportExport();
245 | initDonation();
246 |
247 | blockedChannelsSelect.addEventListener("change", (event) => {
248 | blockedChannelsRemoveBtn.classList.toggle("outlined", blockedChannelsSelect.value === "");
249 | });
250 |
251 | blockedChannelsAddBtn.addEventListener("click", addNewRule);
252 | blockedChannelsInput.addEventListener("keydown", (event) => {
253 | if (event.key == "Enter") addNewRule();
254 | });
255 |
256 | blockedChannelsRemoveBtn.addEventListener("click", removeRule);
257 |
258 | blockedChannelsNav.addEventListener("click", () => {
259 | setSettingsState(SettingsState.BLOCKED_CHANNELS);
260 | updateUI();
261 | });
262 | blockedTitlesNav.addEventListener("click", () => {
263 | setSettingsState(SettingsState.BLOCKED_TITLES);
264 | updateUI();
265 | });
266 | blockedNamesNav.addEventListener("click", () => {
267 | setSettingsState(SettingsState.BLOCKED_NAMES);
268 | updateUI();
269 | });
270 | blockedCommentsNav.addEventListener("click", () => {
271 | setSettingsState(SettingsState.BLOCKED_COMMENTS);
272 | updateUI();
273 | });
274 | excludedChannelsNav.addEventListener("click", () => {
275 | setSettingsState(SettingsState.EXCLUDED_CHANNELS);
276 | updateUI();
277 | });
278 | appearanceNav.addEventListener("click", () => {
279 | setSettingsState(SettingsState.APPEARANCE);
280 | updateUI();
281 | });
282 | importExportNav.addEventListener("click", () => {
283 | setSettingsState(SettingsState.IMPORT_EXPORT);
284 | updateUI();
285 | });
286 | aboutNav.addEventListener("click", () => {
287 | setSettingsState(SettingsState.ABOUT);
288 | updateUI();
289 | });
290 | faqNav.addEventListener("click", () => {
291 | setSettingsState(SettingsState.FAQ);
292 | updateUI();
293 | });
294 | })();
295 |
296 | chrome.runtime.onMessage.addListener((message: Message, sender: chrome.runtime.MessageSender) => {
297 | if (message.receiver !== CommunicationRole.SETTINGS) return;
298 |
299 | switch (message.type) {
300 | case MessageType.STORAGE_CHANGED:
301 | loadDataFromStorage();
302 | break;
303 |
304 | default:
305 | break;
306 | }
307 | });
308 |
--------------------------------------------------------------------------------
/ui/settings/scripts/interfaces/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { CommunicationRole, MessageType } from "../enums.js";
2 |
3 | export interface Message {
4 | sender: CommunicationRole;
5 | receiver: CommunicationRole;
6 | type: MessageType;
7 | content: any;
8 | }
9 |
10 | export interface AddBlockingRuleMessage extends Message {
11 | content: {
12 | blockedChannel?: string;
13 | excludedChannel?: string;
14 | blockingChannelRegExp?: string;
15 | blockingCommentRegExp?: string;
16 | blockingVideoTitleRegExp?: string;
17 | caseInsensitive?: boolean;
18 | };
19 | }
20 |
21 | export interface RemoveBlockingRuleMessage extends Message {
22 | content: {
23 | blockedChannel?: string[];
24 | excludedChannel?: string[];
25 | blockingChannelRegExp?: string[];
26 | blockingCommentRegExp?: string[];
27 | blockingVideoTitleRegExp?: string[];
28 | };
29 | }
30 |
31 | export interface RequestSettingsMessage extends Message {
32 | content: undefined;
33 | }
34 |
35 | export interface SettingsChangedMessage extends Message {
36 | content: {
37 | buttonVisible: boolean;
38 | buttonColor: string;
39 | buttonSize: number;
40 | animationSpeed: number;
41 | };
42 | }
43 |
44 | export interface IsBlockedMessage extends Message {
45 | content: {
46 | videoTitle?: string;
47 | userChannelName?: string;
48 | commentContent?: string;
49 | };
50 | }
51 |
52 | export interface StorageChangedMessage extends Message {
53 | content: undefined;
54 | }
55 |
--------------------------------------------------------------------------------
/ui/settings/scripts/interfaces/storage.ts:
--------------------------------------------------------------------------------
1 | import { SettingsDesign } from "../enums.js";
2 |
3 | export interface OldStorageObject {
4 | "0"?: { [key: string]: number }; // blocked channels
5 | "1"?: { [key: string]: number }; // video title RegExp
6 | "2"?: { [key: string]: number }; // channel name RegExp
7 | "3"?: { [key: string]: number }; // comment RegExp
8 | "4"?: { [key: string]: number }; // excluded channels
9 | content_ui: {
10 | "0": boolean; // button_visible
11 | "1": string; // button_color
12 | "2": number; // button_size
13 | "3": number; // animation_speed
14 | };
15 | settings_ui: {
16 | "0": number; // design
17 | "1": boolean; // advanced_view
18 | "2": boolean; // open_popup
19 | };
20 | }
21 |
22 | export interface KeyValueMap {
23 | [key: string]: string;
24 | }
25 |
26 | export interface StorageObject {
27 | version: string;
28 |
29 | blockedChannels: string[];
30 | excludedChannels: string[];
31 |
32 | blockedVideoTitles: KeyValueMap;
33 | blockedChannelsRegExp: KeyValueMap;
34 | blockedComments: KeyValueMap;
35 | }
36 |
37 | export interface SettingsStorageObject {
38 | version: string;
39 |
40 | settings: {
41 | design: SettingsDesign;
42 | advancedView: boolean;
43 | openPopup: boolean;
44 | buttonVisible: boolean;
45 | buttonColor: string;
46 | buttonSize: number;
47 | animationSpeed: number;
48 | };
49 | }
50 |
51 | export interface CombinedStorageObject extends SettingsStorageObject, StorageObject {}
52 |
--------------------------------------------------------------------------------
/ui/settings/scripts/navigation.ts:
--------------------------------------------------------------------------------
1 | export function initNavigation() {
2 | const burgerMenu = document.getElementById("burger-menu") as HTMLButtonElement;
3 | const nav = document.getElementById("main-nav") as HTMLElement;
4 |
5 | console.log("burgerMenu", burgerMenu);
6 |
7 | burgerMenu.addEventListener("click", () => {
8 | nav.classList.toggle("open");
9 | console.log("toggle nav");
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/ui/settings/scripts/settings.ts:
--------------------------------------------------------------------------------
1 | import { CommunicationRole, MessageType, SettingsDesign } from "./enums.js";
2 | import { SettingsChangedMessage } from "./interfaces/interfaces.js";
3 | import { SettingsStorageObject } from "./interfaces/storage.js";
4 |
5 | const modeDropdown = document.getElementById("mode-dropdown") as HTMLSelectElement;
6 | const btnColorInput = document.getElementById("block-btn-color-picker") as HTMLInputElement;
7 | const btnSizeSlider = document.getElementById("btn-size-slider") as HTMLInputElement;
8 | const popupCheckbox = document.getElementById("popup-checkbox") as HTMLInputElement;
9 | const showBtnCheckbox = document.getElementById("show-btn-checkbox") as HTMLInputElement;
10 | const animationSpeedSlider = document.getElementById("animation-speed-slider") as HTMLInputElement;
11 | const resetBtn = document.getElementById("reset-appearance-btn") as HTMLButtonElement;
12 |
13 | let defaultStorage: SettingsStorageObject = {
14 | version: "0",
15 | settings: {
16 | design: SettingsDesign.DETECT,
17 | advancedView: false,
18 | openPopup: false,
19 | buttonVisible: true,
20 | buttonColor: "#717171",
21 | buttonSize: 142,
22 | animationSpeed: 200,
23 | },
24 | };
25 | let settings = { ...defaultStorage.settings };
26 |
27 | loadSettingsDataFromStorage();
28 |
29 | export function loadSettingsDataFromStorage() {
30 | chrome.storage.local.get(defaultStorage).then((result) => {
31 | const storageObject = result as SettingsStorageObject;
32 | console.log("Loaded stored data", storageObject);
33 |
34 | if (storageObject.version === "0") {
35 | // Should not be possible because the service worker converts / fill the storage
36 | } else {
37 | settings = storageObject.settings;
38 | }
39 | updateUI();
40 | });
41 | }
42 |
43 | function updateUI() {
44 | updateColorScheme();
45 | updateBtnColor();
46 | updateBtnSize();
47 | updatePopup();
48 | updateShowBtn();
49 | updateAnimationSpeed();
50 | }
51 |
52 | function updateColorScheme() {
53 | document.body.classList.toggle("detect-scheme", settings.design === SettingsDesign.DETECT);
54 | document.body.classList.toggle("dark-scheme", settings.design === SettingsDesign.DARK);
55 | modeDropdown.value = `${settings.design}`;
56 | }
57 |
58 | function updateBtnColor() {
59 | btnColorInput.value = settings.buttonColor;
60 | }
61 |
62 | function updateBtnSize() {
63 | btnSizeSlider.value = `${settings.buttonSize}`;
64 | }
65 |
66 | function updatePopup() {
67 | popupCheckbox.checked = settings.openPopup;
68 | }
69 |
70 | function updateShowBtn() {
71 | showBtnCheckbox.checked = settings.buttonVisible;
72 | }
73 |
74 | function updateAnimationSpeed() {
75 | animationSpeedSlider.value = `${settings.animationSpeed}`;
76 | }
77 |
78 | export function initAppearanceUI() {
79 | modeDropdown.addEventListener("change", () => {
80 | settings.design = Number(modeDropdown.value);
81 | chrome.storage.local.set({ settings });
82 | updateColorScheme();
83 | });
84 |
85 | btnColorInput.addEventListener("change", () => {
86 | settings.buttonColor = btnColorInput.value;
87 | chrome.storage.local.set({ settings });
88 | updateBtnColor();
89 | sendSettingChangedMessage();
90 | });
91 |
92 | btnSizeSlider.addEventListener("change", () => {
93 | settings.buttonSize = Number(btnSizeSlider.value);
94 | chrome.storage.local.set({ settings });
95 | updateBtnSize();
96 | sendSettingChangedMessage();
97 | });
98 |
99 | popupCheckbox.addEventListener("change", () => {
100 | settings.openPopup = popupCheckbox.checked;
101 | chrome.storage.local.set({ settings });
102 | updatePopup();
103 | });
104 |
105 | showBtnCheckbox.addEventListener("change", () => {
106 | settings.buttonVisible = showBtnCheckbox.checked;
107 | chrome.storage.local.set({ settings });
108 | updateShowBtn();
109 | sendSettingChangedMessage();
110 | });
111 |
112 | animationSpeedSlider.addEventListener("change", () => {
113 | settings.animationSpeed = Number(animationSpeedSlider.value);
114 | chrome.storage.local.set({ settings });
115 | updateAnimationSpeed();
116 | sendSettingChangedMessage();
117 | });
118 |
119 | resetBtn.addEventListener("click", () => {
120 | settings = { ...defaultStorage.settings };
121 | chrome.storage.local.set({ settings });
122 | updateUI();
123 | sendSettingChangedMessage();
124 | });
125 | }
126 |
127 | function sendSettingChangedMessage() {
128 | const message: SettingsChangedMessage = {
129 | sender: CommunicationRole.SETTINGS,
130 | receiver: CommunicationRole.SERVICE_WORKER,
131 | type: MessageType.SETTINGS_CHANGED,
132 | content: {
133 | buttonVisible: settings.buttonVisible,
134 | buttonColor: settings.buttonColor,
135 | buttonSize: settings.buttonSize,
136 | animationSpeed: settings.animationSpeed,
137 | },
138 | };
139 | chrome.runtime.sendMessage(message);
140 | }
141 |
--------------------------------------------------------------------------------
/ui/settings/style/main.scss:
--------------------------------------------------------------------------------
1 | @import "modules/variables";
2 | @import "modules/index";
3 | @import "modules/sections";
4 | @import "modules/navigation";
5 | @import "modules/header";
6 | @import "modules/switch";
7 | @import "modules/donate";
8 | @import "modules/faq";
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | margin: 0;
14 | padding: 0;
15 | box-sizing: border-box;
16 | }
17 |
18 | body {
19 | width: 100%;
20 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
21 | }
22 |
23 | svg:not([fill]) {
24 | fill: currentColor;
25 | }
26 |
27 | hr {
28 | color: currentColor;
29 | width: 100%;
30 | border: inherit;
31 | border-bottom: none;
32 | }
33 |
34 | [aria-busy="true"] {
35 | cursor: progress;
36 | }
37 |
38 | [aria-controls],
39 | button {
40 | cursor: pointer;
41 | }
42 |
43 | [aria-disabled="true"],
44 | [disabled] {
45 | cursor: default;
46 | }
47 |
48 | @media (prefers-reduced-motion: reduce) {
49 | *,
50 | ::before,
51 | ::after {
52 | animation-delay: -1ms !important;
53 | animation-duration: 1ms !important;
54 | animation-iteration-count: 1 !important;
55 | background-attachment: initial !important;
56 | scroll-behavior: auto !important;
57 | transition-delay: 0s !important;
58 | transition-duration: 0s !important;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_donate.scss:
--------------------------------------------------------------------------------
1 | #donation-dialog {
2 | width: 400px;
3 |
4 | background-color: var(--surface-color);
5 | border-radius: 8px;
6 | border: none;
7 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
8 | box-shadow: color-mix(in srgb, rgb(0, 0, 0) 30%, transparent) 0 1px 2px 0,
9 | color-mix(in srgb, rgb(0, 0, 0) 15%, transparent) 0 2px 6px 2px;
10 |
11 | padding: 0;
12 | margin: 100px auto;
13 | color: var(--font-color);
14 |
15 | .head {
16 | width: 100%;
17 | display: flex;
18 | flex-direction: row;
19 | justify-content: center;
20 | align-items: center;
21 |
22 | h1 {
23 | margin: 8px 16px;
24 | flex: 1;
25 |
26 | font-size: 1.25rem;
27 | font-weight: 500;
28 | }
29 |
30 | button {
31 | min-width: auto;
32 | background: none;
33 | padding: 0 16px;
34 | color: var(--font-color);
35 | }
36 | }
37 |
38 | .main-section {
39 | display: flex;
40 | flex-direction: column;
41 | justify-content: space-around;
42 | align-items: center;
43 |
44 | width: 100%;
45 | flex: 1;
46 | padding-bottom: 16px;
47 |
48 | button {
49 | padding: 8px 16px;
50 | margin-top: 16px;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_faq.scss:
--------------------------------------------------------------------------------
1 | #faq-card {
2 | padding: 0;
3 | overflow: hidden;
4 |
5 | .faq-section {
6 | margin: 0;
7 | padding: 0;
8 |
9 | .faq-heading {
10 | cursor: pointer;
11 | padding: 16px;
12 | margin: 0;
13 |
14 | border-top: 2px solid var(--hr-color);
15 |
16 | .arrow {
17 | .open-arrow {
18 | display: block;
19 | }
20 | .close-arrow {
21 | display: none;
22 | }
23 | }
24 | }
25 |
26 | .faq-content {
27 | display: grid;
28 | grid-template-rows: 0fr;
29 | transition: grid-template-rows 0.3s ease-out;
30 |
31 | margin: 0;
32 | padding: 0;
33 |
34 | .faq-content-inner {
35 | overflow: hidden;
36 |
37 | margin: 0;
38 | padding: 0;
39 |
40 | .inner {
41 | margin: 0;
42 | padding: 16px;
43 | border-top: 2px solid var(--hr-color);
44 | }
45 | }
46 | }
47 |
48 | &:first-of-type {
49 | .faq-heading {
50 | border-top: none;
51 | }
52 | }
53 |
54 | &.active {
55 | .faq-heading {
56 | background-color: var(--primary-color);
57 | color: var(--on-primary-color);
58 |
59 | .arrow {
60 | .open-arrow {
61 | display: none;
62 | }
63 | .close-arrow {
64 | display: block;
65 | }
66 | }
67 | }
68 |
69 | .faq-content {
70 | grid-template-rows: 1fr;
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_header.scss:
--------------------------------------------------------------------------------
1 | header {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: space-between;
5 |
6 | div {
7 | display: flex;
8 | flex-direction: row;
9 | }
10 |
11 | img.icon {
12 | width: 32px;
13 | height: 32px;
14 |
15 | margin: 0 16px 0 0;
16 | }
17 |
18 | #burger-menu {
19 | display: none;
20 |
21 | margin: 0;
22 | padding: 0;
23 | border: none;
24 | border-radius: 0;
25 | width: fit-content;
26 | min-width: auto;
27 | background: none;
28 |
29 | .icon {
30 | width: 16px;
31 | height: 16px;
32 | }
33 | }
34 | }
35 |
36 | @media (max-width: 950px) {
37 | header {
38 | #burger-menu {
39 | display: block;
40 | }
41 | #icon {
42 | display: none;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: Arial, sans-serif;
4 |
5 | background-color: var(--background-color);
6 | color: var(--font-color);
7 | }
8 |
9 | a {
10 | color: var(--primary-highlight-color);
11 | }
12 |
13 | .strong {
14 | font-weight: bolder;
15 | }
16 |
17 | .card {
18 | display: flex;
19 | flex-direction: column;
20 |
21 | background-color: var(--surface-color);
22 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
23 | box-shadow: color-mix(in srgb, rgb(0, 0, 0) 30%, transparent) 0 1px 2px 0,
24 | color-mix(in srgb, rgb(0, 0, 0) 15%, transparent) 0 2px 6px 2px;
25 | border-radius: 8px;
26 | padding: 16px 0;
27 | margin: 8px;
28 | width: 100%;
29 | min-width: 550px;
30 | max-width: 680px;
31 |
32 | div {
33 | margin-left: 16px;
34 | margin-right: 16px;
35 | }
36 | }
37 |
38 | .textfield {
39 | display: flex;
40 | height: 36px;
41 | padding: 0 0 0 16px;
42 |
43 | background-color: var(--input-color);
44 | border: 1px solid var(--border-color);
45 | border-radius: 1rem;
46 |
47 | input[type="text"] {
48 | width: 100%;
49 |
50 | border: none;
51 | background-color: var(--input-color);
52 | color: var(--font-color);
53 |
54 | &:focus {
55 | outline: none;
56 | }
57 | }
58 |
59 | button {
60 | background-color: var(--surface-color);
61 | color: var(--primary-highlight-color);
62 | margin: 0 0 0 8px;
63 | border: none;
64 |
65 | &:hover {
66 | background-color: var(--highlight-color);
67 | }
68 | }
69 |
70 | &:focus-within {
71 | border: 2px solid var(--border-color-active);
72 | }
73 | }
74 |
75 | button {
76 | background-color: var(--primary-color);
77 | color: var(--on-primary-color);
78 | padding: var(--size-05);
79 | border-radius: var(--size-1);
80 | border: none;
81 | min-width: 100px;
82 |
83 | &:hover {
84 | background-color: var(--primary-highlight-color);
85 | }
86 |
87 | &.outlined {
88 | background: none;
89 | border: 2px solid var(--primary-highlight-color);
90 | color: var(--primary-highlight-color);
91 |
92 | &:hover {
93 | background-color: var(--highlight-color);
94 | }
95 | }
96 | }
97 |
98 | hr {
99 | height: 2px;
100 | width: 100%;
101 | margin: 16px 0;
102 | background-color: var(--hr-color);
103 | }
104 |
105 | select[multiple="multiple"] {
106 | background-color: var(--background-color);
107 | color: var(--font-color);
108 | margin: 16px;
109 |
110 | option {
111 | padding: var(--size-05);
112 |
113 | &.case-insensitive {
114 | &::after {
115 | content: " (case-insensitive)";
116 | color: var(--primary-color);
117 | font-style: italic;
118 | }
119 | }
120 |
121 | &:checked {
122 | background-color: var(--primary-color);
123 | color: var(--on-primary-color);
124 | }
125 |
126 | &:hover {
127 | background-color: var(--highlight-color);
128 | color: var(--font-color);
129 | }
130 |
131 | &:hover &:checked {
132 | background-color: red;
133 | color: var(--font-color);
134 | }
135 | }
136 |
137 | &::-webkit-scrollbar {
138 | width: 8px;
139 | }
140 | &::-webkit-scrollbar-thumb {
141 | background-color: var(--primary-color);
142 | border-radius: 5px;
143 | margin: 1px;
144 | }
145 | &::-webkit-scrollbar-track {
146 | background-color: var(--highlight-color);
147 | border-radius: 5px;
148 | }
149 |
150 | &.larger {
151 | height: 250px;
152 | }
153 |
154 | &.largest {
155 | height: 375px;
156 | }
157 | }
158 |
159 | .align-right {
160 | display: flex;
161 | justify-content: flex-end;
162 | }
163 |
164 | .card-like {
165 | width: 100%;
166 | min-width: 550px;
167 | max-width: 680px;
168 |
169 | margin: 8px;
170 | }
171 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_navigation.scss:
--------------------------------------------------------------------------------
1 | header {
2 | padding: 16px;
3 |
4 | h1 {
5 | font-size: 1.5rem;
6 | font-weight: 500;
7 | }
8 | }
9 |
10 | nav {
11 | position: absolute;
12 | max-width: 200px;
13 | flex-shrink: 0;
14 |
15 | overflow: hidden;
16 | transition: max-width 0.3s ease-out;
17 |
18 | ul {
19 | li {
20 | padding: 8px 16px;
21 | font-size: 14px;
22 | font-weight: 500;
23 |
24 | text-wrap: nowrap;
25 |
26 | &.active,
27 | &:hover {
28 | border-radius: 0 var(--size-1) var(--size-1) 0;
29 | border: none;
30 | min-width: 100px;
31 | }
32 |
33 | &:hover {
34 | background-color: var(--highlight-color);
35 | color: var(--font-color);
36 | }
37 |
38 | &.active {
39 | background-color: var(--primary-color);
40 | color: var(--on-primary-color);
41 | }
42 |
43 | cursor: pointer;
44 | }
45 | }
46 | }
47 |
48 | @media (max-width: 1400px) {
49 | nav {
50 | position: initial;
51 | }
52 | }
53 |
54 | @media (max-width: 950px) {
55 | nav {
56 | max-width: 0;
57 |
58 | &.open {
59 | max-width: 200px;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_sections.scss:
--------------------------------------------------------------------------------
1 | #main-container {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: start;
5 | }
6 |
7 | .section {
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 |
13 | flex-shrink: 1;
14 |
15 | width: 100%;
16 | padding: 0 32px 32px 32px;
17 |
18 | h2 {
19 | width: 100%;
20 | min-width: 550px;
21 | max-width: 680px;
22 |
23 | font-size: 14px;
24 | font-weight: 500;
25 | }
26 |
27 | h3 {
28 | font-size: 16px;
29 | font-weight: 500;
30 | }
31 |
32 | h4 {
33 | font-size: 14px;
34 | font-weight: 500;
35 | margin-top: 8px;
36 | margin-bottom: 8px;
37 | }
38 |
39 | p {
40 | font-size: 14px;
41 | margin-top: 8px;
42 | margin-bottom: 8px;
43 | }
44 | }
45 |
46 | .settings-row {
47 | display: flex;
48 | justify-content: space-between;
49 |
50 | label {
51 | display: flex;
52 | justify-content: center;
53 | flex-direction: column;
54 | font-size: 1rem;
55 | }
56 |
57 | input,
58 | select {
59 | width: 25%;
60 | }
61 |
62 | select {
63 | background-color: var(--background-color);
64 | color: var(--font-color);
65 | padding: 8px;
66 | border-radius: 8px;
67 | cursor: pointer;
68 | }
69 |
70 | input[type="color"] {
71 | border: none;
72 | background: none;
73 | height: 32px;
74 | padding: 0;
75 | }
76 |
77 | input[type="range"] {
78 | &::-webkit-slider-thumb {
79 | background: var(--primary-color);
80 | }
81 | }
82 |
83 | &.space-top {
84 | margin-top: 16px;
85 | }
86 |
87 | &.disabled {
88 | cursor: not-allowed;
89 |
90 | label,
91 | select,
92 | input,
93 | span {
94 | cursor: not-allowed;
95 | }
96 |
97 | label {
98 | color: var(--font-color-disabled);
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_switch.scss:
--------------------------------------------------------------------------------
1 | /* TOGGLE-SWITCH */
2 | /* The switch - the box around the slider */
3 |
4 | .switch {
5 | position: relative;
6 | display: inline-block;
7 | width: 32px;
8 | height: 20px;
9 | }
10 |
11 | /* Hide default HTML checkbox */
12 | .switch input {
13 | opacity: 0;
14 | width: 0;
15 | height: 0;
16 | }
17 |
18 | /* The slider */
19 | .slider {
20 | position: absolute;
21 | cursor: pointer;
22 | top: 0;
23 | left: 0;
24 | right: 0;
25 | bottom: 0;
26 | background: var(--surface-color);
27 | border: 2px solid var(--primary-color);
28 | -webkit-transition: 0.4s;
29 | transition: 0.4s;
30 | }
31 |
32 | .slider:before {
33 | position: absolute;
34 | content: "";
35 | height: 12px;
36 | width: 12px;
37 | left: 2px;
38 | bottom: 2px;
39 | background-color: var(--primary-color);
40 | -webkit-transition: 0.4s;
41 | transition: 0.4s;
42 | }
43 |
44 | input:checked + .slider {
45 | background-color: var(--primary-color);
46 | }
47 |
48 | input:focus + .slider {
49 | box-shadow: 0 0 1px #2196f3;
50 | }
51 |
52 | input:checked + .slider:before {
53 | transform: translateX(12px);
54 | background-color: var(--surface-color);
55 | }
56 |
57 | /* Rounded sliders */
58 | .slider.round {
59 | border-radius: 16px;
60 | }
61 |
62 | .slider.round:before {
63 | border-radius: 50%;
64 | }
65 |
--------------------------------------------------------------------------------
/ui/settings/style/modules/_variables.scss:
--------------------------------------------------------------------------------
1 | $primary-color: #0078d4;
2 | $primary-color-darken: darken($primary-color, 10%);
3 | $primary-color-lighten: lighten($primary-color, 20%);
4 |
5 | $primary-color-dark-mode: #0060aa;
6 | $primary-color-darken-dark-mode: darken($primary-color-dark-mode, 10%);
7 | $primary-color-lighten-dark-mode: lighten($primary-color-dark-mode, 20%);
8 |
9 | body {
10 | font-size: larger;
11 | --size-05: 0.5rem;
12 | --size-1: 1rem;
13 | --size-2: 1.2rem;
14 | --size-3: 1.5rem;
15 |
16 | --background-color: #ffffff;
17 | --surface-color: #ffffff;
18 | --input-color: #edf1fa;
19 |
20 | --font-color: #141414;
21 | --font-color-disabled: #707070;
22 |
23 | --primary-color: #0078d4;
24 | --on-primary-color: #ffffff;
25 |
26 | --hr-color: #f0f0f0;
27 |
28 | --border-color: #c7c7c7;
29 | --border-color-active: #0078d4;
30 |
31 | --highlight-color: #ededed;
32 | --primary-highlight-color: #005ea7;
33 | }
34 |
35 | body.dark-scheme {
36 | --background-color: #202124;
37 | --surface-color: #292a2d;
38 | --input-color: #282828;
39 |
40 | --font-color: #e3e3e3;
41 | --font-color-disabled: #707070;
42 |
43 | --primary-color: #005ea7;
44 | --on-primary-color: #e3e3e3;
45 |
46 | --hr-color: #3f4042;
47 |
48 | --border-color: #757575;
49 | --border-color-active: #acc6f9;
50 |
51 | --highlight-color: #38393b;
52 | --primary-highlight-color: #0078d4;
53 | }
54 |
55 | @media (prefers-color-scheme: dark) {
56 | body.detect-scheme {
57 | --background-color: #202124;
58 | --surface-color: #292a2d;
59 | --input-color: #282828;
60 |
61 | --font-color: #e3e3e3;
62 | --font-color-disabled: #707070;
63 |
64 | --primary-color: #005ea7;
65 | --on-primary-color: #e3e3e3;
66 |
67 | --hr-color: #3f4042;
68 |
69 | --border-color: #757575;
70 | --border-color-active: #acc6f9;
71 |
72 | --highlight-color: #38393b;
73 | --primary-highlight-color: #0078d4;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ES6",
5 | "outDir": "../dist/ui",
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "skipLibCheck": true
10 | },
11 | "include": ["./**/*.ts"],
12 | "exclude": ["../dist"]
13 | }
14 |
--------------------------------------------------------------------------------