From 09186f86002886c38a2cea96a9f8444c71c2decd Mon Sep 17 00:00:00 2001 From: FatCat <82377182+FatCatClient@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:31:15 +0100 Subject: [PATCH 001/184] Rudimentary global settings w/ character overrides --- chat/Chat.vue | 24 ++++ chat/SettingsView.vue | 212 ++++++++++++++++++++++---------- chat/core.ts | 138 ++++++++++++++++++++- chat/interfaces.ts | 24 +++- chat/locales/en_us.json | 13 ++ components/SettingsTriState.vue | 88 +++++++++++++ electron/filesystem.ts | 5 +- 7 files changed, 428 insertions(+), 76 deletions(-) create mode 100644 components/SettingsTriState.vue diff --git a/chat/Chat.vue b/chat/Chat.vue index a95061e8..74b1c478 100644 --- a/chat/Chat.vue +++ b/chat/Chat.vue @@ -172,6 +172,20 @@
{{ error }}
{{ l('chat.disconnected') }} + + +

{{ l('settings.migration.prompt') }}

+
this.$refs['settingsMigration']).show(true); + } + // tslint:disable-next-line:no-floating-promises core.siteSession.onConnectionEstablished(); }); @@ -442,6 +461,11 @@ (this.$refs['reconnecting']).hide(); } + async migrateSettings(useCurrentAsGlobal: boolean): Promise { + await core.migrateToGlobalSettings(useCurrentAsGlobal); + (this.$refs['settingsMigration']).hide(); + } + selectCharacter(character: SimpleCharacter): void { this.selectedCharacter = character; } diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index b02f51ed..fa41786e 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -8,6 +8,18 @@ dialogClass="modal-70" iconClass="fas fa-user-gear" > + + + + -
+
{{ l('warning.info') }}
- {{ l('settings.charactersToGeneral') }} + {{ l('settings.chat.mode.global') }}
- +
+
+
{{ l('warning.info') }}
- {{ l('settings.charactersToGeneral.instructions') }} + {{ l('settings.chat.mode.character') }}
@@ -54,9 +68,16 @@
+
@@ -67,9 +88,16 @@
+ @@ -1126,9 +1154,10 @@ import Tabs from '../components/tabs'; import { BBCodeView } from '../bbcode/view'; import SettingsCheckbox from '../components/SettingsCheckbox.vue'; + import SettingsTriState from '../components/SettingsTriState.vue'; import { UserInterfaceBBCodeParser } from '../bbcode/user-interface'; import core from './core'; - import { Settings as SettingsInterface } from './interfaces'; + import { Settings as SettingsInterface, PartialSettings } from './interfaces'; import l from './localize'; import { SmartFilterSettings, @@ -1149,6 +1178,7 @@ tabs: Tabs, bbcode: BBCodeView(bbcodeParser), 'settings-checkbox': SettingsCheckbox, + 'settings-tristate': SettingsTriState, 'user-view': UserView } }) @@ -1156,7 +1186,11 @@ l = l; availableImports: ReadonlyArray = []; selectedTab = '0'; + settingsMode = '0'; // '0' = global, '1' = character overrides + //do we want this as a name? might be more legible importCharacter = ''; + + // Global settings values playSound!: boolean; clickOpensMessage!: boolean; disallowedTags!: string; @@ -1221,85 +1255,101 @@ smartFilterTypes = smartFilterTypesOrigin; + // Character override values (undefined = use global) + characterOverrides: PartialSettings = {}; + async load(): Promise { - const settings = core.state.settings; - this.playSound = settings.playSound; - this.clickOpensMessage = settings.clickOpensMessage; - this.disallowedTags = settings.disallowedTags.join(','); - this.notifications = settings.notifications; - this.highlight = settings.highlight; - this.highlightWords = settings.highlightWords.join(','); - this.showAvatars = settings.showAvatars; - this.animatedEicons = settings.animatedEicons; - this.smoothMosaics = settings.smoothMosaics; - this.idleTimer = settings.idleTimer.toString(); - this.messageSeparators = settings.messageSeparators; - this.eventMessages = settings.eventMessages; - this.joinMessages = settings.joinMessages; - this.alwaysNotify = settings.alwaysNotify; - this.logMessages = settings.logMessages; - this.logAds = settings.logAds; - this.fontSize = settings.fontSize.toString(); - this.showNeedsReply = settings.showNeedsReply; - this.enterSend = settings.enterSend; - this.colorBookmarks = settings.colorBookmarks; - this.showPerCharacterFriends = settings.showPerCharacterFriends; - this.hideNonCharacterFriends = settings.hideNonCharacterFriends; - this.bbCodeBar = settings.bbCodeBar; + // Load global settings + const globalSettings = core.state.globalSettings; + this.playSound = globalSettings.playSound; + this.clickOpensMessage = globalSettings.clickOpensMessage; + this.disallowedTags = globalSettings.disallowedTags.join(','); + this.notifications = globalSettings.notifications; + this.highlight = globalSettings.highlight; + this.highlightWords = globalSettings.highlightWords.join(','); + this.showAvatars = globalSettings.showAvatars; + this.animatedEicons = globalSettings.animatedEicons; + this.smoothMosaics = globalSettings.smoothMosaics; + this.idleTimer = globalSettings.idleTimer.toString(); + this.messageSeparators = globalSettings.messageSeparators; + this.eventMessages = globalSettings.eventMessages; + this.joinMessages = globalSettings.joinMessages; + this.alwaysNotify = globalSettings.alwaysNotify; + this.logMessages = globalSettings.logMessages; + this.logAds = globalSettings.logAds; + this.fontSize = globalSettings.fontSize.toString(); + this.showNeedsReply = globalSettings.showNeedsReply; + this.enterSend = globalSettings.enterSend; + this.colorBookmarks = globalSettings.colorBookmarks; + this.showPerCharacterFriends = globalSettings.showPerCharacterFriends; + this.hideNonCharacterFriends = globalSettings.hideNonCharacterFriends; + this.bbCodeBar = globalSettings.bbCodeBar; this.availableImports = ( await core.settingsStore.getAvailableCharacters() ).filter(x => x !== core.connection.character); // settings.rising - this.risingAdScore = settings.risingAdScore; - this.risingLinkPreview = settings.risingLinkPreview; - this.risingAutoCompareKinks = settings.risingAutoCompareKinks; - - this.risingAutoExpandCustomKinks = settings.risingAutoExpandCustomKinks; - this.risingCharacterPreview = settings.risingCharacterPreview; - this.risingComparisonInUserMenu = settings.risingComparisonInUserMenu; - this.risingComparisonInSearch = settings.risingComparisonInSearch; - this.risingShowUnreadOfflineCount = settings.risingShowUnreadOfflineCount; - - this.risingColorblindMode = settings.risingColorblindMode; - this.risingShowPortraitNearInput = settings.risingShowPortraitNearInput; - this.risingShowPortraitInMessage = settings.risingShowPortraitInMessage; + this.risingAdScore = globalSettings.risingAdScore; + this.risingLinkPreview = globalSettings.risingLinkPreview; + this.risingAutoCompareKinks = globalSettings.risingAutoCompareKinks; + + this.risingAutoExpandCustomKinks = + globalSettings.risingAutoExpandCustomKinks; + this.risingCharacterPreview = globalSettings.risingCharacterPreview; + this.risingComparisonInUserMenu = + globalSettings.risingComparisonInUserMenu; + this.risingComparisonInSearch = globalSettings.risingComparisonInSearch; + this.risingShowUnreadOfflineCount = + globalSettings.risingShowUnreadOfflineCount; + + this.risingColorblindMode = globalSettings.risingColorblindMode; + this.risingShowPortraitNearInput = + globalSettings.risingShowPortraitNearInput; + this.risingShowPortraitInMessage = + globalSettings.risingShowPortraitInMessage; this.risingShowHighQualityPortraits = - settings.risingShowHighQualityPortraits; + globalSettings.risingShowHighQualityPortraits; this.horizonMessagePortraitHighQuality = - settings.horizonMessagePortraitHighQuality; + globalSettings.horizonMessagePortraitHighQuality; this.horizonShowCustomCharacterColors = - settings.horizonShowCustomCharacterColors; - this.horizonShowDeveloperBadges = settings.horizonShowDeveloperBadges; - this.horizonShowGenderMarker = settings.horizonShowGenderMarker; - this.horizonGenderMarkerOrigColor = settings.horizonGenderMarkerOrigColor; - this.horizonChangeOfflineColor = settings.horizonChangeOfflineColor; - this.chatLayoutMode = settings.chatLayoutMode || 'classic'; - this.messageGrouping = settings.messageGrouping; - this.horizonUseColorPicker = settings.horizonUseColorPicker; - - this.horizonCacheDraftMessages = settings.horizonCacheDraftMessages; + globalSettings.horizonShowCustomCharacterColors; + this.horizonShowDeveloperBadges = + globalSettings.horizonShowDeveloperBadges; + this.horizonShowGenderMarker = globalSettings.horizonShowGenderMarker; + this.horizonGenderMarkerOrigColor = + globalSettings.horizonGenderMarkerOrigColor; + this.horizonChangeOfflineColor = globalSettings.horizonChangeOfflineColor; + this.chatLayoutMode = globalSettings.chatLayoutMode || 'classic'; + this.messageGrouping = globalSettings.messageGrouping; + this.horizonUseColorPicker = globalSettings.horizonUseColorPicker; + + this.horizonCacheDraftMessages = globalSettings.horizonCacheDraftMessages; this.horizonSaveDraftMessagesToDiskTimer = - settings.horizonSaveDraftMessagesToDiskTimer.toString(); + globalSettings.horizonSaveDraftMessagesToDiskTimer.toString(); - this.horizonNotifyFriendSignIn = settings.horizonNotifyFriendSignIn; + this.horizonNotifyFriendSignIn = globalSettings.horizonNotifyFriendSignIn; this.horizonShowSigninNotifications = - settings.horizonShowSigninNotifications; + globalSettings.horizonShowSigninNotifications; this.horizonShowDuplicateStatusNotifications = - settings.horizonShowDuplicateStatusNotifications; - this.horizonHighlightUsers = settings.horizonHighlightUsers.join(','); - this.risingFilter = settings.risingFilter; + globalSettings.horizonShowDuplicateStatusNotifications; + this.horizonHighlightUsers = + globalSettings.horizonHighlightUsers.join(','); + this.risingFilter = globalSettings.risingFilter; this.risingAvailableThemes = fs .readdirSync(path.join(__dirname, 'themes')) .filter(x => x.substr(-4) === '.css') .map(x => x.slice(0, -4)); - this.risingCharacterTheme = settings.risingCharacterTheme; + this.risingCharacterTheme = globalSettings.risingCharacterTheme; this.horizonPersistentMemberFilters = - typeof (settings as any).horizonPersistentMemberFilters === 'boolean' - ? (settings as any).horizonPersistentMemberFilters + typeof (globalSettings as any).horizonPersistentMemberFilters === + 'boolean' + ? (globalSettings as any).horizonPersistentMemberFilters : false; + + // Load character overrides + this.characterOverrides = { ...core.state.characterSettings }; } async doImport(): Promise { @@ -1342,6 +1392,16 @@ } async submit(): Promise { + if (this.settingsMode === '0') { + // Save global settings + await this.submitGlobalSettings(); + } else { + // Save character overrides + await this.submitCharacterOverrides(); + } + } + + async submitGlobalSettings(): Promise { const oldRisingFilter = JSON.parse( JSON.stringify(core.state.settings.risingFilter) ); @@ -1356,9 +1416,9 @@ this.horizonSaveDraftMessagesToDiskTimer ); - const previousSettings = core.state.settings; + const previousSettings = core.state.globalSettings; - core.state.settings = { + core.state.globalSettings = { playSound: this.playSound, soundTheme: previousSettings.soundTheme, clickOpensMessage: this.clickOpensMessage, @@ -1375,7 +1435,7 @@ showAvatars: this.showAvatars, animatedEicons: this.animatedEicons, smoothMosaics: this.smoothMosaics, - soundTheme: core.state.settings.soundTheme || 'default', + soundTheme: core.state.globalSettings.soundTheme || 'default', idleTimer: isNaN(idleTimer) ? 0 : idleTimer < 0 @@ -1466,7 +1526,7 @@ ? this.risingCharacterTheme : undefined }; - console.log('SETTINGS', minAge, maxAge, core.state.settings); + console.log('GLOBAL SETTINGS', minAge, maxAge, core.state.globalSettings); const newRisingFilter = JSON.parse( JSON.stringify(core.state.settings.risingFilter) @@ -1481,6 +1541,20 @@ EventBus.$emit('configuration-update', core.state.settings); } + async submitCharacterOverrides(): Promise { + // Save character-specific overrides + core.state.characterSettings = this.characterOverrides; + + const newRisingFilter = JSON.parse( + JSON.stringify(core.state.settings.risingFilter) + ); + + // Rebuild filters if the effective risingFilter changed + this.rebuildFilters(); + + EventBus.$emit('configuration-update', core.state.settings); + } + rebuildFilters() { core.cache.profileCache.onEachInMemory(c => { const oldFiltered = c.match.isFiltered; @@ -1510,6 +1584,10 @@ return !Number.isNaN(n) && Number.isFinite(n) ? n : null; } + get currentCharacter(): string { + return core.connection.character; + } + getExceptionList(): string { return _.join(this.risingFilter.exceptionNames, '\n'); } diff --git a/chat/core.ts b/chat/core.ts index ffc514d5..7d7059c7 100644 --- a/chat/core.ts +++ b/chat/core.ts @@ -22,6 +22,7 @@ import { Conversation, Logs, Notifications, + PartialSettings, Settings, State as StateInterface } from './interfaces'; @@ -38,8 +39,37 @@ function createBBCodeParser(): BBCodeParser { return parser; } +/** + * Creates a Proxy that merges global settings with character overrides. + * Character settings take precedence; undefined values fall back to global. + * @param global The global settings object (from '_' character). + * @param character The character-specific overrides (partial settings). + * @returns A Proxy object that provides merged access to settings. + */ +function createMergedSettingsProxy( + global: Settings, + character: PartialSettings +): Settings { + return new Proxy(global, { + get(target, prop: keyof Settings) { + if (prop in character && character[prop] !== undefined) { + return character[prop]; + } + return target[prop]; + }, + set(_target, prop: keyof Settings, value) { + // Setting a property updates the character overrides + (character as any)[prop] = value; + return true; + } + }) as Settings; +} + class State implements StateInterface { _settings: Settings | undefined = undefined; + _globalSettings: Settings | undefined = undefined; + _characterSettings: PartialSettings = {}; + needsSettingsMigration: boolean = false; hiddenUsers: string[] = []; favoriteEIcons: Record = {}; @@ -49,8 +79,43 @@ class State implements StateInterface { } set settings(value: Settings) { + //For backward compatibility, setting settings updates the merged proxy and saves character settings this._settings = value; //tslint:disable-next-line:no-floating-promises + if (data.settingsStore !== undefined) + data.settingsStore.set('settings', this._characterSettings); + //Why do we need to this again? + //It was in the original version, but I'm afraid of removing it in any of the copy-paste setters. + data.bbCodeParser = createBBCodeParser(); + } + + get globalSettings(): Settings { + if (this._globalSettings === undefined) + throw new Error('Global settings load failed.'); + return this._globalSettings; + } + + set globalSettings(value: Settings) { + this._globalSettings = value; + + this._settings = createMergedSettingsProxy(value, this._characterSettings); + //tslint:disable-next-line:no-floating-promises + if (data.settingsStore !== undefined) + data.settingsStore.set('settings', value, '_'); + data.bbCodeParser = createBBCodeParser(); + } + + get characterSettings(): PartialSettings { + return this._characterSettings; + } + + set characterSettings(value: PartialSettings) { + this._characterSettings = value; + + if (this._globalSettings !== undefined) { + this._settings = createMergedSettingsProxy(this._globalSettings, value); + } + //tslint:disable-next-line:no-floating-promises if (data.settingsStore !== undefined) data.settingsStore.set('settings', value); data.bbCodeParser = createBBCodeParser(); @@ -104,19 +169,81 @@ const data = { vue.$watch(getter, callback); }, async reloadSettings(): Promise { - const s = await core.settingsStore.get('settings'); + // Load global settings from '_' character + const globalRaw = await core.settingsStore.get('settings', '_'); + + const needsMigration = globalRaw === undefined; - state._settings = _.mergeWith(new SettingsImpl(), s, (oVal, sVal) => { - if (_.isArray(oVal) && _.isArray(sVal)) { - return sVal; + const globalSettings = _.mergeWith( + new SettingsImpl(), + globalRaw, + (oVal, sVal) => { + if (_.isArray(oVal) && _.isArray(sVal)) { + return sVal; + } } - }); + ); + state._globalSettings = globalSettings; + + // Load character-specific overrides + const characterRaw = await core.settingsStore.get('settings'); + const characterSettings: PartialSettings = + characterRaw !== undefined ? (characterRaw as PartialSettings) : {}; + state._characterSettings = characterSettings; + + // Create merged settings proxy + state._settings = createMergedSettingsProxy( + globalSettings, + characterSettings + ); const hiddenUsers = await core.settingsStore.get('hiddenUsers'); state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : []; const favoriteEIcons = await core.settingsStore.get('favoriteEIcons'); state.favoriteEIcons = favoriteEIcons !== undefined ? favoriteEIcons : {}; + + //If migration needed, check for existing character settings to migrate + if (needsMigration && characterRaw !== undefined) { + //Existing character has settings but no global exists... + //set flag for UI to prompt user for migration choice + state.needsSettingsMigration = true; + } else if (needsMigration) { + //No existing settings at all, just use defaults as global + await core.settingsStore.set('settings', globalSettings, '_'); + state.needsSettingsMigration = false; + } else { + state.needsSettingsMigration = false; + } + }, + + /** + * Migrate character settings to global settings. + * @param useCurrentAsGlobal If true, current character settings become global defaults. + * If false, start fresh with default settings. + */ + async migrateToGlobalSettings(useCurrentAsGlobal: boolean): Promise { + if (useCurrentAsGlobal) { + // Use current character's settings as the global settings + const currentSettings = state._settings!; + await core.settingsStore.set('settings', currentSettings, '_'); + state._globalSettings = currentSettings; + // Clear character overrides since they're now global + state._characterSettings = {}; + await core.settingsStore.set('settings', {}); + } else { + const defaults = new SettingsImpl(); + await core.settingsStore.set('settings', defaults, '_'); + state._globalSettings = defaults; + // Keep current character settings as overrides (they're already loaded) + } + // Mark migration as complete + state.needsSettingsMigration = false; + // Recreate merged proxy + state._settings = createMergedSettingsProxy( + state._globalSettings!, + state._characterSettings + ); } }; @@ -174,6 +301,7 @@ export interface Core { readonly siteSession: SiteSession; watch(getter: (this: VueState) => T, callback: WatchHandler): void; + migrateToGlobalSettings(useCurrentAsGlobal: boolean): Promise; } const core = (data); /*tslint:disable-line:no-any*/ //hack diff --git a/chat/interfaces.ts b/chat/interfaces.ts index bb32025e..762903fa 100644 --- a/chat/interfaces.ts +++ b/chat/interfaces.ts @@ -236,7 +236,8 @@ export type AvailableSort = 'normal' | 'status' | 'gender' | 'compatibility'; export namespace Settings { export type Keys = { - settings: Settings; + /** Global settings stored under '_' are of the type "Settings",character overrides are of the type "PartialSettings" */ + settings: Settings | PartialSettings; pinned: { channels: string[]; private: string[] }; conversationSettings: { [key: string]: Conversation.Settings | undefined }; modes: { [key: string]: Channel.Mode | undefined }; @@ -259,7 +260,11 @@ export namespace Settings { character?: string ): Promise; getAvailableCharacters(): Promise>; - set(key: K, value: Keys[K]): Promise; + set( + key: K, + value: Keys[K], + character?: string + ): Promise; } export interface Settings { @@ -328,9 +333,18 @@ export namespace Settings { readonly risingCharacterTheme: string | undefined; readonly soundTheme: string; } + + /** + * Partial settings used for character-level overrides. + * Properties set to `undefined` indicate "use global value". + */ + export type PartialSettings = { + [K in keyof Settings]?: Settings[K] | undefined; + }; } export type Settings = Settings.Settings; +export type PartialSettings = Settings.PartialSettings; export interface Notifications { isInBackground: boolean; @@ -347,7 +361,13 @@ export interface Notifications { } export interface State { + /** Merged settings (global + character overrides). Use this for reading settings in logic. */ settings: Settings; + /** Global settings shared across all characters. Stored under character '_'. */ + globalSettings: Settings; + /** Character-specific overrides. Properties set to undefined use global value. */ + characterSettings: PartialSettings; + needsSettingsMigration: boolean; hiddenUsers: string[]; favoriteEIcons: Record; } diff --git a/chat/locales/en_us.json b/chat/locales/en_us.json index 336acb67..6232d890 100644 --- a/chat/locales/en_us.json +++ b/chat/locales/en_us.json @@ -598,6 +598,19 @@ "settings.charactersToGeneral": "In the nearby future, most of these settings will be moved to the global settings menu, where they will be applied globally across your characters. You will still be able to set character-level overrides though.", "settings.charactersToGeneral.generalInfo": "Looking a bit empty? In the nearby future, most of the settings in the character-specific settings (the ones you see in the chat window once you've signed in) will be moved to this general window, where you will be able to apply global default settings too. Expect a slight redesign for both menus.", "settings.charactersToGeneral.instructions": "This window itself has been redesigned and reorganized to prepare for this. Expect it soon™.", + "settings.tab.global": "Global Settings", + "settings.tab.character": "Settings for {0}", + "settings.override.true": "Override: On", + "settings.override.false": "Override: Off", + "settings.override.useGlobal": "Use global value", + "settings.override.globalOn": "Global: On", + "settings.override.globalOff": "Global: Off", + "settings.overridden": "Overridden", + "settings.migration.title": "Settings Migration", + "settings.migration.prompt": "Global settings allow you to share defaults across all characters. How would you like to set up your global settings?", + "settings.migration.useCurrent": "Use current character's settings as global defaults", + "settings.migration.startFresh": "Start with default settings", + "settings.chat": "Chat Settings", "settings.chat.bbcode": "BBCode", "settings.chat.behavior": "Behavior", "settings.chat.logging": "Logging", diff --git a/components/SettingsTriState.vue b/components/SettingsTriState.vue new file mode 100644 index 00000000..85629c8e --- /dev/null +++ b/components/SettingsTriState.vue @@ -0,0 +1,88 @@ + + + diff --git a/electron/filesystem.ts b/electron/filesystem.ts index 76456774..15489cd6 100644 --- a/electron/filesystem.ts +++ b/electron/filesystem.ts @@ -433,9 +433,10 @@ export class SettingsStore implements Settings.Store { //tslint:disable-next-line:no-async-without-await async set( key: K, - value: Settings.Keys[K] + value: Settings.Keys[K], + character?: string ): Promise { - writeFile(path.join(getSettingsDir(), key), JSON.stringify(value)); + writeFile(path.join(getSettingsDir(character), key), JSON.stringify(value)); } } From 1fb5ab33d5b9d94da098f3858e7784d4f9158b0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Feb 2026 14:37:18 +0000 Subject: [PATCH 002/184] style: auto-format with Prettier --- signatures/cla.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signatures/cla.json b/signatures/cla.json index 191e6492..38354972 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -9,4 +9,4 @@ "pullRequestNo": 555 } ] -} \ No newline at end of file +} From ea0fa4abccb58f8113080d368893bf3887e3a83a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Feb 2026 14:37:24 +0000 Subject: [PATCH 003/184] chore: add formatting commit to blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d2272f1a..fb57ae97 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -26,3 +26,6 @@ ac91b363b8b96fd766c133291ace502eb7a16acd fc39172d0e46649761044902911124f430c174ea 57b2effb293259757c02ddbfd329478f33f2e1e5 8293dc6c768a37c4423853e6a23209ffef5fee90 + +# Auto-format 2026-02-18 +1fb5ab33d5b9d94da098f3858e7784d4f9158b0f From d2a309af74c90e07881479e2eda33327c2a59f89 Mon Sep 17 00:00:00 2001 From: FatCat <82377182+FatCatClient@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:47:05 +0200 Subject: [PATCH 004/184] Various settings menu redesign things for global settings: - Updated tabs component to support vertical tabs, and distinct tab item types (temporary hack code\) - Redesigned SettingsView to use vertical tab layout, and split up various groups of settings --- chat/SettingsView.vue | 2360 ++++++++++++++++++++---------------- chat/locales/en_us.json | 4 +- components/tabs.ts | 260 +++- scss/_flist_overrides.scss | 25 + 4 files changed, 1545 insertions(+), 1104 deletions(-) diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index 6d1d895e..44220f0e 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -1,1157 +1,1387 @@ @@ -1200,7 +1430,7 @@ return { l, availableImports: [] as ReadonlyArray, - selectedTab: '0', + selectedTab: 'chat.bbcode', settingsMode: '0', importCharacter: '', characterOverrides: {} as PartialSettings, diff --git a/chat/locales/en_us.json b/chat/locales/en_us.json index 78e8a8f8..16787007 100644 --- a/chat/locales/en_us.json +++ b/chat/locales/en_us.json @@ -25,6 +25,7 @@ "action.preferences": "Preferences", "action.quit": "Quit", "action.reset": "Reset", + "action.save": "Save", "action.saveAndClose": "Save and Close", "action.saveChanges": "Save changes", "action.select": "Select", @@ -604,7 +605,6 @@ "settings.cache.memoryCount.description": "The maximum amount of characters to keep in active system memory. Higher values mean that more RAM might be used after some time.", "settings.changeOfflineColor": "Make color in chat change to grey when a user goes offline", "settings.channelMembersFilters": "Channel members filters", - "settings.character": "Character Settings", "settings.charactersToGeneral": "In the nearby future, most of these settings will be moved to the global settings menu, where they will be applied globally across your characters. You will still be able to set character-level overrides though.", "settings.charactersToGeneral.generalInfo": "Looking a bit empty? In the nearby future, most of the settings in the character-specific settings (the ones you see in the chat window once you've signed in) will be moved to this general window, where you will be able to apply global default settings too. Expect a slight redesign for both menus.", "settings.charactersToGeneral.instructions": "This window itself has been redesigned and reorganized to prepare for this. Expect it soon™.", @@ -867,6 +867,7 @@ "settings.systemLogLevel.warn": "Warn", "settings.tabs.accessibility": "Accessibility", "settings.tabs.advanced": "Advanced", + "settings.tabs.app": "App", "settings.tabs.appearance": "Appearance", "settings.tabs.behavior": "App behavior", "settings.tabs.chat": "Chat", @@ -876,6 +877,7 @@ "settings.tabs.import": "Import", "settings.tabs.look": "Look and feel", "settings.tabs.notifications": "Notifications", + "settings.tabs.pings": "Mentions and highlights", "settings.tabs.profiles": "Profiles", "settings.tabs.smartFilters": "Smart Filters ☀️", "settings.theme": "Theme", diff --git a/components/tabs.ts b/components/tabs.ts index 7d156555..8c524613 100644 --- a/components/tabs.ts +++ b/components/tabs.ts @@ -1,13 +1,83 @@ import { defineComponent, ref, computed, watch, PropType } from 'vue'; import { CreateElement, VNode } from 'vue'; +interface TabListTabItem { + readonly type: 'tab'; + readonly key: string; + readonly label: string; + readonly iconClass?: string; +} + +interface TabListLabelItem { + readonly type: 'label'; + readonly label: string; + readonly iconClass?: string; +} + +interface TabListDividerItem { + readonly type: 'divider'; +} + +type TabListItem = TabListTabItem | TabListLabelItem | TabListDividerItem; + +interface TabInputTabItem { + readonly type?: 'tab'; + readonly id?: string; + readonly key?: string; + readonly value?: string; + readonly label: string; + readonly iconClass?: string; +} + +interface TabInputLabelItem { + readonly type: 'label'; + readonly label: string; + readonly iconClass?: string; +} + +interface TabInputDividerItem { + readonly type: 'divider'; +} + +type TabInputItem = TabInputTabItem | TabInputLabelItem | TabInputDividerItem; + interface TabsProps { value?: string; - tabs?: { readonly [key: string]: string } | string[]; + tabs?: { readonly [key: string]: string } | Array; fullWidth?: boolean; tabClass?: string; } +// I'm so sorry for this. It's a (hopefully temporary!!!) placeholder. +// We didn't want to update every instance of the tabs component to use the new format yet, so we need to support both the old and the new format for now. +// This is a bit of a mess, but it should be fine as long as we remove the old format support soon™. +// Trust me, it's on the checklist for the settings PR. (#640) + +function isDividerItem(item: unknown): item is TabInputDividerItem { + return ( + typeof item === 'object' && + item !== null && + (item as { type?: string }).type === 'divider' + ); +} + +function isLabelItem(item: unknown): item is TabInputLabelItem { + return ( + typeof item === 'object' && + item !== null && + (item as { type?: string }).type === 'label' + ); +} + +function isTabItem(item: unknown): item is TabInputTabItem { + return ( + typeof item === 'object' && + item !== null && + (!('type' in (item as object)) || + (item as { type?: string }).type === 'tab') + ); +} + const Tabs = defineComponent({ name: 'Tabs', props: { @@ -17,7 +87,7 @@ const Tabs = defineComponent({ }, tabs: { type: [Object, Array] as PropType< - { readonly [key: string]: string } | string[] + { readonly [key: string]: string } | Array >, default: () => ({}) }, @@ -34,23 +104,87 @@ const Tabs = defineComponent({ setup(props: TabsProps, { emit }) { const internalValue = ref(props.value); - const children = computed(() => { - let result: { [key: string]: string }; + const items = computed(() => { const tabs = props.tabs || []; if (Array.isArray(tabs)) { - result = {}; - tabs.forEach((tab, index) => { - result[String(index)] = tab; - }); - } else { - result = tabs; + return tabs.reduce((result, tab, index) => { + if (typeof tab === 'string') { + result.push({ type: 'tab', key: String(index), label: tab }); + return result; + } + + if (isDividerItem(tab)) { + result.push({ type: 'divider' }); + return result; + } + + if (isLabelItem(tab)) { + result.push({ + type: 'label', + label: tab.label, + iconClass: tab.iconClass + }); + return result; + } + + if (isTabItem(tab)) { + const key = tab.id ?? tab.key ?? tab.value ?? String(index); + result.push({ + type: 'tab', + key, + label: tab.label, + iconClass: tab.iconClass + }); + } + + return result; + }, []); } + return Object.keys(tabs).map(key => ({ + type: 'tab' as const, + key, + label: tabs[key]! + })); + }); + + const tabsByKey = computed<{ [key: string]: TabListTabItem }>(() => { + const result: { [key: string]: TabListTabItem } = {}; + items.value.forEach(item => { + if (item.type === 'tab') { + result[item.key] = item; + } + }); return result; }); - const keys = computed(() => Object.keys(children.value)); + const keys = computed(() => + items.value.reduce((result, item) => { + if (item.type === 'tab') { + result.push(item.key); + } + return result; + }, []) + ); + + const isTabActive = (key: string) => internalValue.value === key; + + const getFirstTabKey = () => keys.value[0]; + + const shouldSelectFirstTab = () => + internalValue.value === undefined || + tabsByKey.value[internalValue.value] === undefined; + + const selectFirstTabIfNeeded = () => { + if (shouldSelectFirstTab()) { + const firstKey = getFirstTabKey(); + if (firstKey) { + internalValue.value = firstKey; + emit('input', firstKey); + } + } + }; watch( () => props.value, @@ -60,18 +194,9 @@ const Tabs = defineComponent({ ); watch( - [children, () => props.value], + [items, () => props.value], () => { - if ( - internalValue.value === undefined || - children.value[internalValue.value] === undefined - ) { - const firstKey = keys.value[0]; - if (firstKey) { - internalValue.value = firstKey; - emit('input', firstKey); - } - } + selectFirstTabIfNeeded(); }, { immediate: true } ); @@ -83,14 +208,84 @@ const Tabs = defineComponent({ return { internalValue, - children, - keys, + items, handleTabClick, fullWidth: computed(() => props.fullWidth), - tabClass: computed(() => props.tabClass) + tabClass: computed(() => props.tabClass), + isTabActive }; }, render(createElement: CreateElement): VNode { + const renderItem = (item: TabListItem, index: number) => { + if (item.type === 'divider') { + return createElement('li', { + key: `divider-${index}`, + staticClass: 'nav-item nav-divider' + }); + } + + if (item.type === 'label') { + const labelChildren: Array = []; + if (item.iconClass) { + labelChildren.push( + createElement('i', { + class: item.iconClass, + style: { marginRight: '0.35rem' } + }) + ); + } + labelChildren.push(item.label); + + return createElement( + 'li', + { key: `label-${index}`, staticClass: 'nav-item disabled' }, + [ + createElement( + 'span', + { + staticClass: 'nav-link disabled', + attrs: { 'aria-disabled': 'true' } + }, + labelChildren + ) + ] + ); + } + + const tabChildren: Array = []; + if (item.iconClass) { + tabChildren.push( + createElement('i', { + class: item.iconClass, + style: { marginRight: '0.35rem' } + }) + ); + } + tabChildren.push(item.label); + + return createElement( + 'li', + { key: `tab-${item.key}`, staticClass: 'nav-item' }, + [ + createElement( + 'a', + { + attrs: { href: '#' }, + staticClass: 'nav-link', + class: { active: this.isTabActive(item.key) }, + on: { + click: (event: MouseEvent) => { + event.preventDefault(); + this.handleTabClick(item.key); + } + } + }, + tabChildren + ) + ] + ); + }; + return createElement( 'div', { @@ -98,19 +293,8 @@ const Tabs = defineComponent({ }, [ createElement('ul', { staticClass: `nav ${this.tabClass}` }, [ - this.keys.map((key: string) => - createElement('li', { staticClass: 'nav-item' }, [ - createElement( - 'a', - { - attrs: { href: '#' }, - staticClass: 'nav-link', - class: { active: this.internalValue === key }, - on: { click: () => this.handleTabClick(key) } - }, - [this.children[key]!] - ) - ]) + this.items.map((item: TabListItem, index: number) => + renderItem(item, index) ), ...(this.fullWidth ? [] diff --git a/scss/_flist_overrides.scss b/scss/_flist_overrides.scss index b6b93fc5..05d96deb 100644 --- a/scss/_flist_overrides.scss +++ b/scss/_flist_overrides.scss @@ -157,6 +157,31 @@ input:checked { margin-bottom: -1px; } +.nav-tabs .nav-item.nav-divider { + align-self: stretch; + width: 1px; + margin: 0 0.35rem; + border-left: 1px solid $gray-300; + pointer-events: none; +} + +.nav.flex-column .nav-item.nav-divider { + width: auto; + height: 1px; + border-left: none; + border-top: 1px solid $gray-300; + margin: 0.35rem 0; +} +.nav.flex-column .nav-item.nav-divider:first-child { + margin-top: 0; +} +.nav.flex-column .nav-item.nav-divider:last-child { + margin-bottom: 0; +} +.nav.flex-column .nav-link:not(.disabled) { + margin-left: 1em; +} + #window-tabs .nav-tabs .nav-link.active { border-bottom: 1px solid $body-bg; } From ba09e4170ead0b3d495723773beb001b4c14299a Mon Sep 17 00:00:00 2001 From: FatCat <82377182+FatCatClient@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:56:22 +0200 Subject: [PATCH 005/184] Groups up true/ false chat settings into a settings mode-based parent component + some language string fixes --- chat/SettingsView.vue | 70 +++++++++++++++++---------------- chat/locales/en_us.json | 4 ++ components/SettingsCheckbox.vue | 2 +- components/SettingsRadio.vue | 2 +- components/SettingsTriState.vue | 4 +- 5 files changed, 44 insertions(+), 38 deletions(-) diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index c2dd23ba..6e67ae70 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -138,14 +138,14 @@
{{ l('warning.info') }}
- {{ l('settings.chat.mode.global') }} + {{ l('settings.mode.global') }}
{{ l('warning.info') }}
- {{ l('settings.chat.mode.character') }} + {{ l('settings.mode.character', currentCharacter || '') }}
@@ -190,17 +190,16 @@ {{ l('settings.animatedEicons') }} - - + :overrideValue="characterOverrides.animatedEicons" + :usingGlobal="isUsingGlobal()" + name="animatedEicons" + @update:globalValue="animatedEicons = $event" + @update:overrideValue=" + $set(characterOverrides, 'animatedEicons', $event) + " + >
@@ -210,17 +209,16 @@ {{ l('settings.smoothMosaics') }}
- - + :overrideValue="characterOverrides.smoothMosaics" + :usingGlobal="isUsingGlobal()" + name="smoothMosaics" + @update:globalValue="smoothMosaics = $event" + @update:overrideValue=" + $set(characterOverrides, 'smoothMosaics', $event) + " + > @@ -532,10 +530,16 @@ {{ l('settings.showAvatars') }} - + @@ -1000,13 +1004,6 @@ > - - - - - - -
@@ -1413,6 +1410,7 @@ import { BBCodeView } from '../bbcode/view'; import SettingsCheckbox from '../components/SettingsCheckbox.vue'; import SettingsTriState from '../components/SettingsTriState.vue'; + import SettingsOverride from '../components/SettingsOverride.vue'; import { UserInterfaceBBCodeParser } from '../bbcode/user-interface'; import core from './core'; import { @@ -1445,7 +1443,8 @@ 'settings-tristate': SettingsTriState, 'user-view': UserView, - 'virtual-list': VirtualList + 'virtual-list': VirtualList, + 'settings-override': SettingsOverride }, data() { return { @@ -1546,6 +1545,9 @@ } }, methods: { + isUsingGlobal(): boolean { + return this.settingsMode === '0'; + }, async load(): Promise { const globalSettings = core.state.globalSettings; this.playSound = globalSettings.playSound; diff --git a/chat/locales/en_us.json b/chat/locales/en_us.json index 16787007..c8eac654 100644 --- a/chat/locales/en_us.json +++ b/chat/locales/en_us.json @@ -629,6 +629,8 @@ "settings.chatLayoutMode.classic": "Classic", "settings.chatLayoutMode.modern": "Modern", "settings.chatLayoutMode.modernDescription": "Modern layout shows larger avatars and bubble-style messages, similar to Discord.", + "settings.checkbox": "Checkbox", + "settings.checkbox.override": "Override settings", "settings.clickOpensMessage": "Clicking users opens messages (instead of their profile)", "settings.closeToTray": "Close to tray", "settings.colorBookmarks": "Show friends/bookmarks in a different colour", @@ -795,6 +797,8 @@ "settings.messageSeparators": "Display separator lines between messages", "settings.minAge": "Characters younger than (years)", "settings.misc": "Misc", + "settings.mode.global": "These settings will apply to all characters, unless you override them.", + "settings.mode.character": "These settings will only apply to {0}.", "settings.nativeWindowShowSingleTab": "Show tab line when only one tab is open (requires restart)", "settings.nativeWindowShowSingleTab.note": "You will need to use the keyboard shortcut Ctrl+Shift+T if you want to open tabs", "settings.notifications": "Show desktop/push notifications", diff --git a/components/SettingsCheckbox.vue b/components/SettingsCheckbox.vue index 9f3a6f25..c5040eb0 100644 --- a/components/SettingsCheckbox.vue +++ b/components/SettingsCheckbox.vue @@ -2,7 +2,7 @@
Date: Sat, 25 Apr 2026 18:58:32 +0200 Subject: [PATCH 006/184] Why does 'git commit -a' not include untracked files? --- components/SettingsOverride.vue | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 components/SettingsOverride.vue diff --git a/components/SettingsOverride.vue b/components/SettingsOverride.vue new file mode 100644 index 00000000..521c09a7 --- /dev/null +++ b/components/SettingsOverride.vue @@ -0,0 +1,46 @@ + + + From f059cae9f3b73db1df0c4c0042eda106a047e164 Mon Sep 17 00:00:00 2001 From: FatCat <82377182+FatCatClient@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:12:50 +0200 Subject: [PATCH 007/184] Components for overridable list and number values + fixes some typing issues re: undefined placeholder values --- chat/SettingsView.vue | 288 ++++++++++++++++++++--------------- chat/conversations.ts | 17 ++- chat/locales/en_us.json | 9 +- components/SettingsArray.vue | 60 ++++++++ components/SettingsInput.vue | 64 ++++++++ 5 files changed, 304 insertions(+), 134 deletions(-) create mode 100644 components/SettingsArray.vue create mode 100644 components/SettingsInput.vue diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index 6e67ae70..d018926f 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -173,13 +173,18 @@
{{ l('settings.chat.bbcode') }}
- -
@@ -233,10 +238,16 @@ {{ l('settings.bbCodeBar') }}
- +
@@ -247,10 +258,16 @@ {{ l('settings.enterSend') }} - + @@ -307,13 +324,16 @@ -
@@ -825,23 +845,31 @@
- -
- -
@@ -1411,7 +1439,10 @@ import SettingsCheckbox from '../components/SettingsCheckbox.vue'; import SettingsTriState from '../components/SettingsTriState.vue'; import SettingsOverride from '../components/SettingsOverride.vue'; + import SettingsInput from '../components/SettingsInput.vue'; + import SettingsArray from '../components/SettingsArray.vue'; import { UserInterfaceBBCodeParser } from '../bbcode/user-interface'; + import { Settings as DefaultSettings } from './common'; import core from './core'; import { Settings as SettingsInterface, @@ -1439,14 +1470,16 @@ tabs: Tabs, bbcode: BBCodeView(bbcodeParser), 'settings-checkbox': SettingsCheckbox, - 'settings-tristate': SettingsTriState, - 'user-view': UserView, 'virtual-list': VirtualList, - 'settings-override': SettingsOverride + 'settings-override': SettingsOverride, + 'settings-input': SettingsInput, + 'settings-array': SettingsArray }, data() { + const defaultSettings = new DefaultSettings(); + return { l, availableImports: [] as ReadonlyArray, @@ -1454,66 +1487,80 @@ settingsMode: '0', importCharacter: '', characterOverrides: {} as PartialSettings, - playSound: undefined as any as boolean, - clickOpensMessage: undefined as any as boolean, - disallowedTags: undefined as any as string, - notifications: undefined as any as boolean, - highlight: undefined as any as boolean, - highlightWords: undefined as any as string, - showAvatars: undefined as any as boolean, - animatedEicons: undefined as any as boolean, - smoothMosaics: undefined as any as boolean, - idleTimer: undefined as any as string, - messageSeparators: undefined as any as boolean, - eventMessages: undefined as any as boolean, - joinMessages: undefined as any as boolean, - alwaysNotify: undefined as any as boolean, - logMessages: undefined as any as boolean, - logAds: undefined as any as boolean, - fontSize: undefined as any as string, - showNeedsReply: undefined as any as boolean, - enterSend: undefined as any as boolean, - colorBookmarks: undefined as any as boolean, - showPerCharacterFriends: undefined as any as boolean, - hideNonCharacterFriends: undefined as any as boolean, - bbCodeBar: undefined as any as boolean, - - risingAdScore: undefined as any as boolean, - risingLinkPreview: undefined as any as boolean, - risingAutoCompareKinks: undefined as any as boolean, - - risingAutoExpandCustomKinks: undefined as any as boolean, - risingCharacterPreview: undefined as any as boolean, - risingComparisonInUserMenu: undefined as any as boolean, - risingComparisonInSearch: undefined as any as boolean, - - risingShowUnreadOfflineCount: undefined as any as boolean, - risingColorblindMode: undefined as any as boolean, - - risingShowPortraitNearInput: undefined as any as boolean, - risingShowPortraitInMessage: undefined as any as boolean, - risingShowHighQualityPortraits: undefined as any as boolean, - horizonMessagePortraitHighQuality: undefined as any as boolean, - horizonShowCustomCharacterColors: undefined as any as boolean, - horizonShowDeveloperBadges: undefined as any as boolean, - horizonShowGenderMarker: undefined as any as boolean, - horizonGenderMarkerOrigColor: undefined as any as boolean, - horizonChangeOfflineColor: undefined as any as boolean, - horizonNotifyFriendSignIn: undefined as any as boolean, - horizonShowSigninNotifications: undefined as any as boolean, - horizonShowDuplicateStatusNotifications: undefined as any as boolean, - horizonHighlightUsers: undefined as any as string, - chatLayoutMode: undefined as any as 'classic' | 'modern', - messageGrouping: undefined as any as boolean, - forceQuickConvoList: undefined as any as boolean, - horizonUseColorPicker: undefined as any as boolean, - - horizonCacheDraftMessages: undefined as any as boolean, - horizonSaveDraftMessagesToDiskTimer: undefined as any as string, - - risingFilter: {} as any as SmartFilterSettings, - - horizonPersistentMemberFilters: undefined as any as boolean, + playSound: defaultSettings.playSound, + clickOpensMessage: defaultSettings.clickOpensMessage, + disallowedTags: [] as ReadonlyArray, + notifications: defaultSettings.notifications, + highlight: defaultSettings.highlight, + highlightWords: [] as ReadonlyArray, + showAvatars: defaultSettings.showAvatars, + animatedEicons: defaultSettings.animatedEicons, + smoothMosaics: defaultSettings.smoothMosaics, + idleTimer: 0 as number, + messageSeparators: defaultSettings.messageSeparators, + eventMessages: defaultSettings.eventMessages, + joinMessages: defaultSettings.joinMessages, + alwaysNotify: defaultSettings.alwaysNotify, + logMessages: defaultSettings.logMessages, + logAds: defaultSettings.logAds, + fontSize: defaultSettings.fontSize, + showNeedsReply: defaultSettings.showNeedsReply, + enterSend: defaultSettings.enterSend, + colorBookmarks: defaultSettings.colorBookmarks, + showPerCharacterFriends: defaultSettings.showPerCharacterFriends, + hideNonCharacterFriends: defaultSettings.hideNonCharacterFriends, + bbCodeBar: defaultSettings.bbCodeBar, + + risingAdScore: defaultSettings.risingAdScore, + risingLinkPreview: defaultSettings.risingLinkPreview, + risingAutoCompareKinks: defaultSettings.risingAutoCompareKinks, + + risingAutoExpandCustomKinks: + defaultSettings.risingAutoExpandCustomKinks, + risingCharacterPreview: defaultSettings.risingCharacterPreview, + risingComparisonInUserMenu: defaultSettings.risingComparisonInUserMenu, + risingComparisonInSearch: defaultSettings.risingComparisonInSearch, + + risingShowUnreadOfflineCount: + defaultSettings.risingShowUnreadOfflineCount, + risingColorblindMode: defaultSettings.risingColorblindMode, + + risingShowPortraitNearInput: + defaultSettings.risingShowPortraitNearInput, + risingShowPortraitInMessage: + defaultSettings.risingShowPortraitInMessage, + risingShowHighQualityPortraits: + defaultSettings.risingShowHighQualityPortraits, + horizonMessagePortraitHighQuality: + defaultSettings.horizonMessagePortraitHighQuality, + horizonShowCustomCharacterColors: + defaultSettings.horizonShowCustomCharacterColors, + horizonShowDeveloperBadges: defaultSettings.horizonShowDeveloperBadges, + horizonShowGenderMarker: defaultSettings.horizonShowGenderMarker, + horizonGenderMarkerOrigColor: + defaultSettings.horizonGenderMarkerOrigColor, + horizonChangeOfflineColor: defaultSettings.horizonChangeOfflineColor, + horizonNotifyFriendSignIn: defaultSettings.horizonNotifyFriendSignIn, + horizonShowSigninNotifications: + defaultSettings.horizonShowSigninNotifications, + horizonShowDuplicateStatusNotifications: + defaultSettings.horizonShowDuplicateStatusNotifications, + horizonHighlightUsers: [] as ReadonlyArray, + chatLayoutMode: defaultSettings.chatLayoutMode, + messageGrouping: defaultSettings.messageGrouping, + forceQuickConvoList: defaultSettings.forceQuickConvoList, + horizonUseColorPicker: defaultSettings.horizonUseColorPicker, + + horizonCacheDraftMessages: defaultSettings.horizonCacheDraftMessages, + horizonSaveDraftMessagesToDiskTimer: + defaultSettings.horizonSaveDraftMessagesToDiskTimer, + + risingFilter: _.cloneDeep( + defaultSettings.risingFilter + ) as SmartFilterSettings, + + horizonPersistentMemberFilters: + defaultSettings.horizonPersistentMemberFilters, risingAvailableThemes: [] as ReadonlyArray, risingCharacterTheme: undefined as string | undefined, @@ -1549,24 +1596,30 @@ return this.settingsMode === '0'; }, async load(): Promise { - const globalSettings = core.state.globalSettings; + const globalSettings = core.state.globalSettings as any; + const defaultSettings = new DefaultSettings(); + + for (const [key, value] of Object.entries(defaultSettings)) { + if (globalSettings[key] === undefined) globalSettings[key] = value; + } + this.playSound = globalSettings.playSound; this.clickOpensMessage = globalSettings.clickOpensMessage; - this.disallowedTags = globalSettings.disallowedTags.join(','); + this.disallowedTags = globalSettings.disallowedTags ?? []; this.notifications = globalSettings.notifications; this.highlight = globalSettings.highlight; - this.highlightWords = globalSettings.highlightWords.join(','); + this.highlightWords = globalSettings.highlightWords ?? []; this.showAvatars = globalSettings.showAvatars; this.animatedEicons = globalSettings.animatedEicons; this.smoothMosaics = globalSettings.smoothMosaics; - this.idleTimer = globalSettings.idleTimer.toString(); + this.idleTimer = globalSettings.idleTimer ?? 0; this.messageSeparators = globalSettings.messageSeparators; this.eventMessages = globalSettings.eventMessages; this.joinMessages = globalSettings.joinMessages; this.alwaysNotify = globalSettings.alwaysNotify; this.logMessages = globalSettings.logMessages; this.logAds = globalSettings.logAds; - this.fontSize = globalSettings.fontSize.toString(); + this.fontSize = globalSettings.fontSize; this.showNeedsReply = globalSettings.showNeedsReply; this.enterSend = globalSettings.enterSend; this.colorBookmarks = globalSettings.colorBookmarks; @@ -1618,7 +1671,7 @@ this.horizonCacheDraftMessages = globalSettings.horizonCacheDraftMessages; this.horizonSaveDraftMessagesToDiskTimer = - globalSettings.horizonSaveDraftMessagesToDiskTimer.toString(); + globalSettings.horizonSaveDraftMessagesToDiskTimer; this.horizonNotifyFriendSignIn = globalSettings.horizonNotifyFriendSignIn; @@ -1626,8 +1679,7 @@ globalSettings.horizonShowSigninNotifications; this.horizonShowDuplicateStatusNotifications = globalSettings.horizonShowDuplicateStatusNotifications; - this.horizonHighlightUsers = - globalSettings.horizonHighlightUsers.join(','); + this.horizonHighlightUsers = globalSettings.horizonHighlightUsers ?? []; this.risingFilter = globalSettings.risingFilter; this.risingAvailableThemes = fs @@ -1689,9 +1741,6 @@ JSON.stringify(core.state.settings.risingFilter) ); - const idleTimer = parseInt(this.idleTimer, 10); - const fontSize = parseFloat(this.fontSize); - const minAge = this.getAsNumber(this.risingFilter.minAge); const maxAge = this.getAsNumber(this.risingFilter.maxAge); @@ -1706,39 +1755,27 @@ soundTheme: previousSettings.soundTheme || 'default', soundThemeSoundVolumes: previousSettings.soundThemeSoundVolumes, clickOpensMessage: this.clickOpensMessage, - disallowedTags: this.disallowedTags - .split(',') - .map(x => x.trim()) - .filter(x => x.length), + disallowedTags: this.disallowedTags, notifications: this.notifications, highlight: this.highlight, - highlightWords: this.highlightWords - .split(',') - .map(x => x.trim()) - .filter(x => x.length), + highlightWords: this.highlightWords, showAvatars: this.showAvatars, animatedEicons: this.animatedEicons, smoothMosaics: this.smoothMosaics, - idleTimer: isNaN(idleTimer) - ? 0 - : idleTimer < 0 + idleTimer: + this.idleTimer < 0 ? 0 - : idleTimer > 1440 + : this.idleTimer > 1440 ? 1440 - : idleTimer, + : this.idleTimer, messageSeparators: this.messageSeparators, eventMessages: this.eventMessages, joinMessages: this.joinMessages, alwaysNotify: this.alwaysNotify, logMessages: this.logMessages, logAds: this.logAds, - fontSize: isNaN(fontSize) - ? 14 - : fontSize < 10 - ? 10 - : fontSize > 24 - ? 24 - : fontSize, + fontSize: + this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize, showNeedsReply: this.showNeedsReply, enterSend: this.enterSend, colorBookmarks: this.colorBookmarks, @@ -1777,10 +1814,7 @@ horizonShowSigninNotifications: this.horizonShowSigninNotifications, horizonShowDuplicateStatusNotifications: this.horizonShowDuplicateStatusNotifications, - horizonHighlightUsers: this.horizonHighlightUsers - .split(',') - .map(x => x.trim()) - .filter(x => x.length), + horizonHighlightUsers: this.horizonHighlightUsers, chatLayoutMode: this.chatLayoutMode, messageGrouping: this.messageGrouping, forceQuickConvoList: this.forceQuickConvoList, diff --git a/chat/conversations.ts b/chat/conversations.ts index f323922a..4f7be3d2 100644 --- a/chat/conversations.ts +++ b/chat/conversations.ts @@ -1273,9 +1273,20 @@ export default function (this: any): Interfaces.State { const words = conversation.settings.highlightWords.slice(); const watchedCharacters = conversation.settings.horizonHighlightUsers.slice(); - if (conversation.settings.defaultHighlights) - words.push(...core.state.settings.highlightWords); - watchedCharacters.push(...core.state.settings.horizonHighlightUsers); + if (conversation.settings.defaultHighlights) { + const globalHighlightWords = core.state.globalSettings.highlightWords; + const characterHighlightWords = + core.state.characterSettings.highlightWords ?? []; + words.push(...globalHighlightWords, ...characterHighlightWords); + } + const globalWatchedCharacters = + core.state.globalSettings.horizonHighlightUsers; + const characterWatchedCharacters = + core.state.characterSettings.horizonHighlightUsers ?? []; + watchedCharacters.push( + ...globalWatchedCharacters, + ...characterWatchedCharacters + ); if ( (conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight) || diff --git a/chat/locales/en_us.json b/chat/locales/en_us.json index c8eac654..d8986940 100644 --- a/chat/locales/en_us.json +++ b/chat/locales/en_us.json @@ -580,6 +580,7 @@ "settings.appearance.layout": "Page layout", "settings.appearance.messages": "Message appearance", "settings.appearance.users": "User appearance", + "settings.arrayInstructions": "Split values with commas or tabs.", "settings.autoReply": "Send an automatic 'no thank you' response to matching characters if they message you", "settings.autoReplyCustom": "Use a custom message defined below instead of the default message", "settings.autoReplyNote": "You will still see messages unless you have the \"Hide private channel messages\" option above selected. Even then, if they send a second message it will bypass the restriction and show you their message. These messages are limited to 10000 characters.", @@ -644,7 +645,7 @@ "settings.customCss.warning": "Warning: Don't enter anything in here from people you do not fully trust.", "settings.dangerZone": "Danger Zone!", "settings.defaultHighlights": "Use global highlight words", - "settings.disallowedTags": "Disallowed BBCode tags (comma-separated)", + "settings.disallowedTags": "Disallowed BBCode tags", "settings.displayLanguage": "Display Language", "settings.displayLanguage.note": "Languages might not be fully translated yet.\nWe have a tool for tracking progress, so you do not need to report missing translations.", "settings.enterAge": "Enter age", @@ -692,9 +693,9 @@ "settings.hideAds.empty": "You aren't currently hiding the ads of any users.", "settings.hideNonCharacterFriends": "Additionally hide non-character friends", "settings.highlight": "Notify for messages containing your name", - "settings.highlightUsers": "Notify when the following characters send a message in a channel (comma-separated)", - "settings.highlightUsers.conversation": "Notify when the following characters send a message (comma-separated)", - "settings.highlightWords": "Custom highlight notify words (comma-separated)", + "settings.highlightUsers": "Notify when the following characters send a message in a channel", + "settings.highlightUsers.conversation": "Notify when the following characters send a message", + "settings.highlightWords": "Custom highlight notify words", "settings.horizon.persistentMemberFilters": "Persistent Filters", "settings.horizon.persistentMemberFilters.help": "When enabled, channel member filters (gender and member sort) will be saved per-character and restored when you sign in as that character.", "settings.horizonCacheDraftMessages": "Automatically save and restore in-progress messages while using Horizon (change requires new tab)", diff --git a/components/SettingsArray.vue b/components/SettingsArray.vue new file mode 100644 index 00000000..7d1c6377 --- /dev/null +++ b/components/SettingsArray.vue @@ -0,0 +1,60 @@ + + + diff --git a/components/SettingsInput.vue b/components/SettingsInput.vue new file mode 100644 index 00000000..64dfb9d0 --- /dev/null +++ b/components/SettingsInput.vue @@ -0,0 +1,64 @@ + + From 52933ab0bcc4e475408caa87d8536add28b01507 Mon Sep 17 00:00:00 2001 From: FatCat <82377182+FatCatClient@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:26:46 +0200 Subject: [PATCH 008/184] Converts all character settings custom checkboxes to overridable ones --- chat/SettingsView.vue | 711 +++++++++++++++++++++++++++++++----------- 1 file changed, 529 insertions(+), 182 deletions(-) diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index d018926f..121c37ea 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -204,7 +204,7 @@ @update:overrideValue=" $set(characterOverrides, 'animatedEicons', $event) " - > + />
@@ -223,7 +223,7 @@ @update:overrideValue=" $set(characterOverrides, 'smoothMosaics', $event) " - > + />
@@ -247,7 +247,7 @@ @update:overrideValue=" $set(characterOverrides, 'bbCodeBar', $event) " - > + /> @@ -267,7 +267,7 @@ @update:overrideValue=" $set(characterOverrides, 'enterSend', $event) " - > + /> @@ -278,11 +278,17 @@ {{ l('settings.horizonUseColorPicker') }} - + /> @@ -296,10 +302,22 @@ {{ l('settings.showPortraitNearInput') }} - + @@ -314,10 +332,16 @@ {{ l('settings.clickOpensMessage') }} - +
@@ -343,10 +367,16 @@ {{ l('settings.showNeedsReply') }}
- +
@@ -359,10 +389,22 @@ {{ l('settings.risingShowUnreadOfflineCount') }}
- + @@ -373,10 +415,16 @@ {{ l('settings.colorBookmarks') }} - + @@ -387,10 +435,16 @@ {{ l('settings.showPerCharacterFriends') }} - + @@ -401,10 +455,16 @@ {{ l('settings.hideNonCharacterFriends') }} - + @@ -419,10 +479,16 @@ {{ l('settings.logMessages') }} - +
@@ -432,10 +498,16 @@ {{ l('settings.logAds') }}
- + @@ -450,10 +522,20 @@ {{ l('settings.horizonCacheDraftMessages') }} - + @@ -464,13 +546,24 @@ > {{ l('settings.horizonSaveDraftMessagesToDiskTimer') }} - @@ -485,10 +578,16 @@ {{ l('settings.preview.link') }} - + @@ -499,10 +598,16 @@ {{ l('settings.preview.character') }} - + @@ -514,13 +619,16 @@ -
@@ -573,10 +681,22 @@ {{ l('settings.showHighQualityPortraits') }}
- + @@ -587,10 +707,16 @@ {{ l('settings.forceQuickConvoList') }} - + @@ -602,6 +728,9 @@ + Date: Sat, 24 Oct 2026 13:42:33 +0000 Subject: [PATCH 122/184] Translated using Weblate (Italian) Currently translated at 100.0% (1193 of 1193 strings) Translation: Horizon/UI Translate-URL: https://translate.horizn.moe/projects/horizon/User-Interface/it/ --- chat/locales/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chat/locales/it.json b/chat/locales/it.json index a6579758..3045fccb 100644 --- a/chat/locales/it.json +++ b/chat/locales/it.json @@ -326,6 +326,7 @@ "commands.setmode.param0.help": "Modalità stanza valida, ossia \"ads\", \"chat\" or \"both\" (Pubblicità, chat o entrambi).", "commands.setowner": "Imposta proprietario canale", "commands.setowner.help": "Imposta un altro personaggio come proprietario di un canale. Il proprietario precedente sarà declassato a membro.", + "commands.spacedCommand": "\"/{command}\" ha uno spazio in fronte, sarà inviato come messaggio invece di eseguire il comando. Premi Invia ancora per mandarlo a prescindere.", "commands.status": "Imposta stato", "commands.status.help": "Imposta il tuo stato assieme ad un messaggio opzionale.", "commands.status.param0": "Stato", @@ -789,7 +790,7 @@ "settings.flashWindow": "Abilita lampeggio nella barra applicazioni", "settings.flashWindow.note": "Su Linux questa opzione potrebbe non essere supportata dal tuo window manager o il tuo ambiente desktop.", "settings.fontSize": "Dimensione font", - "settings.forceNativeWindowControls": "Forza controlli di finestra nativi (richiede un riavvio)", + "settings.forceNativeWindowControls": "Forza la barra nativa della finestra (richiede un riavvio)", "settings.forceQuickConvoList": "Forza l'elenco delle conversazioni 'scambio rapido' in stile mobile", "settings.genderIconUseOriginalColor": "Colora l'icona di genere con il colore di genere originale invece del colore personalizzato del nome", "settings.hideAds.empty": "Non stai nascondendo pubblicità di nessun utente al moment.", @@ -1156,6 +1157,7 @@ "unit.years": "Anni", "update.autoInstall": "Scarica e installa automaticamente gli aggiornamenti in futuro", "update.installOnQuit.note": "L'aggiornamento si installa automaticamente la prossima volta che riavvii l'app.", + "update.noUpdateAvailable": "Nessun aggiornamento disponibile. Stai usando la versione più recente di Horizon.", "update.restart.confirm": "Versione {version} è stata scaricata.\nVuoi riavviare e installare l'aggiornamento ora?", "update.restart.confirm.connected": "Versione {version} è stata scaricata. ma sei ancora connesso alla chat.\nSei sicuro di voler riavviare e installare l'aggiornamento ora?", "update.splash.updating": "Aggiornando Horizon...", From cd90acb8744a278109a9028504d28ce477f136be Mon Sep 17 00:00:00 2001 From: FatCat Date: Sat, 24 Oct 2026 16:12:56 +0200 Subject: [PATCH 123/184] fix: User avatar formatting in match report Closes #997 --- site/character_page/match-report.vue | 9 --------- 1 file changed, 9 deletions(-) diff --git a/site/character_page/match-report.vue b/site/character_page/match-report.vue index 3b846f75..40180829 100644 --- a/site/character_page/match-report.vue +++ b/site/character_page/match-report.vue @@ -93,15 +93,6 @@ }); const getAvatarUrl = (name: string) => { - const c = core.characters.get(name); - - if ( - c.overrides.avatarUrl && - core.state.settings.risingShowHighQualityPortraits - ) { - return c.overrides.avatarUrl; - } - return Utils.avatarURL(name); }; From 1fd5025a7a5927ada7f59b6ea771ae83e984b761 Mon Sep 17 00:00:00 2001 From: Danny IW Date: Sat, 24 Oct 2026 14:28:35 +0000 Subject: [PATCH 124/184] Translated using Weblate (Spanish) Currently translated at 100.0% (1193 of 1193 strings) Translation: Horizon/UI Translate-URL: https://translate.horizn.moe/projects/horizon/User-Interface/es/ --- chat/locales/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chat/locales/es.json b/chat/locales/es.json index 67f15ce0..914cd5d8 100644 --- a/chat/locales/es.json +++ b/chat/locales/es.json @@ -326,6 +326,7 @@ "commands.setmode.param0.help": "Un modo de sala válido: “ads”, “chat” o “both”", "commands.setowner": "Asignar propietario del canal", "commands.setowner.help": "Asigna el propietario de un canal a otro personaje. El propietario anterior pasará a ser un miembro.", + "commands.spacedCommand": "\"/{command}\" tiene un espacio en frente, será enviado como un mensaje en vez de ejecutarlo como un comando. Pulsa Intro de nuevo para enviarlo.", "commands.status": "Establecer estado", "commands.status.help": "Establece tu estado junto con un mensaje opcional.", "commands.status.param0": "Estado", @@ -789,7 +790,7 @@ "settings.flashWindow": "Activar parpadeo en la barra de tareas", "settings.flashWindow.note": "En Linux, esta característica puede que no esté soportada por tu administrador de ventanas o por el entorno de escritorio.", "settings.fontSize": "Tamaño de fuente", - "settings.forceNativeWindowControls": "Forzar los controles de ventana nativos (requiere reiniciar)", + "settings.forceNativeWindowControls": "Forzar la brrra de título de ventana nativa (requiere reiniciar)", "settings.forceQuickConvoList": "Forzar lista de conversaciones tipo “cambio rápido” estilo móvil", "settings.genderIconUseOriginalColor": "Hacer que el icono de género use el color de género original en lugar del color personalizado del nombre", "settings.hideAds.empty": "Actualmente no estás ocultando los anuncios de ningún usuario.", From a2f5ca2ec25113a5867538cf91eabd23d5a25dd3 Mon Sep 17 00:00:00 2001 From: FatCat Date: Sat, 24 Oct 2026 19:30:20 +0200 Subject: [PATCH 125/184] Global settings: Initial transplant of some app settings to global settings menu --- chat/SettingsView.vue | 459 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 445 insertions(+), 14 deletions(-) diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index 44688b3f..a562ec4d 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -150,18 +150,418 @@
{{ l('warning.info') }}
- Placeholder for global app settings. These are still in the - bespoke window for now. + Placeholder for general app settings. The stuff inside this tab + is still in the bespoke window for now. +
+
+
+
+
+ {{ l('settings.theme') }} +
+
+
+ + +
+
+
+ +
+
+ + + +
+ +
+ {{ l('settings.theme.textColors') }} +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
{{ l('user.profile') }}
+
+
+ + +
+
+
+ +
+ {{ + l( + `settings.profileViewerGalleryType.note.${generalSettings.profileViewerGalleryType}` + ) + }} +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+ {{ l('settings.horizonForceAsciiProfiles.note') }} +
+
+
+ +
{{ l('settings.behavior.chat') }}
+
+ +
+ {{ `${l('settings.logDir.note.onedrive')} ` }} +
+ +
+
{{ l('settings.behavior.window') }}
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+ {{ l('settings.nativeWindowShowSingleTab.note') }} +
+
+
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+ + {{ + l('settings.reducedMotion.description') + }}
@@ -685,7 +1085,7 @@ {{ l('settings.useDefaultTheme') }} - @@ -1803,7 +2203,7 @@ - - diff --git a/electron/browser_windows.ts b/electron/browser_windows.ts index c284d78f..012b28e3 100644 --- a/electron/browser_windows.ts +++ b/electron/browser_windows.ts @@ -792,74 +792,6 @@ export function closeUpdaterSplash(): void { updaterSplash = undefined; } -/** - * Creates a new settings window. - * @function - * @param {GeneralSettings} settings - * The general settings for the application, modified by the user in the settings window. - * @param {ImporterHint} ImporterHint - * Handles what and how we should import - * @param {electron.BrowserWindow} parentWindow - * The parent window for the settings window. This is used to create a modal dialog. - * @returns {electron.BrowserWindow | undefined} - * Returns the newly created settings window or undefined if creation failed. - */ -export function createSettingsWindow( - settings: GeneralSettings, - ImporterHint: ImporterHint, - parentWindow: electron.BrowserWindow -): electron.BrowserWindow | undefined { - let desiredHeight = 570; - let desiredWidth = 885; - - const windowProperties: electron.BrowserWindowConstructorOptions = { - center: true, - show: false, - icon: process.platform === 'win32' ? winIcon : pngIcon, - frame: false, - width: desiredWidth, - minWidth: desiredWidth, - height: desiredHeight, - minHeight: desiredHeight, - resizable: true, - modal: true, - parent: parentWindow, - maximizable: false, - webPreferences: { - webviewTag: true, - nodeIntegration: true, - nodeIntegrationInWorker: true, - spellcheck: true, - enableRemoteModule: true, - contextIsolation: false, - partition: 'persist:fchat' - } as any - }; - - if (process.platform === 'darwin') { - windowProperties.titleBarStyle = 'hiddenInset'; - } else if (settings.forceNativeWindowControls) { - windowProperties.frame = true; - windowProperties.minimizable = false; - windowProperties.maximizable = false; - windowProperties.autoHideMenuBar = true; - } - const browserWindow = new electron.BrowserWindow(windowProperties); - remoteMain.enable(browserWindow.webContents); - browserWindow.loadFile(path.join(__dirname, 'settings.html'), { - query: { - settings: JSON.stringify(settings), - import: ImporterHint === 'none' ? '' : ImporterHint - } - }); - - browserWindow.once('ready-to-show', () => { - browserWindow.show(); - }); - - return browserWindow; -} - /** * Creates and configures a modal changelog window as a child of the specified parent Electron BrowserWindow. * diff --git a/electron/main.ts b/electron/main.ts index f72435f8..85c7e692 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1429,25 +1429,13 @@ async function onReady(): Promise { id: 'newTab', accelerator: 'CmdOrCtrl+Shift+T' }, - //TODO remove this once we move the actual app settings stuff to the unified SettingsView { label: l('action.preferences'), click: (_m, window: electron.BrowserWindow) => { - browserWindows.createSettingsWindow( - settings, - shouldImportSettings ? 'auto' : 'none', - window - ); + window.webContents.send('settings-menu'); }, accelerator: 'CmdOrCtrl+,' }, - { - label: l('action.preferences'), - click: (_m, window: electron.BrowserWindow) => { - window.webContents.send('settings-menu'); - } - // accelerator: 'CmdOrCtrl+,' - }, { label: l('fixLogs.action'), click: (_m, window: electron.BrowserWindow) => diff --git a/electron/settings.html b/electron/settings.html deleted file mode 100644 index eb2838a5..00000000 --- a/electron/settings.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Settings - - - -
- - - - diff --git a/electron/settings.ts b/electron/settings.ts deleted file mode 100644 index 750e3718..00000000 --- a/electron/settings.ts +++ /dev/null @@ -1,28 +0,0 @@ -//For potential git history reasons, this file used to be named "browser_options.ts" prior to f257b4c6a9d6fc06c1a6f7354e38c2dbd7bc69f6 -import * as qs from 'querystring'; -import log from 'electron-log'; //tslint:disable-line:match-default-export-name - -import { GeneralSettings } from './common'; -import BrowserOption from './Settings.vue'; - -log.info('init.settings'); - -const params = <{ [key: string]: string | undefined }>( - qs.parse(window.location.search.substr(1)) -); -const settings = JSON.parse(params['settings']!); - -const logLevel = process.env.NODE_ENV === 'production' ? 'info' : 'silly'; - -log.transports.file.level = settings.risingSystemLogLevel || logLevel; -log.transports.console.level = settings.risingSystemLogLevel || logLevel; -log.transports.file.maxSize = 5 * 1024 * 1024; - -log.info('init.settings.vue'); - -new BrowserOption({ - el: '#browserOption', - data: { settings } -}); - -log.debug('init.settings.vue.done'); diff --git a/electron/webpack.config.js b/electron/webpack.config.js index ad7ff3d3..8b402f6a 100644 --- a/electron/webpack.config.js +++ b/electron/webpack.config.js @@ -102,11 +102,6 @@ const mainConfig = { path.join(__dirname, 'window.html'), path.join(__dirname, 'build', 'tray@2x.png') ], - settings: [ - path.join(__dirname, 'settings.ts'), - path.join(__dirname, 'settings.html'), - path.join(__dirname, 'build', 'tray@2x.png') - ], changelog: [ path.join(__dirname, 'changelog.ts'), path.join(__dirname, 'changelog.html'), From de43a346087b9d1b9263e98a0f69c4a25972964b Mon Sep 17 00:00:00 2001 From: FatCat Date: Fri, 30 Oct 2026 11:26:05 +0100 Subject: [PATCH 149/184] Development changelog features for global settings --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 532ad599..a0810bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Development + +- Added a new member to `core` for platform-related functionality, to help decouple some Electron-specific code from the chat module. [[Commit]](https://github.com/Fchat-Horizon/Horizon/commit/8090bccd8f4fd0f91ccdb900d990cd73ae829505) +- Spellcheck language settings are now decoupled from Electron, and are automatically hidden from the UI if the platform does not expose any available languages.. [[Commit]](https://github.com/Fchat-Horizon/Horizon/commit/16834353e94bad55405850bb11669ce1dfc4d004) + ## [2.4.1] - 2026-10-06 ### Changed From 17416583673e6321d912eff090ed8d69f56528b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Oct 2026 16:01:15 +0000 Subject: [PATCH 150/184] style: auto-format with Prettier --- chat/zip.ts | 2 +- site/site-session.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/chat/zip.ts b/chat/zip.ts index b1c37efe..85646978 100644 --- a/chat/zip.ts +++ b/chat/zip.ts @@ -39,7 +39,7 @@ export default class Zip { ? 5 : 6; length += l; - let byte = l === 1 ? c : (0xff00 >> l) % 256 | (c >>> ((l - 1) * 6)); + let byte = l === 1 ? c : ((0xff00 >> l) % 256) | (c >>> ((l - 1) * 6)); --l; while (true) { crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff]; diff --git a/site/site-session.ts b/site/site-session.ts index f014b47e..94ddf365 100644 --- a/site/site-session.ts +++ b/site/site-session.ts @@ -12,8 +12,10 @@ export interface SiteSessionInterface { stop(): Promise; } -export interface SiteSessionInterfaceCollection - extends Record { +export interface SiteSessionInterfaceCollection extends Record< + string, + SiteSessionInterface +> { notes: NoteChecker; } From 1f37206803e0d253f5bd7389f726e90327927dce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Oct 2026 16:01:16 +0000 Subject: [PATCH 151/184] chore: add formatting commit to blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 9cdc406a..f89d5146 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -57,3 +57,6 @@ fbda6ec72a7aca61f1b28496935a3cd1f8866b6d # Auto-format 2026-10-04 00a8662195ac491c1b0943f6c214f1f41fb11013 + +# Auto-format 2026-10-30 +17416583673e6321d912eff090ed8d69f56528b1 From a832077900e5c5b4ef9a12af136e8af773be8302 Mon Sep 17 00:00:00 2001 From: FatCat Date: Fri, 30 Oct 2026 17:38:00 +0100 Subject: [PATCH 152/184] fix: Scrollbars being unstyled on ALL platforms instead of just macOS --- scss/themes/_chat.scss | 62 +++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/scss/themes/_chat.scss b/scss/themes/_chat.scss index e921d30a..9b2f2756 100644 --- a/scss/themes/_chat.scss +++ b/scss/themes/_chat.scss @@ -135,7 +135,7 @@ $selection-color: color-contrast($primary); } } -body::not(.platform-darwin) * { +div#page:not(.platform-darwin) * { &::-webkit-scrollbar-track { --horizon-scrollbar-track-box-shadow: inset 0 0 8px var(--bs-border-color); --horizon-scrollbar-track-border-radius: 10px; @@ -149,48 +149,60 @@ body::not(.platform-darwin) * { &::-webkit-scrollbar { --horizon-scrollbar-thickness: 8px; - width: --horizon-scrollbar-thickness; - height: --horizon-scrollbar-thickness; + width: var(--horizon-scrollbar-thickness); + height: var(--horizon-scrollbar-thickness); } &::-webkit-scrollbar-thumb { - --horizon-scrollbar-thumb-bg: $gray-300; + --horizon-scrollbar-thumb-bg: #{$gray-300}; --horizon-scrollbar-thumb-border-radius: 10px; --horizon-scrollbar-thumb-box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8); + --horizon-scrollbar-thumb-display: initial; border-radius: var(--horizon-scrollbar-thumb-border-radius); box-shadow: var(--horizon-scrollbar-thumb-box-shadow); - background-color: --horizon-scrollbar-thumb-bg; + background-color: var(--horizon-scrollbar-thumb-bg); + display: var(--horizon-scrollbar-thumb-display); &:hover { - --horizon-scrollbar-thumb-bg: $gray-500; + --horizon-scrollbar-thumb-bg: #{$gray-500}; } &:active { - background-color: $gray-700; + --horizon-scrollbar-thumb-bg: #{$gray-700}; } } - &.hidden-scrollbar { - overflow-y: scroll; - &::-webkit-scrollbar-thumb { - display: none; - } - &::-webkit-scrollbar-track { - box-shadow: none; - } - &::-webkit-scrollbar { - width: 8px; - } + .badge-warning { + color: black; } } -/* -.hidden-scrollbar:hover { - //This has no purpose, but for some godawful reason the SCSS compilation breaks if we don't put an additional rule here. - //If we don't, for some reason this winds up only displaying the thumb once you hover over it. - //So I just picked a rule that has 0 impact on anything else. +.platform-darwin { + .hidden-scrollbar { + overflow-y: scroll; + } +} + +div#page:not(.platform-darwin) .hidden-scrollbar { + --horizon-scrollbar-hidden-width: 8px; overflow-y: scroll; &::-webkit-scrollbar-thumb { - display: block; + --horizon-scrollbar-thumb-display: none; + } + + &::-webkit-scrollbar-track { + --horizon-scrollbar-track-box-shadow: none; + } + &::-webkit-scrollbar { + --horizon-scrollbar-thickness: var(--horizon-scrollbar-hidden-width); + } + + &:hover { + //This has no purpose, but for some godawful reason the SCSS compilation breaks if we don't put an additional rule here. + //If we don't, for some reason this winds up only displaying the thumb once you hover over it. + //So I just picked a rule that has 0 impact on anything else. + overflow-y: scroll; + &::-webkit-scrollbar-thumb { + --horizon-scrollbar-thumb-display: block; + } } } -*/ From 4509298a1a7d4fcabdddf888f019174f70d7198d Mon Sep 17 00:00:00 2001 From: FatCat Date: Sun, 1 Nov 2026 11:01:56 +0100 Subject: [PATCH 153/184] fix: Open settings menu from both buttons --- chat/ChatView.vue | 7 ++++++- chat/SettingsView.vue | 11 ++++++++++- chat/preview/event-bus.ts | 5 +++++ electron/Index.vue | 14 ++++++++++---- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/chat/ChatView.vue b/chat/ChatView.vue index 7115821f..d4186dc7 100644 --- a/chat/ChatView.vue +++ b/chat/ChatView.vue @@ -77,7 +77,7 @@ role="button" class="userInfo-button-item btn btn-outline-secondary" :title="l('settings.character')" - disabled + @click.prevent="openCharacterSettings()" > @@ -469,6 +469,7 @@ import CustomDialog from '../components/custom_dialog'; import Modal from '../components/Modal.vue'; import QuickJump from './QuickJump.vue'; + import { EventBus } from './preview/event-bus'; import { group, log } from 'node:console'; const unreadClasses = { @@ -731,6 +732,10 @@ window.removeEventListener('mouseup', this.mouseButtonListener); }, methods: { + openCharacterSettings(): void { + EventBus.$emit('open-settings-menu', { mode: 'character' }); + }, + onMouseButton(e: MouseEvent): void { // Mouse button 3 = back, 4 = forward const navigated = diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index 496d6640..6996d2a4 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -1,5 +1,6 @@ diff --git a/components/settings/SettingsRadio.vue b/components/settings/SettingsRadio.vue new file mode 100644 index 00000000..4c12fd94 --- /dev/null +++ b/components/settings/SettingsRadio.vue @@ -0,0 +1,56 @@ + + + diff --git a/components/SettingsSelectOverride.vue b/components/settings/SettingsSelectOverride.vue similarity index 97% rename from components/SettingsSelectOverride.vue rename to components/settings/SettingsSelectOverride.vue index c312ad27..dbe14797 100644 --- a/components/SettingsSelectOverride.vue +++ b/components/settings/SettingsSelectOverride.vue @@ -22,7 +22,7 @@ diff --git a/scss/_flist_overrides.scss b/scss/_flist_overrides.scss index 91cbac82..29c05027 100644 --- a/scss/_flist_overrides.scss +++ b/scss/_flist_overrides.scss @@ -201,6 +201,12 @@ input:checked { --bs-nav-tabs-link-active-color: var(--bs-body-color); } +.form-check.form-switch .form-check-input:indeterminate { + background-position: center; + background-color: $secondary; + border-color: $secondary; +} + .nav-tabs li { a { border-top-left-radius: 0.4rem; From 23bea20912f859cdfc8806676638266a48788323 Mon Sep 17 00:00:00 2001 From: FatCat Date: Fri, 6 Nov 2026 10:40:22 +0100 Subject: [PATCH 171/184] fix: disallowedTags placeholder formatting --- chat/locales/en-US.json | 2 +- chat/locales/es.json | 2 +- chat/locales/fr.json | 2 +- chat/locales/hu.json | 2 +- chat/locales/it.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chat/locales/en-US.json b/chat/locales/en-US.json index da5f86fa..c97c9180 100644 --- a/chat/locales/en-US.json +++ b/chat/locales/en-US.json @@ -754,7 +754,7 @@ "settings.dataManager.section.vanilla": "Import from Vanilla", "settings.defaultHighlights": "Use global highlight words", "settings.disallowedTags": "Disallowed BBCode tags", - "settings.disallowedTags.placeholder": "Type tags without brackets, ie {tagname}.", + "settings.disallowedTags.placeholder": "Type tags without brackets, ie. {tagName}.", "settings.displayLanguage": "Display Language", "settings.displayLanguage.note": "Languages might not be fully translated yet.\nWe have a tool for tracking progress, so you do not need to report missing translations.", "settings.enterAge": "Enter age", diff --git a/chat/locales/es.json b/chat/locales/es.json index d0af67d9..1cebe857 100644 --- a/chat/locales/es.json +++ b/chat/locales/es.json @@ -747,7 +747,7 @@ "settings.dataManager.section.vanilla": "Importar desde Vanilla", "settings.defaultHighlights": "Usar palabras resaltadas globales", "settings.disallowedTags": "Etiquetas BBCode no permitidas", - "settings.disallowedTags.placeholder": "Escribe las etiquetas sin corchetes, ej. {tagname}.", + "settings.disallowedTags.placeholder": "Escribe las etiquetas sin corchetes, ej. {tagName}.", "settings.displayLanguage": "Idioma de la interfaz", "settings.displayLanguage.note": "Es posible que algunos idiomas aún no estén completamente traducidos.\nContamos con una herramienta para hacer seguimiento del progreso, por lo que no es necesario que informes sobre traducciones faltantes.", "settings.enterAge": "Introducir edad", diff --git a/chat/locales/fr.json b/chat/locales/fr.json index 007f95fc..2e2f7843 100644 --- a/chat/locales/fr.json +++ b/chat/locales/fr.json @@ -747,7 +747,7 @@ "settings.dataManager.section.vanilla": "Importer depuis Vanilla", "settings.defaultHighlights": "Utiliser les pings globaux", "settings.disallowedTags": "Tags BBCode interdits", - "settings.disallowedTags.placeholder": "Saisissez les balises sans crochets, c'est-à-dire {tagname}.", + "settings.disallowedTags.placeholder": "Saisissez les balises sans crochets, c'est-à-dire {tagName}.", "settings.displayLanguage": "Langage affiché", "settings.displayLanguage.note": "Les langues peuvent ne pas être entièrement traduites.\nNous avons un outil pour suivre l'avancement, merci de ne pas signaler les traductions manquantes.", "settings.enterAge": "Indiquez l'age", diff --git a/chat/locales/hu.json b/chat/locales/hu.json index dc4043f8..06979f89 100644 --- a/chat/locales/hu.json +++ b/chat/locales/hu.json @@ -742,7 +742,7 @@ "settings.dataManager.section.vanilla": "Importálás alapértelmezettből", "settings.defaultHighlights": "Globális kiemelt szavak használata", "settings.disallowedTags": "Nem megengedett BBCode címkék", - "settings.disallowedTags.placeholder": "A címkéket zárójel nélkül gépeld be, pl. {tagname}.", + "settings.disallowedTags.placeholder": "A címkéket zárójel nélkül gépeld be, pl. {tagName}.", "settings.displayLanguage": "Megjelenítési nyelv", "settings.displayLanguage.note": "Nem minden nyelv fordítása teljes.\nSaját eszközzel követjük a fordítási folyamatot, úgyhogy nem szükséges hiányzó fordítások jelentése.", "settings.enterAge": "Add meg a kort", diff --git a/chat/locales/it.json b/chat/locales/it.json index 10521f69..134f3517 100644 --- a/chat/locales/it.json +++ b/chat/locales/it.json @@ -747,7 +747,7 @@ "settings.dataManager.section.vanilla": "Importa da vanilla", "settings.defaultHighlights": "Usa parole di notifica globali", "settings.disallowedTags": "Tipi di BBCode disattivati", - "settings.disallowedTags.placeholder": "Scrivi i tag senza le parentesi, come {tagname}.", + "settings.disallowedTags.placeholder": "Scrivi i tag senza le parentesi, come {tagName}.", "settings.displayLanguage": "Lingua di display", "settings.displayLanguage.note": "Le lingue potrebbero non essere ancora interamente tradotte.\nAbbiamo uno strumento per controllare i progressi, quindi non c'è bisogno di segnalare traduzioni mancanti.", "settings.enterAge": "Immetti età", From c8060ee16d2b36b105ddcb3660c3945dfdf628a4 Mon Sep 17 00:00:00 2001 From: FatCat Date: Fri, 6 Nov 2026 15:01:27 +0100 Subject: [PATCH 172/184] Settings menu visual touchups: - Consistent padding/ margins for individual settings - Restyled array tags for better legibility - Array tags now show global values as dummy tags when looking at per-character settings --- chat/SettingsView.vue | 30 ++++++++++----------- components/settings/SettingsArray.vue | 30 +++++++++++++-------- scss/_flist_overrides.scss | 39 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue index a74783cf..97df9218 100644 --- a/chat/SettingsView.vue +++ b/chat/SettingsView.vue @@ -190,7 +190,7 @@ {{ l('settings.spellcheck.language') }} -
+