From 3551d5c650f4561b8a0f73b6e041a6c4a103909b Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 3 Sep 2025 14:26:42 -0400 Subject: [PATCH] 4.78.1 * Fixed: Certain page elements not being correctly hidden when FFZ first loads. * Fixed: Backup and Restore not allowing you to restore a backup with blobs if the current settings provider does not support blobs. It now prompts you to confirm the restoration. * Fixed: The settings provider switcher now more accurately displays if providers contain existing data. * Fixed: Issue where the FFZ Control Center fails to load in a pop-out on Firefox. * Fixed: FFZ believing it is in developer mode when loaded from an extension in some cases. * Changed: The setting to hide Stories now hides them in the directory as well. * Changed: The settings provider switcher now displays a warning if transferring your settings would erase data in the destination provider. * Changed: The settings provider switcher now displays a tag on providers that are Cross-Origin Compatible. --- package.json | 2 +- .../main_menu/components/backup-restore.vue | 112 ++++++++++++------ src/modules/main_menu/components/provider.vue | 42 ++++++- src/settings/index.ts | 24 +++- src/settings/providers.ts | 33 +++++- .../twitch-twilight/modules/chat/index.js | 4 +- .../twitch-twilight/modules/loadable.tsx | 79 +++++++++++- src/utilities/constants.ts | 6 +- 8 files changed, 247 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 88580726..6b5a5d1d 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.78.0", + "version": "4.78.1", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/main_menu/components/backup-restore.vue b/src/modules/main_menu/components/backup-restore.vue index 910e49e8..91bb809a 100644 --- a/src/modules/main_menu/components/backup-restore.vue +++ b/src/modules/main_menu/components/backup-restore.vue @@ -4,7 +4,32 @@ {{ t('setting.backup-restore.about', 'This tool allows you to backup and restore your FrankerFaceZ settings, including all settings from the Control Center along with other data such as favorited emotes and blocked games.') }} -
+
+

+ {{ t('setting.backup-restore.blob-notice', 'This backup contains binary data not supported by the current storage provider.') }} +

+
+ {{ t('setting.backup-restore.blob-notice.desc', 'The binary data will be discarded and certain settings, notably ones with custom sound files, may not work correctly. Do you wish to continue? Alternatively, you will need to change your settings provider to one supporting binary data.') }} +
+
+ +
+
+ +
@@ -69,6 +84,17 @@
+
+

+ {{ t('setting.provider.warn.title', 'Be careful!') }} +

+
+ +
+
@@ -110,6 +136,7 @@ export default { const ffz = this.context.getFFZ(), settings = ffz.resolve('settings'), providers = [], + prov_keys = {}, transfers = {}; for(const [key, val] of Object.entries(settings.getProviders())) { @@ -117,7 +144,8 @@ export default { key, priority: val.priority || 0, has_data: null, - has_blobs: val.supportsBlobs, + has_blobs: val.canSupportBlobs(settings), + has_crossorigin: val.crossOrigin(settings), has_trans: val.allowTransfer, i18n_key: `setting.provider.${key}.title`, title: val.title || key, @@ -125,10 +153,11 @@ export default { description: val.description }; + prov_keys[key] = prov; transfers[key] = val.allowTransfer; - if ( val.supported() ) - Promise.resolve(val.hasContent()).then(v => { + if ( val.supported(settings) ) + Promise.resolve(val.hasContent(settings)).then(v => { prov.has_data = v; }); @@ -143,6 +172,7 @@ export default { backup: false, not_www: window.location.host !== 'www.twitch.tv', providers, + prov_keys, transfers, current, selected: current diff --git a/src/settings/index.ts b/src/settings/index.ts index 4c00dae7..f2d079ca 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -19,7 +19,7 @@ import * as CLEARABLES from './clearables'; import type { SettingsProfileMetadata, ContextData, ExportedFullDump, SettingsClearable, SettingDefinition, SettingProcessor, SettingUiDefinition, SettingValidator, SettingType, ExportedBlobMetadata, SettingsKeys, AllSettingsKeys, ConcreteLocalStorageData } from './types'; import type { FilterType } from 'utilities/filtering'; -import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, SettingsProvider } from './providers'; +import { AdvancedSettingsProvider, IGNORE_CONTENT_KEYS, LocalStorageProvider, Providers, SettingsProvider } from './providers'; import type { AddonInfo, SettingsTypeMap } from 'utilities/types'; import { FFZEvent } from '../utilities/events'; @@ -170,12 +170,17 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> // Load any dynamic providers that have been registered. // Now that we're here, no further providers can be registered, so seal them. window.ffz_providers = window.ffz_providers || []; - Object.seal(window.ffz_providers); + try { + Object.seal(window.ffz_providers); + } catch(err) { + this.log.warn('Unable to seal window.ffz_providers:', err); + } if ( window.ffz_providers.length > 0 ) { const evt = { settings: this, Provider: SettingsProvider, AdvancedProvider: AdvancedSettingsProvider, + IGNORE_CONTENT_KEYS: IGNORE_CONTENT_KEYS, registerProvider: (key: string, provider: typeof SettingsProvider) => { if ( ! this.providers[key] && provider.supported(this) ) this.providers[key] = provider; @@ -786,8 +791,21 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> ((a[1] as any).priority ?? 0) ); + // Remove unsupported providers. + for(let i = providers.length - 1; i >= 0; i--) { + if ( ! providers[i][1].supported(this) ) + providers.splice(i, 1); + } + + // If there's a provider that has content, then use it. for(const [key, provider] of providers) { - if ( provider.supported(this) && await provider.hasContent(this) ) // eslint-disable-line no-await-in-loop + if ( await provider.hasContent(this) ) // eslint-disable-line no-await-in-loop + return key; + } + + // Select the first provider that allows itself to be the default. + for(const [key, provider] of providers) { + if ( provider.allowAsDefault(this) ) return key; } diff --git a/src/settings/providers.ts b/src/settings/providers.ts index 37a2dd28..88a5a3ae 100644 --- a/src/settings/providers.ts +++ b/src/settings/providers.ts @@ -1,23 +1,30 @@ 'use strict'; import { isValidBlob, deserializeBlob, serializeBlob, BlobLike, SerializedBlobLike } from 'utilities/blobs'; +import { EXTENSION } from 'utilities/constants'; +import {EventEmitter} from 'utilities/events'; +import {TicketLock, has, once} from 'utilities/object'; +import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types'; // ============================================================================ // Settings Providers // ============================================================================ -import {EventEmitter} from 'utilities/events'; -import {TicketLock, has, once} from 'utilities/object'; import type SettingsManager from '.'; -import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types'; -import { EXTENSION } from '../utilities/constants'; const DB_VERSION = 1, NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv', NOT_WWW_YT = window.location.host !== 'www.youtube.com'; +export const IGNORE_CONTENT_KEYS = [ + 'client-id', + 'cfg-seen', + 'cfg-collapsed' +]; + + // ============================================================================ // Types // ============================================================================ @@ -51,6 +58,9 @@ export abstract class SettingsProvider extends EventEmitter { static title: string; static description: string; + static crossOrigin(manager: SettingsManager) { return false; } + static canSupportBlobs(manager: SettingsManager) { return false; } + static hasContent: (manager: SettingsManager) => OptionalPromise; @@ -74,6 +84,10 @@ export abstract class SettingsProvider extends EventEmitter { return false; } + static allowAsDefault(manager: SettingsManager) { + return true; + } + static allowTransfer = true; static shouldUpdate = true; @@ -129,6 +143,8 @@ export abstract class SettingsProvider extends EventEmitter { export abstract class AdvancedSettingsProvider extends SettingsProvider { + static canSupportBlobs() { return true; } + get supportsBlobs() { return true; } isValidBlob(blob: any): blob is BlobLike { @@ -421,7 +437,7 @@ export class LocalStorageProvider extends SettingsProvider { const prefix = 'FFZ:setting:'; for(const key in localStorage) - if ( key.startsWith(prefix) && has(localStorage, key) ) + if ( key.startsWith(prefix) && ! IGNORE_CONTENT_KEYS.includes(key.slice(prefix.length)) && has(localStorage, key) ) return true; return false; @@ -695,7 +711,11 @@ export class IndexedDBProvider extends AdvancedSettingsProvider { } r2.onsuccess = () => { - const success = Array.isArray(r2.result) && r2.result.length > 0; + let success = false; + if ( Array.isArray(r2.result) && r2.result.length > 0 ) { + success = r2.result.filter(key => !IGNORE_CONTENT_KEYS.includes(key as string)).length > 0; + } + db.close(); return resolve(success); } @@ -1446,6 +1466,7 @@ export class CrossOriginStorageBridge extends RemoteSettingsProvider { static hasContent() { return CrossOriginStorageBridge.supported(); } + static allowAsDefault() { return false; } static priority = 100; static title = 'Cross-Origin Storage Bridge'; diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index ff5a166b..3163e0f3 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -403,9 +403,9 @@ export default class ChatHook extends Module { ui: { path: 'Chat > Filtering > Block >> Callout Types @{"description":"This filter allows you to remove callouts of specific types from Twitch chat. Callouts are special messages that can be pinned to the bottom of chat and often have associated actions, like claiming a drop or sharing your resubscription."}', component: 'blocked-types', - getExtraTerms: () => Object.keys(this.callout_types).filter(key => ! UNBLOCKABLE_CALLOUTS.includes(key)), + getExtraTerms: () => Object.keys(this.callout_types ?? CALLOUT_TYPES).filter(key => ! UNBLOCKABLE_CALLOUTS.includes(key)), data: () => Object - .keys(this.callout_types) + .keys(this.callout_types ?? CALLOUT_TYPES) .filter(key => ! UNBLOCKABLE_CALLOUTS.includes(key)) .sort() } diff --git a/src/sites/twitch-twilight/modules/loadable.tsx b/src/sites/twitch-twilight/modules/loadable.tsx index b52d0475..c69fe3f7 100644 --- a/src/sites/twitch-twilight/modules/loadable.tsx +++ b/src/sites/twitch-twilight/modules/loadable.tsx @@ -24,6 +24,7 @@ declare module 'utilities/types' { 'chat.hype.show-pinned': boolean; 'layout.turbo-cta': boolean; 'layout.subtember': boolean; + 'layout.side-nav.hide-stories': boolean; } } @@ -43,6 +44,13 @@ type ErrorBoundaryNode = ReactStateNode<{ onErrorBoundaryTestEmit: any } +type SettingToggleNode = ReactStateNode<{ + name: string; + children: any; +}> & { + render: AnyFunction; +}; + export default class Loadable extends Module { @@ -53,10 +61,12 @@ export default class Loadable extends Module { // State overrides: Map; + setting_overrides: Map; // Fine ErrorBoundaryComponent: FineWrapper; LoadableComponent: FineWrapper; + SettingsToggleComponent: FineWrapper; constructor(name?: string, parent?: GenericModule) { super(name, parent); @@ -83,8 +93,17 @@ export default class Loadable extends Module { (n as ErrorBoundaryNode).onErrorBoundaryTestEmit ); - this.overrides = new Map(); + this.SettingsToggleComponent = this.fine.define( + 'settings-toggle-component', + n => + (n as SettingToggleNode).props?.name && + (n as SettingToggleNode).props?.children && + (n as SettingToggleNode).render && + (n as SettingToggleNode).render.toString().includes('defaultThreshold') + ); + this.overrides = new Map(); + this.setting_overrides = new Map(); } onEnable() { @@ -100,6 +119,10 @@ export default class Loadable extends Module { this.toggle('TokenizedCommerceBanner', val); }); + this.settings.getChanges('layout.side-nav.hide-stories', val => { + this.toggleSetting('stories_web', !val); + }) + this.ErrorBoundaryComponent.ready((cls, instances) => { this.log.debug('Found Error Boundary component wrapper.'); @@ -122,6 +145,7 @@ export default class Loadable extends Module { } this.ErrorBoundaryComponent.updateInstances(); + this.ErrorBoundaryComponent.forceUpdate(); }); this.LoadableComponent.ready((cls, instances) => { @@ -167,6 +191,32 @@ export default class Loadable extends Module { } this.LoadableComponent.updateInstances(); + this.LoadableComponent.forceUpdate(); + }); + + this.SettingsToggleComponent.ready((cls, instances) => { + this.log.debug('Found Settings Toggle component wrapper.'); + + const t = this, + proto = cls.prototype as SettingToggleNode, + old_render = proto.render; + + (proto as any)._ffz_wrapped_render = old_render; + proto.render = function() { + try { + const type = this.props.name; + if ( t.setting_overrides.has(type) && ! t.shouldRenderSetting(type) ) + return null; + } catch(err) { + /* no-op */ + console.error(err); + } + + return old_render.call(this); + } + + this.SettingsToggleComponent.updateInstances(); + this.SettingsToggleComponent.forceUpdate(); }); } @@ -184,6 +234,20 @@ export default class Loadable extends Module { } } + toggleSetting(cmp: string, state: boolean | null = null) { + const existing = this.setting_overrides.get(cmp) ?? true; + + if ( state == null ) + state = ! existing; + else + state = !! state; + + if ( state !== existing ) { + this.setting_overrides.set(cmp, state); + this.updateSetting(cmp); + } + } + update(cmp: string) { for(const inst of this.LoadableComponent.instances) { const type = inst.props?.component; @@ -200,8 +264,21 @@ export default class Loadable extends Module { } } + updateSetting(cmp: string) { + for(const inst of this.SettingsToggleComponent.instances) { + const name = inst.props?.name; + if ( name && name === cmp ) { + inst.forceUpdate(); + } + } + } + shouldRender(cmp: string) { return this.overrides.get(cmp) ?? true; } + shouldRenderSetting(cmp: string) { + return this.setting_overrides.get(cmp) ?? true; + } + } diff --git a/src/utilities/constants.ts b/src/utilities/constants.ts index f8819a97..2a7c16b4 100644 --- a/src/utilities/constants.ts +++ b/src/utilities/constants.ts @@ -2,12 +2,12 @@ declare global { let __extension__: string | undefined; } -/** Whether or not FrankerFaceZ was loaded from a development server. */ -export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev'); - /** Whether or not FrankerFaceZ was loaded as a packed web extension. */ export const EXTENSION = !!__extension__; +/** Whether or not FrankerFaceZ was loaded from a development server. */ +export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev') && !EXTENSION; + /** The base URL of the FrankerFaceZ CDN. */ export const SERVER = DEBUG ? 'https://localhost:8000' : 'https://cdn2.frankerfacez.com';