diff --git a/src/anilist-extras.user.ts b/src/anilist-extras.user.ts index 9ac7cc3..730285d 100644 --- a/src/anilist-extras.user.ts +++ b/src/anilist-extras.user.ts @@ -1,5 +1,6 @@ import '@/utils/Polyfill'; import '@/utils/Logs'; +import { purgeUnusedSettings } from './utils/Settings'; import { observe, addStyles, getMalId } from './utils/Helpers'; import { anilistModules, malModules, activeModules, ModuleEmitter, ModuleEvents } from './utils/ModuleLoader'; @@ -7,8 +8,8 @@ import { anilistModules, malModules, activeModules, ModuleEmitter, ModuleEvents import '@/modules/anilist/settingsPage'; import '@/modules/anilist/addMalLink'; import '@/modules/anilist/addMalScore'; -import '@/modules/anilist/addAniListScore'; import '@/modules/anilist/addMalScoreAndLink'; +import '@/modules/anilist/addAniListScore'; import '@/modules/anilist/addViewToggles'; import '@/modules/anilist/addMalCharacters'; import '@/modules/anilist/addOpEdSongs'; @@ -41,6 +42,9 @@ addStyles(` } `); +// Clean up unused settings. +purgeUnusedSettings(); + /* eslint-disable promise/prefer-await-to-then */ let currentPage: URL; diff --git a/src/modules/anilist/settingsPage.ts b/src/modules/anilist/settingsPage.ts index 625cda0..0edea89 100644 --- a/src/modules/anilist/settingsPage.ts +++ b/src/modules/anilist/settingsPage.ts @@ -1,6 +1,6 @@ import Storage from '@/utils/Storage'; import Cache from '@/utils/Cache'; -import SettingsManager from '@/utils/Settings'; +import SettingsManager, { purgeUnusedSettings } from '@/utils/Settings'; import { $, waitFor, @@ -357,8 +357,111 @@ registerModule.anilist({ } } - createElement('div', { + const restoreSettingsInput = createElement('input', { + attributes: { + type: 'file', + accept: '.json', + }, + events: { + change(event) { + const file = (event.target as HTMLInputElement).files?.[0]; + + if (!file) return; + + if (file.type !== 'application/json') { + // eslint-disable-next-line no-alert + alert('Invalid file type. Please select a valid JSON file.'); + return; + } + + const fileReader = new FileReader(); + + fileReader.onload = event => { + const result = event.target?.result; + try { + const fileContents = JSON.parse(result as string); + + if (!fileContents.alextrasMeta) { + // eslint-disable-next-line no-alert + alert('Invalid JSON file. Please select a valid JSON file.'); + return; + } + + // Make sure we keep the API token if it exists. + const apiToken = Storage.get('apiToken'); + if (apiToken) { + fileContents.apiToken = apiToken; + } + + localStorage.setItem('anilist-extras', JSON.stringify(fileContents)); + // eslint-disable-next-line no-alert + alert('AniList Extras settings have been restored. Page will refresh.'); + location.reload(); + } catch { + // eslint-disable-next-line no-alert + alert('Invalid JSON file. Please select a valid JSON file.'); + } + }; + + fileReader.readAsText(file); + }, + }, + }); + + createElement('section', { children: [ + createElement('div', { + attributes: { + class: 'button', + }, + textContent: 'Backup Settings', + events: { + async click() { + purgeUnusedSettings(); + + const storage = Storage.getAll(); + + // Remove API token from backup. + if (storage.apiToken) { + delete storage.apiToken; + } + + storage.alextrasMeta = { + version: ALEXTRAS_VERSION, + createdAt: new Date().toISOString(), + }; + + const settingsBackup = JSON.stringify(storage, null, '\t'); + + const settingsBlob = new Blob([settingsBackup], { + type: 'application/json', + }); + + const aElement = createElement('a', { + attributes: { + download: `anilist-extras-settings-${new Date().toISOString()}.json`, + href: URL.createObjectURL(settingsBlob), + }, + }); + + aElement.click(); + }, + }, + }), + createElement('div', { + attributes: { + class: 'button', + }, + styles: { + position: 'relative', + }, + textContent: 'Restore Settings', + events: { + click() { + restoreSettingsInput.click(); + }, + }, + }), createElement('div', { attributes: { class: 'button danger', @@ -387,11 +490,18 @@ registerModule.anilist({ }, }, }), + createElement('div', { + styles: { + marginTop: '15px', + color: 'rgb(var(--color-red))', + }, + textContent: 'Restoring settings will overwrite your current settings.', + }), ], appendTo: settingsContainer, }); - createElement('div', { + createElement('section', { children: [ createElement('h4', { textContent: 'AniList Extras Version: ', @@ -437,7 +547,7 @@ registerModule.anilist({ styles: { color: 'rgb(var(--color-blue))', }, - textContent: 'https://github.com/pilar6195/AniList-Extras', + textContent: 'pilar6195/AniList-Extras', }), ], }), @@ -462,6 +572,18 @@ addStyles(` gap: 10px; } + @media only screen and (min-width: 761px) and (max-width: 950px) { + .alextras--settings-body { + grid-template-columns: 1fr; + } + } + + @media only screen and (max-width: 650px) { + .alextras--settings-body { + grid-template-columns: 1fr; + } + } + .alextras--module { display: flex; flex-direction: column; diff --git a/src/utils/Settings.ts b/src/utils/Settings.ts index d7caaf3..c97b02a 100644 --- a/src/utils/Settings.ts +++ b/src/utils/Settings.ts @@ -1,5 +1,28 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ import Storage from './Storage'; -import { getModule } from './ModuleLoader'; +import { anilistModules, malModules, getModule } from './ModuleLoader'; + +export const purgeUnusedSettings = () => { + const moduleIds = [...anilistModules, ...malModules].map(m => m.id); + const settings = Storage.getAll(); + + for (const key of ['settings', 'moduleStates']) { + if (!settings[key]) continue; + + let changed = false; + + for (const moduleId in settings[key]) { + if (!moduleIds.includes(moduleId)) { + delete settings[key][moduleId]; + changed = true; + } + } + + if (changed) { + Storage.set(key, settings[key]); + } + } +}; /** * Settings manager for registered modules. @@ -26,28 +49,38 @@ export default class SettingsManager { * Set a setting value. */ public set(key: string, value: any) { - const moduleSettings = Storage.get(this.moduleId, {}); + const settings = Storage.get('settings', {}); + const moduleSettings = settings[this.moduleId] ?? {}; moduleSettings[key] = value; - Storage.set(this.moduleId, moduleSettings); + settings[this.moduleId] = moduleSettings; + Storage.set('settings', settings); } /** * Get a setting value. - * If the setting is not found, it will return the default value from the module settings. - * If the setting is not specified in the module settings, it will return the default value passed as the second argument. + * If the setting is not found, it will return the default value passed as the second argument. + * If a default value is not provided, it will return the default value from the module settings, if any. */ public get(key: string, defaultValue?: any) { - const moduleSettings = Storage.get(this.moduleId, {}); - return moduleSettings[key] ?? this.module.settingsPage?.[key].default ?? defaultValue; + const settings = Storage.get('settings', {}); + const moduleSettings = settings[this.moduleId] ?? {}; + return moduleSettings[key] ?? defaultValue ?? this.module.settingsPage?.[key].default; } /** * Delete a setting. */ public remove(key: string) { - const moduleSettings = Storage.get(this.moduleId, {}); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + const settings = Storage.get('settings', {}); + const moduleSettings = settings[this.moduleId] ?? {}; delete moduleSettings[key]; - Storage.set(this.moduleId, moduleSettings); + settings[this.moduleId] = moduleSettings; + Storage.set('settings', settings); + } + + public clear() { + const settings = Storage.get('settings', {}); + delete settings[this.moduleId]; + Storage.set('settings', settings); } }