c1d6ca3bbdc3fb076fc10cb4b574a9b6189ec33f
31
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
16730d5ed9 |
chore(release): 2.0.2 -- BetterTTV out, placeholders fixed
Forge Announce / Post changelog to Hellion Forge (push) Successful in 9s
Security Scan (reusable) / Security Scan (push) Successful in 27s
Security / scan (push) Successful in 27s
Build / Build (Release) (push) Successful in 34s
Release / Build and attach release ZIP (push) Successful in 31s
BetterTTV emote support is removed rather than switched off. Its shared-emote
endpoint went behind authentication and that is where nearly all of them came
from; what was left is the 65-entry global set, eleven of those on the plugin's
own known-broken list, so 54 largely static images from Twitch's early days.
Exactly one is animated, and that one is 492 frames at 140x140 -- 37 MB of
texture memory for a single emote, uploaded one frame at a time. Not worth a
network call and an on-disk cache on every start.
Gone with it: the download path, the cache directory, the GIF renderer, the
settings section, the block list, and 13 translation keys across 50 resource
files. 1567 lines. The plugin now makes no outbound network calls at all, which
is the claim PRIVACY.md has always wanted to make without an asterisk.
EmotePayload and its MessagePack type byte stay, deliberately. Around a thousand
rows in a two-month-old database carry them, and dropping the type would make
those fail to deserialise -- the history is the one thing 2.0.0 promised not to
touch. Nothing writes one any more and a stored emote renders as the code that
was typed, which is what the sender saw when they typed it.
Five descriptions in the Window tab printed {0} where the plugin name belonged,
handed to the widget directly instead of through string.Format the way the rows
around them do. Every language was affected including English; German surfaced
it because the placeholder lands at the start of the sentence there. A test now
walks every placeholder-carrying resource string, finds its uses, and fails on
an unformatted one -- verified by putting the defect back and watching it go red.
New preview images. The old set was from 2026-05-08, older than every cycle in
2.0.0, and showed stacked ImGui defaults to anyone browsing the installer. Taken
with screenshot mode on so no character names reach a public repo. The wizard
takes the theme picker's slot.
|
||
|
|
97d31bb8e4 |
fix(defaults): the values everyone had set by hand become the defaults
The 2.0.0 reset put every install on the shipped defaults for the first time, and that exposed which of them had never actually been used. The sidebar was the loudest: 44 pixels, a width carried over from the v1.2.0 icon-only layout and left in place long after the sidebar started drawing labels beside those icons. Every tab name came out clipped. Nobody had noticed in cycles because everyone had widened it by hand -- 160 in the config this was measured against. Default 160, and the floor moves from 40 to 130: where a tab name stops being readable in German, which is the longest of the 25 languages, rather than where the icons stop fitting. Anyone who wants it slimmer wants the collapsed layout, and that is a separate width. Glyph ranges had the same shape of problem with a worse outcome. They were only ever filled when someone picked a language explicitly, so an install left on "follow Dalamud" -- the default -- got none at all. That went unnoticed while configs accumulated ranges over months; a fresh config has none, and a tester on a Korean, Chinese, Cyrillic or Greek client would have come out of this update reading boxes. The load path derives them from the Dalamud UI language too now. The rest are preference defaults taken from a config that has been in daily use across every one of these cycles: compact density off, title bar off, compact timestamps on, compact tell tabs on, honorific glow on, 100 messages of tell history preloaded, inactive opacity 0.75, command help on the right. New tell tabs open as pop-outs, because the wizard's closing step tells the user to try /tell and watch exactly that happen. Two values were deliberately not carried over. SeenPopOutInputHint and SeenPopOutHeaderHint are not preferences, they are "this user has seen it" markers -- shipping them as true would mean no new user ever sees the hints that explain a feature people did not find on their own. The greeted toggle stays off as well: it is opt-in for people who greet. Also dropped the light-bulb emoji from the wizard's closing hint in all 25 languages. UI icons come from the icon font here, not from emoji. |
||
|
|
89c73be71c |
feat(release): 2.0.0 -- nine cycles ship, and the config starts over
v1.6.0 through v1.15.0 were never published. The whole window layer was being rewritten from ImGui defaults to custom drawing, and repo.json deliberately kept its links on v1.5.6 so nobody could update into a half-finished state. That work lands here in one release. The config is not migrated, it is replaced. A config carried across nine cycles of window rebuilds holds values chosen against surfaces that no longer exist, and starting over is the only way to be sure every install is on the same defaults. The message database is a separate file and is not touched. The old config is copied to HellionChat.json.pre-2.0.0.bak first, so rolling back to 1.5.6 stays a file copy rather than an evening of clicking settings back in. Config schema 27. The reset runs from the constructor, before LoadAsync seeds the default tabs, and the self-test now fails if the tab list is empty at /xlperf -- that ordering breaking would leave every user with no tabs and nothing else in the plugin would notice. Default layout gains an Emote tab: custom emotes, standard emotes and echo. A tester asked for it, emotes get lost between system notices otherwise. Manifest, README, roadmap and the four changelog consumers are on 2.0.0. The release notes lead with what was fixed rather than what was redrawn -- retroactive cleanup that could never be applied, a compaction that reported deleting nothing while deleting everything, pinned tell tabs that came up empty for a session. And with the one behaviour change users should know about: the channel grid is authoritative over storage now, so anyone who had unticked channels while the unknown-channel failsafe was on stores less than they did before. |
||
|
|
52237fda7e |
feat(wizard): the profile cards and the welcome page speak the window's language
The cards were the last surface still drawn from ImGui defaults: four emoji on stock child frames, sized to a fixed 2x2 grid that clipped the longer privacy descriptions in half the supported languages. They are tiles in the row language now -- a resting surface, an accent bar down the left edge when chosen, a held hover over both, tracked caps for the heading. The emoji are FontAwesome glyphs from the icon font the rest of the plugin already uses. Each card measures its own height from its text and the pair in a row takes the taller of the two, inside a scrolling frame, because cutting a privacy choice off mid-sentence is not a thing this plugin gets to do. Step three moves onto ToggleSwitch rows and a drawn theme field with a popup built from PopupRow, using the transient widget overloads throughout: the Func/Action pair saves on every click, and this step is staged until Finish. The welcome page is opaque, unlike every other window here. Those are read at a glance over the game; this one is read once and carries a privacy decision. The fox sat on a hardcoded off-white rectangle that read as paper taped to the window -- it is a disc now, tinted from the theme accent and only lightened as far as the black linework needs, measured rather than set. Three defects surfaced while doing it, all older than this work: - The GDPR notice for full history had been translated into 25 languages and drawn nowhere since the four-step rewrite dropped it on 2026-05-18. It is on the card where the choice is made. - Two cards claimed to be recommended: the badge sat on casual while the data minimisation heading still said "(recommended)" in every language. The suffix is gone, and the word it carried became the badge label. - The wizard had no way back into it at all. /hellion wizard reopens it, and OnOpen resets the staged state so a second run cannot commit picks from a first one the user never saw. The welcome text drops the fork framing: Chat 2 and this plugin have diverged far enough that the codebases no longer line up, so it reads as history in a muted line rather than as a justification up front. In its place is the notice that plugins are a grey area in this game and do not belong in public channels. Channel names in it come from Language.<lang>.resx per language, so the German build says Sagen/Rufen/Schreien and the Polish one says Say/Yell/Shout, which is what a Polish player actually sees on an English client. |
||
|
|
6bdfecb1df |
feat(chat): the input row learns the window's own language
Five filled plates become two ghost buttons and a menu. The plates were ImGui defaults sitting between a drawn pill and a drawn status bar -- three shape languages in the one row a user works in, which is what the tester's screenshot made obvious. The ghosts follow the sidebar's icon buttons: nothing at rest, a held hover fill, the glyph lifting toward the accent. Symbols stay outside the menu because they are used mid-sentence. Theme, settings, screenshot and hide move in; the screenshot toggle was allowed in only because its state moved to the status bar first, as an accent pill that shows while the mode is on. A privacy state behind a closed menu answers nothing. The input field paints its own rounded surface and hands ImGui a transparent frame, so the widget draws only text and caret -- the Boutique.Inputs technique from Character Select+, no rebuild of the widget itself. Focus is a two-pixel accent rail on the left edge: the same mark the rows, the popups and the message list already use for "this is where you are". The channel pill gets the CS+ treatment, a faint white gradient and a one-pixel light along the top edge. White over the fill rather than a second hue, so it reads as depth in every palette. And the popups that open from the row are made of PopupRow now -- the sidebar row's shape, which is also exactly what CS+ draws for its own popup entries. The channel picker was the reported case: a drawn pill opening a list of naked selectables, the style breaking mid-click. A hundred-odd pixels of button reserve go back to the input field. |
||
|
|
100290ea4d |
feat(chat): put the channel header above both windows
Drawn before the scroll child in each, so it stays put while the log moves. The child's height is deliberately left alone: it is given as a negative value, and ImGui resolves those against the space still available from the current cursor -- which the header has already reduced. Subtracting it a second time would have opened a gap of exactly the header's height above the input row, in both windows. Where the name shows follows one rule: never twice on the same screen. The sidebar layout is the only place nothing else carries it, so that is the only place the header says it. The top-tab strip carries it, and a pop-out with its title bar on carries it in the title. The pop-out's plain title row is gone, replaced by the header. The comment it left behind is worth keeping in mind -- an earlier header row was removed exactly because it repeated the tab name one line below the title bar. That warning is now the rule rather than a reason to have no header at all. The trailing detail is the home world and the clock, and the clock follows the same Use24HourClock setting the message timestamps do. Two clock formats in one window, with the header sitting directly above a column of timestamps, would be a defect rather than a preference. Culture is pinned for the same reason it is pinned in the message list. |
||
|
|
e0c9efca05 |
i18n: the five spots the smoke test found, and a BOM in the export
Reported from a real pass through the window. The theme categories were a static readonly array, so the five names froze at whatever language the plugin started in and a runtime switch relabelled the entire window except them. Same shape as the layout labels earlier in this cycle; this one got missed because replacing the literals with resource lookups looks finished until you actually switch. The status bar built its counts from English literals -- tab, tabs, msg, tell, tells -- and the privacy pill said "Privacy-First" in all 25 files. The thousands separator follows the user's culture now too, so German reads 1,2k rather than 1.2k. The live preview claims to show what the window will look like. It was showing English channel names next to a translated placeholder, which is worse than either. Channel labels come from ChatType.Name() now and the status slots share the strings with the real status bar. The four mock chat lines stay English on the earlier decision. And the export wrote a byte order mark. Encoding.UTF8 emits one, and a leading U+FEFF makes the JSON invalid for every strict parser -- confirmed against a real export from the game, where python's json.load refused the file. CSV keeps its BOM, because without one Excel guesses the codepage and mangles every non-ASCII name. The self-test that was supposed to catch that read the file with File.ReadAllText, which strips a BOM while detecting the encoding. It reads bytes now. The status bar tests asserted English literals and started failing on a German machine -- they pin a fixed culture now instead of inheriting the locale of whoever runs them. |
||
|
|
fdb1a98519 |
fix(privacy): the cleanup could never be applied, and three more from the audit
The cleanup preview marked itself stale before it could be drawn. The gate bumps a revision on release so a preview cannot survive a wipe; I then made the preview take the gate, so its own release invalidated it every single time and the apply button never appeared. The feature has been shipping non-functional since it was written, with a self-test that asserted the exact bump that killed it. Read-only operations no longer move the revision, and preview and maintenance have their own marks instead of borrowing Cleanup -- which also stops the five-second metadata refresh from expiring previews, and stops the UI announcing "another operation is running: cleanup" during a VACUUM. The JSON export produced invalid JSON. The chat relation kinds were interpolated straight into the output, and interpolating an enum writes its member name, so every message with a recognised relation came out as "source_kind":LocalPlayer. That is the file a GDPR access request goes out on. The self-test wrote a JSON file and never parsed it; it does now. Retention with the limit at zero still deleted. The slider is labelled "0 = never" and the sweep seeded 31 spec defaults unconditionally before reading the user's overrides, so zero still lost free company, linkshell and party history after ninety days -- and the short-circuit written for exactly this case could never be reached, because the map was never empty. A wipe that worked reported that it had failed. VACUUM needs the database to itself, the refilter walks a lazy reader on the primary connection outside the lock, and the two collide -- after the DELETE has committed. The delete paths no longer let that escape: the rows are gone, an uncompacted file is a housekeeping problem, and telling somebody their history is still there when it is not is a different kind of problem. Also: - CSV cells starting with =, +, - or @ get a leading apostrophe. The content is text other people typed into a chat channel and the file exists to be opened in a spreadsheet. - An export that matched nothing no longer replaces the previous one. It used to write its header, move it into place, and then report that nothing matched. Dalamud's save dialog offers no overwrite confirmation to fall back on, so this is the part that had to move. - The retention sweep says so when it loses the race for the gate, and routes its notifications through the teardown check like everything else. |
||
|
|
9ea9e96145 |
fix: close what the style review found, starting with a gate the metadata skipped
The real defect first. RefreshDatabaseMetadata was the one worker of six that never took the shared lock, and its flag was the one of six missing from the tab's busy state. It calls MessageCount, which holds the read lock, so a wipe could start while it was in there -- and the tab would not have known to grey the button, because it could not see the worker. Both halves fixed. The pattern is why: seven near-copies of one worker skeleton, and each copy decided something slightly different. The clear button failed silently when its thread could not start. The most destructive control in the plugin, pressed, and nothing happens, with no way to tell that from a wipe that worked -- while the three harmless workers beside it do report. Maintenance was the mirror: its comment promises refusals are said out loud, and then swallowed the actual failure. Three start-failure paths also bypassed the notify helper that carries the teardown check, three weeks after it was added for exactly that. The database numbers now wait for a real read, like the clear hint already did. Zero bytes and zero messages read as an empty database, not as a number nobody has fetched. SelectionAfterDelete is gone, with its three tests. The accordion has no selection, so its return value went into a discard -- a function answering a question the interface does not ask, with green tests guarding nothing. The project's own self-test README calls that the anti-pattern of record. Six new keys replaced by the translated orphans that already said the same thing. A commit earlier in this cycle is literally called "stop duplicating a key" and these went past it. The duplicate button also had the label "Add", which is the one string out of ninety-four that was never written. Tests: CleanupDeleteTypes had none, and with the failsafe on -- how a fresh config ships -- it is the path every cleanup takes. Four now, including the one that matters: an empty list deletes nothing rather than everything. And a self-test for the gate wiring, which is what would have caught the metadata worker. The unit tests prove the gate works; nothing proved the workers use it. |
||
|
|
e7b76fb21b |
chore(i18n): remove the keys whose features were removed on purpose
Fifty-four keys across 25 files, and a script that can find the rest. The script reports rather than deletes, because a key with no caller is a question and not a verdict: this cycle has twice found one that only described a feature whose button was torn out, and deleting it would have made the restoration cost 25 files of re-translation. The answer lives in the git history of the deletion, not in a grep. So only the ones where that history says the feature went on purpose: the web interface, the settings card overview the sidebar replaced, the save-and-discard model this window does not have, the LiteDB migration dialog, and three one-time announcements for versions long past. Plus nine this cycle superseded itself, including the three notes telling the reader to press Save first. 251 keys still have no caller. That is not a to-do list -- it is the inventory the search pass turned up: timestamp layouts, collapse duplicate messages, the About tab's prose, the honorific glow, the novice network button. Every one of them describes something that used to work. They stay until each has been decided one way or the other, which is the whole premise of this cycle. |
||
|
|
3a1b863def |
i18n: adopt the client's word for tell, and translate the appearance tab
Two decisions, both yours. Where Square Enix ships a client in a language, that client's word wins. German says Flüstern, French message privé, Japanese テル, Korean 귓속말, Simplified Chinese 密语. Everywhere else there is no official client and the loanword is what players actually say, so it stays. That meant rewriting the whole corpus in those languages rather than just the new keys, which is the reason the split existed in the first place: forty German values and thirty-seven French ones carried the old word. /tell is untouched, because that is a command and not a noun. The mechanical pass left French with three agreement errors -- "des message privé", "ce onglet", "messages de message privé" -- which is what happens when you substring-replace a language with gender and number. Fixed by hand. Traditional Chinese keeps 悄悄話, which the file already used nineteen times; it has no official client of its own. The appearance tab is translated as well. Thirty keys: the colour editor's groups and buttons, the theme categories, fork and import and export, the font labels and the preview's input placeholder. Theme token names stay English -- WindowBg and TextPrimary are JSON keys, not prose -- and so do the brand strings. 465 keys, 25 files, no gaps, no orphans, no placeholder drift. Still English and not in this commit: the database viewer behind /hellionView, which is a documented user command rather than a developer tool, and the symbol picker. Both are their own block. |
||
|
|
5991f49c59 |
i18n: hold the new strings against the glossary the plugin already had
A language review over all 24 files found that the translation batch had been written as if the file were empty. It is not: eleven years of FFXIV vocabulary and several hundred existing values already decided how this plugin says things, and the new strings disagreed with them. Three said something wrong rather than something unusual: - Norwegian called opacity "tetthet", which is density -- and the same file uses that exact word for compact density, so one word stood for two different settings. - Danish called the brand section "Brand", which in Danish is fire. - Turkish called a manifest a notification, in a window that already has a notifications section. Then the glossary itself. "tab" is zakładka in Polish, tab in Romanian and Danish; "plugin" stays plugin in five languages that had translated it; "pop-out" is a loanword in six. The Shift key keeps its name in Swedish, Finnish and Italian, because that is what is printed on it. Fixing pop-out also shortened the three longest row labels in the batch past the point where SettingRow would have clipped them. Czech was the clearest outright error: the existing file addresses the reader informally throughout, and five new strings switched to the formal form. And a gap in my own previous fix: I had moved four of the five tell strings to the established loanword and missed the fifth pair, so the failed-tell warning still said "Flüstern" while its neighbours said "Tell". 435 keys, 25 files, no gaps, no orphans, no placeholder drift, and every one of the five tell keys now carries the same word in every language. Whether German and French should say Flüstern and message privé rather than tell is a real question -- both game clients do -- but that is a glossary decision for the whole file, not something to introduce through four new keys. |
||
|
|
b3c0ec73ca |
i18n(settings): keep the game's own vocabulary, and stop duplicating a key
A review pass found the translation commit had introduced a split rather than closed one. Every existing string in this plugin leaves "tell" untranslated -- it is what the game calls the thing, in twenty values across the German file alone. My new keys translated it, so the channels tab showed "Auto-Tell- Tabs" and, two rows below, "Flüstern automatisch öffnen". Thirteen of fourteen sampled languages had the same break. The four affected keys now use the established word, which also fixes a German line that had tells "aufgehen" like a door. Turkish said the opposite of what the control does: saydamlık is transparency, the slider is opacity, and 1.0 means fully opaque. Every other language uses the opacity word. Corrected, along with the inactive one beside it. Compact density had grown a second key for a field that already had one. Appearance_UseCompactDensity_Name and _Description were sitting there with no caller -- which is precisely what this cycle exists to fix -- so the chat tab uses those and my duplicate is gone again. The German wizard label for the same field said "Kompakter Density-Modus"; it says the same thing as the other two now. Also: Hungarian called a pop-out window "kiemelt" (highlighted), the project's word is "kiugró"; German called it "ausgeklappt", which is what a menu does; Italian and Polish had a clumsy inactive-opacity label. 435 keys, all 25 files, no gaps, no orphans in either direction. One claim in the previous commit was wrong and is worth stating plainly: it said only log lines, thread names, developer tools and brand strings remained English. The appearance tab is still English throughout -- the theme picker, the colour editor, the font section, the live preview -- and so are parts of the database viewer, which is a documented user command, not a developer tool. That is a separate block, not a footnote. |
||
|
|
83306f1f47 |
i18n(settings): translate the settings window
The settings window was rebuilt in v1.10.0 and v1.11.0 with its labels written straight into the C#. Every section heading and most row labels across five tabs read English in all 25 languages, which the memory notes had accepted as a known backlog. Forty-nine keys close it: seventeen section headings, the rows under them, and the two layout choices. All 25 languages, 437 keys each, no gaps and no placeholder drift in either direction. The layout labels are a property rather than a static array now. A static one would hold whichever language the plugin started in, and this plugin switches language at runtime. What stays English, deliberately: log lines and thread names, which no user reads; the two developer tools behind Shift plus Ctrl+Shift, per the decision that developer surfaces stay English; and the About tab's brand lines, attributions and licence identifiers, which are names. |
||
|
|
8e38e3e805 |
refactor(config): delete what was never read, restore what was only orphaned
Block D of v1.12.0. The line between the two is the whole job, and I got it wrong once on the way: six cache fields on Tab looked dead because nothing writes them, and nothing writes them because AutoTellTabTint and TabTintCache went out with the chat window in |
||
|
|
3cb20b6f65 |
feat(chat): insert map-flag and item-link tokens again, and finish the resx parity
The two menu items went out with the v1.5.6 chat window in
|
||
|
|
75c4acd19a |
feat(settings): make eleven settings reachable again
Every field here has a reader in the running plugin and had no control in the window. The config file was the only way to any of them. HideChat is the one that mattered. It defaults to on, it suppresses the game's own chat window, and the only other path to it is a right-click item that sets it to true and can never set it back. Anyone who used that item once had to edit JSON to get their chat back. The new toggle reads and writes the same field, so the two agree instead of fighting. The rest, by tab: - Window: the three remaining hide conditions, the preview minimum that belongs to a toggle already on screen, native item tooltips and their offset. - Chat: an emotes section holding the BetterTTV switch, the cache state, and an editor for the blocked-code list that has been in the config since v1.0 with nowhere to edit it. Blocking one code is a finer instrument than switching emotes off wholesale. Plus auto-translate sorting. - General: the failed-tell warning. The game reports a failed tell in the log only, where somebody who is typing does not see it. - Data & Privacy: battle messages. It decides whether battle lines are persisted at all, which makes it a storage decision rather than a chat one, and it sits in front of the whitelist rather than inside it. MaxParallelPopouts stays out. It is read once in the pool's constructor, the windows are registered once, and registering at runtime is explicitly forbidden -- a slider would do nothing until a reload, which is exactly the kind of decoration this cycle exists to remove. Four new strings in all 25 languages; the other fifteen labels were already translated and waiting. |
||
|
|
4f3bdc3c7b |
fix(privacy): translate the channel list and give it back its presets
Reported after a translation pass: the channel list in Data & Privacy reads English in every language. It was not, as assumed, the client's own naming -- the list called ToString() on the enum member, so it showed "FreeCompanyLoginLogout" while Language.ChatType_* sat next to it holding "Freie Gesellschaft (An-/Abmeldung)" in all 25 languages. Fixing the label exposed how little else the list had. Six translated strings written for exactly this control had no caller: the explanation of what it does, the note that it governs the database and not the chat window, and three presets -- data minimisation, select all, deselect all. Eighty-nine checkboxes without a "recommended" button is not a choice anyone makes. So the list now carries the same eight groups the export uses, with the individual channels one fold away. It is the one control that decides what reaches the database; the two screens describing channels the same way is worth more here than anywhere else. The unknown-channel switch gets its description back, and the telemetry section stops being the last English literal in the tab. It stays a statement rather than a switch: a toggle would imply there is something to turn off. |
||
|
|
0279a1a9d6 |
fix(privacy): close the gaps three review passes found in block A
The worst of them made the block's own privacy promise backwards. PrivacyPersistChannels was given a non-empty field initializer so a fresh config would record conversations only. Dalamud deserialises with Json.NET's defaults, which means ObjectCreationHandling.Auto: a collection field that already holds items gets *populated*, not replaced. Verified against Newtonsoft 13.0.3 -- saved [] loads as the initializer, saved [Say] loads as initializer plus Say. So the change would have unioned the privacy-first list into every existing config on load and switched channels back on that the user had unticked, while also making the v24 migration unreachable and its self-test vacuous. The field is empty again and the seeding moved to CreateFresh, which only runs when there is no config file at all. Cleanup could delete a channel it had promised to keep. The allowlist could only name channels that were already in the database when the preview ran, so an unrecognised channel whose first message arrived afterwards fell outside it. Where the failsafe is on, the deletion now names what goes -- known channels that are not on the list -- instead of what stays. The window closes completely, and a listed channel that happens to be empty right now is safe for the same reason. The cleanup preview was the one long operation that never took the shared lock, while holding an open reader across a full-table scan. That is precisely the case the lock was written for. Clearing the history reported success when it failed. ClearMessages purges the full-text index between the delete and the VACUUM; if that step throws, the plaintext stays on disk and the user was told it was gone. It has its own error string now, in all 25 languages. Also: - One busy state for the whole tab. Cleanup, clear, maintenance and export reach the same store, and per-section flags left two destructive buttons live at once. The lock turned that into a refusal rather than damage, but a refusal you have to trigger to discover is not an answer. - The gate carries a revision, bumped by every mutating operation that finishes. A preview taken before a retention sweep no longer passes as current afterwards: comparing it against the settings alone cannot see that the rows it counted are gone. - Database metadata moved to a worker. Checking "is anything busy" first is not enough, because an operation can take the lock in the gap before COUNT(*) runs, and then the game stands still for a whole file rewrite. - The clear hint stays hidden until the count has actually been read. "0 messages are stored" in front of the clear button is a lie told at the worst possible moment. - Refusal notices read the operation once. Guard and name were two reads of the same field, so a run finishing in between printed a sentence that stopped at the colon. - The retention sweep cannot start twice. The gate only goes busy once the worker reaches TryBegin, and the due-check runs every tick. - Teardown waits up to five seconds for the store to come free rather than disposing the connection under a running VACUUM. - Maintenance has its own flag and says so when it is refused; reload gets the same guard as its neighbour; the breakdown tree keeps its open state across a language switch. |
||
|
|
e24ea79302 |
feat(privacy): reconnect database maintenance and the manual retention run
Two sections that had backends and no buttons. Database: path, size, WAL size, message count, and a clear button. The numbers refresh at most every five seconds and not at all while a long operation owns the store -- MessageCount takes the read lock, and asking for it during a VACUUM means waiting for the whole file to be rewritten, on the draw thread. The old version called ClearMessages straight from the draw thread, VACUUM included; it runs on a worker now. One line beyond the old layout sits above the clear button: how many messages are stored, and that exporting keeps a copy. Whoever is about to throw the history away should be told there is a way not to. The legacy Chat 2 files only get a block when they are actually on disk, and the advanced tools only appear when the section is expanded with Shift held. The message injector is not back: it was deleted with the tab and writing 10,000 fake messages into a user's real database is not something to rebuild on the way past. Retention: an "apply now" button, the running hint, and the last-run line, which v1.11.0 shipped as an English literal while both strings sat translated in all 25 languages. Plus reset-to-spec next to the existing clear-overrides, since the two answer different questions and both were already translated. Retention_Apply_Tooltip stays unused and gets a replacement. It ends with "Save your changes first", and the window it was written for had a Save button. Also here, found while wiring the manual trigger: DbOperationGate.End now takes the operation it releases. It used to reset blindly, on the reasoning that a worker must be able to release from a finally without knowing whether it acquired. That is backwards: a worker whose TryBegin was refused also runs its finally, and a blind reset there hands away the lock of whichever operation actually holds it. Worse than no gate, because the refused worker walks off believing it did nothing while a VACUUM starts under somebody's open reader. |
||
|
|
1ab7ba8377 |
feat(privacy): reconnect the retroactive cleanup
The privacy filter only decides what gets written from now on. Whatever was stored before the user narrowed their channels stays there until something removes it, and that something has had no button since May. Two rules shape the section, both because this deletes history and cannot be undone: - Without a preview the apply button does not exist. Not greyed out, absent. A disabled button is something a user waits for; a missing one is something they have to go and earn. - A preview that no longer matches the settings counts as no preview. The old version only recoloured the number and left the button live, so a changed whitelist could be applied against counts computed for the previous one. The mapping from the live rule to CleanupRetainOnly is the part worth reading twice. CleanupRetainOnly takes one set and deletes everything else, so it can only stand in for the live rule where that rule narrows something: filter off means nothing is filtered, and an empty list means a full wipe, which has its own button and its own confirmation. Both cases now say so instead of offering a destructive action that does not mean what it looks like. Inside that, the allowlist is the whitelist itself plus any stored channel this build does not recognise while the unknown-channel failsafe is on. Deriving it from the counts instead would delete messages that arrive on a whitelisted but currently empty channel between the preview and the apply, and dropping the unrecognised ones would defeat the failsafe, which exists to hold on to a new patch's channel until the user has decided about it. The preview runs on a worker over its own connection. It is a GROUP BY across every stored row, the old version ran it inline on the draw thread, and holding the read lock for it would stall UpsertMessage on the framework thread for the length of the scan. Cleanup_Help_SavedNote stays unused: it tells the reader to press Save first, and the window it was written for had a Save button. |
||
|
|
d59ee62223 |
feat(privacy): reconnect the message export
The exporter has worked since v1.4.8. The form that drives it went out with the old settings window in May, which left PRIVACY.md promising an access request the plugin had no way to answer. New section in the data and privacy tab: time range, sender substring, channel groups, format, and a save dialog. Form state lives in the tab, not the config -- a filter describes one action, and a stale "last 7 days, sender Mira" reappearing weeks later is a worse start than an empty form. StreamForExport now takes a caller-owned connection. The reader stays open for as long as the file is written, seconds to minutes on a large history, and chat keeps arriving throughout -- so the primary connection would be read here and written by UpsertMessage at once, and SqliteConnection is not thread-safe. Holding the read lock instead would trade that for freezing the game. ChannelGroups lifts the eight groups out of the deleted tab and finishes them: 37 of 89 channels belonged to no group and were therefore unreachable in the UI. Game Master channels follow ChatTypeExt.Parent(), so GmTell sits with the other tells rather than under system traffic -- an access request that quietly drops part of what it promises is the dangerous kind of gap. Also here: - OpenSecondaryConnection disposes on a failing pragma. Open can succeed and journal_mode=WAL still time out, and with Pooling=false the connection then survives until a finalizer reaches it. Affects the full-text rebuild worker too. - StreamForExport builds its logger before the reader, so a throwing CreateLogger cannot leave a reader nobody owns. - The export thread takes the gate itself instead of the caller taking it first. Acquiring before Start would strand the gate for the session if thread creation failed, and the gate also holds back the sweep. - Notifications are skipped once teardown has started. The thread has no cancellation path and finishing the file is right, but reporting it to a plugin that is gone is not. - Transient widget rows that return their value instead of saving it. Writing the config file on every keystroke of a sender filter would be both pointless and slow. - Five translated keys for "another database operation is running", in all 25 languages. Two of the four operation names have no trigger yet; they arrive with the cleanup and maintenance sections. |
||
|
|
d1e12ce866 |
fix(popouts): restore a way to close a pop-out
Last commit hid the header row when the title bar was on, to stop the tab name appearing twice. The close button was in that row, and the title bar carries none of its own -- ShowCloseButton is off because closing has to go through the pool to release the slot. So a pop-out with its title bar on could not be closed at all. Pop-in moves into the input row, alongside the settings and hide buttons that are already there. It is red, contrast-checked against the button plate it sits on, and carries a tooltip, since a bare X next to an emoji picker does not say where the tab is going. Wired by setter after construction rather than through the constructor: the window is what the button has to call, and it does not exist yet while its own input row is being built. Routing it through the graph would close a factory-callsite cycle MS.DI cannot detect. The header row is now just the tab name, drawn only when there is no title bar to carry it. InputBar_PopIn_Tooltip ships in all 25 languages, including the Designer entry. New strings get translated with the change from here on, not batched. |
||
|
|
2686a8f74b |
feat(i18n): localise the quick-button tooltips
The three quick-button tooltips were hard-coded English while the rest of the UI goes through HellionStrings. All 25 resource files now carry them, machine translated on Flo's go-ahead. Verified: every file still parses as XML, and the base file stays English so a missing language falls back to what was there before. |
||
|
|
32840623ff | i18n(settings): translate v1.5.6 first-wave control labels | ||
|
|
2acac78b4c | refactor(settings): retitle data and about cards to match merged scope | ||
|
|
ce4c5d9cf9 | i18n(settings): translate new section titles and prune orphan keys | ||
|
|
a600f014eb |
i18n: add quick-picker strings and reduce-motion settings toggle
Five new keys across the EN source plus 24 locale variants (DE plus 23 AI-assisted, each carrying the pending-review marker): the header quick-picker tooltip and two section headers, plus name and description for a new ReduceMotion checkbox. ReduceMotion was a config field with no UI -- the checkbox lands in the Theme & Layout tab's window-style section. Designer.cs hand-edited as a v1.5.4 block matching the v1.4.8 convention. |
||
|
|
35e8d3a7fe |
fix(font): bundled font now actually renders, ship Inter Light, +CJK fallback
Plugin.cs:937 only pushed RegularFont when Config.FontsEnabled was true.
FontsAndColours.cs:50 forces FontsEnabled=false whenever UseHellionFont is
enabled (to hide the chooser UI), so the bundled-font path was silently
dead and the FFXIV Axis game-font took over. Exo 2 looked "almost right"
because it overlaps Axis on basic Latin, so the regression went unnoticed
for the entire v1.5.x series.
The fix routes RegularFont through draw whenever either FontsEnabled or
UseHellionFont is on. First-frame HITCH dropped from ~74 ms to ~20 ms
median (5-reload Linux/Wine sample 17.9-23.6 ms) as a side effect — the
v1.5.1 "too optimistic" defer-pattern hypothesis was actually a symptom
of this bug, not bad math.
Font-stack overhaul on top:
- Inter Light (Static 18pt-Light, 343 KB, SIL OFL 1.1) replaces Exo 2 as
the bundled font. Inter ships full Latin Extended-A/B, Greek polytonic
and Cyrillic Supplement coverage.
- NotoSansCjkRegular added as a third merge layer for Hangul,
Simplified-Chinese-specific Han glyphs, and CJK fallbacks the FFXIV
Japanese font does not ship.
- Two new ExtraGlyphRanges flags (LatinExtended, Greek) implemented via
AddChar pair lists in SetUpRanges.
- Settings.Apply auto-activates the matching ExtraGlyphRanges flag on
language change. Plugin.LoadAsync runs a one-shot migration that ORs
in the required flag for an already-selected language.
- ExtraGlyphRanges CollapsingHeader reachable regardless of
UseHellionFont (was hidden in the early-return branch).
- New WarningText below the language combo: FFXIV's chat engine only
fully supports EN/DE/FR/JA. Other scripts render in the HellionChat
UI but may garble in in-game chat input/send.
Localisation wave (originally a FR-only cycle):
- 24 selectable UI languages. LanguageOverride enum gains 10 new locales
plus 3 previously commented-out (Italian, Korean, Norwegian with ISO
code `nb` instead of `no`). All new values append to keep existing
user-config integer serialisation stable.
- Resource bundle split: HellionStrings.resx (24 locales, 328 keys) for
fork-added strings, Language.resx (24 locales, 456 keys) for the
ChatTwo-Crowdin-heritage. 4 post-sync Crowdin keys backfilled into
13 legacy locales with per-key AI-assisted comment marker.
- Em-dash sweep on EN source plus 18 translations. Russian and Ukrainian
keep their typographic norm.
Old HellionFont.ttf + HellionFont-OFL.txt removed; Inter-Light.ttf +
Inter-OFL.txt take their place. Configuration field UseHellionFont keeps
its name for backwards-compat. Migration v17 stays.
|
||
|
|
38586db9d8 |
fix(l10n): em-dash sweep across EN source and translations, backfill Crowdin gap
- HellionStrings.resx: 10 in-prose em-dashes -> period/colon per style guide
- 18 HellionStrings.<lang>.resx: 114 mechanical em-dash edits via heuristic
(period before capital, colon otherwise). Skipped: fr (already clean),
zh-Hans/zh-Hant (already clean), ru/uk (em-dash is orthographic norm)
- HellionStrings.de.resx: fix substantive-heuristic miss in Wizard_Cancel_Label
- Language.de.resx: add Hellion Forge maintainer header (native-maintained)
- Backfill the 4 post-Crowdin keys (Options_ColorSelectedInputChannelButton_*,
Options_HideInNewGamePlusMenu_*) into 13 legacy Crowdin locales with
per-key AI-assisted comment marker. All 23 Language.*.resx now at 456 keys.
|
||
|
|
c357873604 |
feat(l10n): add HellionStrings bundle (EN + 22 variants) and Language siblings — WIP v1.5.3
Split fork-added keys into a dedicated HellionStrings resource bundle separate
from the Language.*.resx Chat-2 Crowdin heritage.
- Add HellionStrings.resx (EN source, 328 keys) and HellionStrings.Designer.cs
- Add 22 HellionStrings.<code>.resx variants: ca, cs, da, de, es, fi, fr, hu, it,
ja, ko, nb, nl, pl, pt-BR, pt-PT, ro, ru, sv, tr, uk, zh-Hans, zh-Hant
- Add matching Language.<code>.resx siblings for the new locales with the
Hellion Forge maintainer header
- FR pass: align labels with the rest of the UI
(Confidentialité, Visualiseur, Violet indigo)
|