Compare commits

..

75 Commits

Author SHA1 Message Date
JonKazama-Hellion 46b63ffdd1 merge: Hellion Chat 0.5.1 — Backlog Sweep 2026-05-02 21:34:50 +02:00
JonKazama-Hellion 4ba5004322 chore(release): bump version to 0.5.1
Hardening and polish patch. Eight backlog items from the v0.5.0
codebase review. No new features, no migration.
2026-05-02 21:27:10 +02:00
JonKazama-Hellion 3584c94523 docs(db): explain why pragma statements stay interpolated
Both PRAGMA call sites take values that SQLite does not accept as
bound parameters. ColumnExists takes a hardcoded table name, the
migration call takes a compile-time int from the version sequence.
Comments now state both facts so future readers don't try to wedge a
defensive whitelist into a path that cannot be reached from anywhere
user-controlled.
2026-05-02 21:25:40 +02:00
JonKazama-Hellion 303729f3d3 refactor(db): parameterise DeleteByRetentionPolicy SQL clauses
Per-channel WHERE tuples and the catch-all default-clause now bind
ChatType and cutoff via named parameters instead of being inlined as
literals. Combines BindIntList for the explicit-types exclusion with
explicit AddWithValue for each (type, cutoff) tuple. Behavioural diff
against v0.5.0: none — same retention windows, same cutoff math, just
parameterised.
2026-05-02 21:25:05 +02:00
JonKazama-Hellion 12085ff1e2 refactor(db): parameterise IN-clause SQL via BindIntList
Migrate CleanupRetainOnly, StreamForExport, CountDateRange, GetDateRange
and GetPagedDateRange from interpolated IN lists onto BindIntList.
Eliminates the string-interpolation pattern for SQL value lists in the
IN-clause sites. Behavioural diff against v0.5.0: none — same enum/byte
values, just bound under named parameters instead of inlined.
2026-05-02 21:24:36 +02:00
JonKazama-Hellion e4593a0fda refactor(db): add BindIntList helper for parameterised IN-clauses
Centralised builder for dynamic IN-clauses that binds each value as a
named parameter and returns the comma-joined placeholder string. Used
by the upcoming MessageStore migrations away from string-interpolated
SQL.
2026-05-02 21:23:17 +02:00
JonKazama-Hellion 3fc42963ae feat(autotell): dim selection background of greeted tabs
The text-disabled colour alone made greeted tabs visually weak in the
sidebar. Push dimmed Header and HeaderHovered values alongside the
existing Text push so the selected and hovered states match the
greeted state too. Idle state stays untouched because ImGui Selectable
has no idle background slot.
2026-05-02 21:22:36 +02:00
JonKazama-Hellion 7c52e890e6 feat(privacy): mark cleanup preview as stale when whitelist changes
The preview block caches the deletion estimate from the last refresh.
When the user toggles whitelist channels afterwards the cached number
no longer reflects the current selection. Snapshot the whitelist on
refresh and detect drift on every frame; on drift, grey out the counts
and surface a stale hint plus an emphasised refresh button. Sits
alongside the existing Cleanup_Help_SavedNote, which warns about a
different mismatch (mutable vs saved) and stays as-is.
2026-05-02 21:22:12 +02:00
JonKazama-Hellion 4d977d5118 fix(fonts): marshal font chooser results onto the framework thread
Three FontChooser ContinueWith handlers wrote Mutable.* directly from
the threadpool. Wrap the result-write in Plugin.Framework.Run so the
mutation lands on the same thread that owns the rest of the UI state.
Matches the marshalling pattern already used by Database.cs and
Privacy.cs background work.
2026-05-02 21:20:49 +02:00
JonKazama-Hellion ddd72a878e refactor(emotes): drop async void on LoadData
async void Task.Run() is a no-op for awaiting purposes since the void
returns immediately. Switch LoadData to async Task and have the two
callers fire-and-forget the task directly. Exceptions still go through
the existing try/catch inside LoadData.
2026-05-02 21:20:16 +02:00
JonKazama-Hellion 66450dd518 refactor(i18n): pull tabs and database tab names into hellionstrings
Both tab classes were the last two settings tabs still pulling their
display name from the upstream Language resource bundle. Move them
into HellionStrings so all eight settings tabs share one i18n source.
The unused Language.Options_*_Tab keys stay around for backwards
compat with cherry-picked upstream tabs.
2026-05-02 21:19:40 +02:00
JonKazama-Hellion 7de28ef9b2 refactor(settings): align performance section with helpmarker pattern
Drop the inline wall-of-text description in favour of the standard
HelpMarker tooltip used across the rest of the v0.5.0 settings UX.
Visual consistency for the General tab.
2026-05-02 21:18:30 +02:00
JonKazama-Hellion da3c1f6832 fix(emotes): mark required properties to silence CS8618
Mark Emote.Id, Top100.Id, Top100.Code and Top100.ImageType as required
so the JSON deserializer enforces the contract instead of relying on
default-null semantics. Removes the four CS8618 warnings the build has
been carrying since v0.4.0.
2026-05-02 21:17:57 +02:00
JonKazama-Hellion e66ae1f5b4 merge: Hellion Chat 0.5.0 — Settings UX Polish
Twelve organic settings tabs collapsed into eight themed ones (General,
Appearance, Window, Chat, Tabs, Privacy, Database, Information). Wipe
migration v9→v10 with HellionChat.json.pre-v10-backup safety net.
Default tab layout now spawns six themed tabs out of the box (General,
System, Free Company, Party, Beginner when Novice Network is on,
Linkshell, Tell Exclusive). HelpMarker pattern across every section,
disabled tooltips remain visible. Pre-release polish from full
codebase review covered race-conditions, EmoteCache retry, Allman
bracing, and dead i18n keys.
2026-05-02 18:42:50 +02:00
JonKazama-Hellion 281a1e172f feat(tabs): add dedicated System tab to default layout
Split the technical/notification streams (System, Error, Echo, Debug,
NPC announcements, login/logout, retainer sales, gathering system,
glamour notifications, sign messages, alarms, orchestrion, message
book, random number, progress) out of the General tab into their own
System tab. General now shows player conversation plus the active
gameplay events (loot rolls, crafting, gathering, NPC dialogue, party
finder pings) without burying chat under technical chatter.
2026-05-02 18:28:29 +02:00
JonKazama-Hellion 45a5035426 refactor(tabs): align General preset with maintainer's live config
Drop the channels that already live in dedicated themed tabs (Tells,
emotes, Novice Network, FC and PvP announcements, Sign and Glamour
notifications) so the General tab is the public-chat catch-all instead
of a duplicate of every themed tab. NpcDialogue moves in because the
maintainer reads it alongside system messages.
2026-05-02 18:25:59 +02:00
JonKazama-Hellion e1931fc7d2 feat(tabs): seed default tab layout on first run and v10 wipe
Spawn six themed tabs out of the box instead of one General catch-all:
General (everything), Free Company (FC chat plus FC announcements and
login/logout), Party (Party, CrossParty, Alliance, PvP team plus loot
rolls), Beginner (Novice Network only when ShowNoviceNetwork is on),
Linkshell (all eight regular and cross-world linkshells together) and
Tell Exclusive (TellIncoming/TellOutgoing as a safety-net catch-all in
case Auto-Tell-Tabs misses one).

Tab names live in HellionStrings (EN/DE). The Tabs settings tab gains a
help-text hint above the list recommending one tab per linkshell when
the user is in multiple, since a single combined Linkshell tab gets
noisy fast for active users.
2026-05-02 18:13:15 +02:00
JonKazama-Hellion 2201478a54 fix(v0.5.0): defaults and coupling issues from first walkthrough
- Configuration.cs: ShowTitleBar defaults to true so a fresh install
  shows the window header instead of leaving the user without a drag
  handle and hide button
- Configuration.cs: MaxLinesToRender default drops from 10000 to 5000
  to match the slider's intended ceiling and the previous user-tuned
  baseline
- ChatLogWindow.cs: 24h-clock checkbox now actually flips the format.
  The Bestand path passed null culture which on a German system
  locale always rendered 24h regardless of the toggle
- Appearance.cs + ChatLogWindow.cs + Popout.cs: when Hellion theme is
  enabled the global theme opacity drives the chat-window BgAlpha and
  the legacy WindowAlpha slider is disabled, so the two opacity
  controls no longer fight each other
- Appearance.cs: ticking UseHellionFont now flips FontsEnabled off so
  the two mutually-exclusive font stacks no longer appear active at
  the same time
2026-05-02 18:00:04 +02:00
JonKazama-Hellion 50963ccf1b fix(v0.5.0): pre-release polish from full-codebase review
- Plugin.cs: mark RetentionSweepRunning volatile so the ImGui thread
  reads the latest value without a stale register-cached copy
- EmoteCache.cs: reset State to Unloaded on exception so a later
  trigger can retry instead of being blocked by the early-out
- Settings.cs: switch the SaveAndClose / Discard buttons to Allman
  bracing for consistency with the rest of the file, and include the
  ItemSpacing in the Ko-fi-button right-edge calculation
- Privacy.cs: add a saved-policy hint above the manual retention
  Ctrl+Shift button so the existing Cleanup wording pattern is
  matched here too
- HellionStrings: drop seven unreferenced keys (Theme_Heading,
  Migration_Notification_*, Migration_Webinterface_Removed_*,
  AutoTellTabs_Migration_*) and their EN/DE values, add the new
  Retention_Help_SavedNote string
2026-05-02 17:50:32 +02:00
JonKazama-Hellion fde85e6d69 chore(release): bump version to 0.5.0
Settings UX polish release. Twelve organic settings tabs collapsed
into eight themed ones, theme/font controls moved to Appearance,
About and Changelog merged into Information. Configuration migrates
from v9 to v10 as a wipe with a backup file written next to the live
config; chat history and tabs survive.
2026-05-02 17:37:46 +02:00
JonKazama-Hellion c22b169b73 chore(settings): remove legacy settings tab files
The nine legacy tab implementations were superseded by the new eight
themed tabs. Removing them now that the consolidated structure is in
place keeps SettingsTabs/ aligned with what actually ships.
2026-05-02 17:36:31 +02:00
JonKazama-Hellion 6839ccaf34 feat(settings): build information tab from about and changelog
Three collapsible sections: version info (author, discord handle,
version, issue tracker), about HellionChat (maintainer, mission, build
lineage, license, SE notice, localisation, translator list), and the
changelog (auto-print toggle plus the manifest changelog renderer).
2026-05-02 17:36:07 +02:00
JonKazama-Hellion fa108c2271 refactor(settings): align tabs tab plugin reference to property style
Match the property-style reference used across the other settings tabs
so the field/property mix is gone.
2026-05-02 17:34:24 +02:00
JonKazama-Hellion 395a0d7c98 refactor(settings): polish database tab section structure
Drop the redundant inner Advanced TreeNode in the Maintenance section,
flatten the duplicated indent in the Overview section, and rename the
section headings so they reflect their content (Overview shows
metadata, Maintenance hosts the shift-gated tooling).
2026-05-02 17:33:51 +02:00
JonKazama-Hellion b76bfb3cfc refactor(settings-refactor): regroup Database tab into storage, viewer, stats tree nodes 2026-05-02 17:22:26 +02:00
JonKazama-Hellion 0512e4729c refactor(settings-refactor): remove theme settings from Privacy tab and tighten tree structure 2026-05-02 17:14:40 +02:00
JonKazama-Hellion 654f24c609 docs(settings-refactor): add Chat tab header explaining emote block migration 2026-05-02 17:09:50 +02:00
JonKazama-Hellion 0e2a14197c feat(settings-refactor): populate Chat tab with auto-tell-tabs, behaviour, preview, emotes 2026-05-02 17:04:25 +02:00
JonKazama-Hellion 52e163a472 fix(settings-refactor): show HelpMarker tooltip even when item is disabled 2026-05-02 17:00:25 +02:00
JonKazama-Hellion e086afe2a8 feat(settings-refactor): populate Window tab with hide, inactivity, frame, tooltips 2026-05-02 16:54:23 +02:00
JonKazama-Hellion c97ce7543b feat(settings-refactor): populate Appearance tab with theme, fonts, colours, timestamps 2026-05-02 16:44:50 +02:00
JonKazama-Hellion cca4571470 feat(settings-refactor): populate General tab with input, audio, performance, language sections 2026-05-02 16:36:52 +02:00
JonKazama-Hellion 444d7f8e2e style(settings-refactor): use property-style for Plugin reference in tab stubs 2026-05-02 16:31:42 +02:00
JonKazama-Hellion 71ae95d79c feat(settings-refactor): wire new 8-tab structure with stubs 2026-05-02 16:26:22 +02:00
JonKazama-Hellion 9a38f7f094 chore: rewrite AI disclosure in a more human, less legal-watertight tone 2026-05-02 16:22:48 +02:00
JonKazama-Hellion c33e519bb9 fix(settings-refactor): use pluginConfigs root for backup path 2026-05-02 16:03:51 +02:00
JonKazama-Hellion 14e585ef63 feat(settings-refactor): bump configuration version to 10 with wipe migration 2026-05-02 15:54:35 +02:00
JonKazama-Hellion d4aa3971c5 merge Auto-Tell-Tabs Merge branch'feature/auto-tell-tabs' 2026-05-02 14:48:37 +02:00
JonKazama-Hellion e9ec587e3b chore: bump to 0.4.0 with auto-tell-tabs changelog 2026-05-02 14:33:52 +02:00
JonKazama-Hellion 39cd7ab801 feat(auto-tell-tabs): make greeted toggle button opt-in (default off, greeter-specific) 2026-05-02 14:26:13 +02:00
JonKazama-Hellion bb6259e14d fix(auto-tell-tabs): make greeted toggle button more compact and transparent 2026-05-02 14:24:13 +02:00
JonKazama-Hellion 757370dd53 feat(auto-tell-tabs): add settings sections in chat and privacy tabs with help-marker pattern 2026-05-02 14:19:35 +02:00
JonKazama-Hellion 3f35b76c54 feat(auto-tell-tabs): render section header and greeted toggle in tab sidebar 2026-05-02 14:08:54 +02:00
JonKazama-Hellion 74bdc4f927 feat(auto-tell-tabs): add hint string for plugins that suppress tells (XIV Messanger) 2026-05-02 14:06:27 +02:00
JonKazama-Hellion eb379d84ef feat(auto-tell-tabs): add i18n strings and history preload markers 2026-05-02 14:00:29 +02:00
JonKazama-Hellion 7add74dbbe feat(auto-tell-tabs): enable sidebar tab view by default for fresh installs 2026-05-02 13:56:10 +02:00
JonKazama-Hellion e91c7a3888 fix(auto-tell-tabs): preserve temp tabs across Configuration.UpdateFrom 2026-05-02 13:49:47 +02:00
JonKazama-Hellion f8b0804321 fix(auto-tell-tabs): fall back to SeString payloads for tell sender extraction 2026-05-02 13:45:00 +02:00
JonKazama-Hellion a9d4e9bd69 feat(auto-tell-tabs): wire AutoTellTabsService into plugin lifecycle 2026-05-02 13:29:09 +02:00
JonKazama-Hellion 7e3e4c8b72 feat(auto-tell-tabs): implement greeted toggle with frame-race guard 2026-05-02 13:28:17 +02:00
JonKazama-Hellion 397c84be2c feat(auto-tell-tabs): spawn temp tab with synchronous history preload 2026-05-02 13:27:53 +02:00
JonKazama-Hellion 269708150d feat(auto-tell-tabs): implement logout cleanup of temp tabs 2026-05-02 13:02:15 +02:00
JonKazama-Hellion a2977ef75b feat(auto-tell-tabs): implement LRU drop with greeted priority 2026-05-02 13:01:51 +02:00
JonKazama-Hellion baa4d011e8 feat(auto-tell-tabs): implement HandleTell with sender extraction and tab lookup 2026-05-02 13:01:27 +02:00
JonKazama-Hellion 4810e8b518 feat(auto-tell-tabs): add AutoTellTabsService skeleton with lifecycle 2026-05-02 12:59:58 +02:00
JonKazama-Hellion 133f5c536f feat(auto-tell-tabs): emit MessageProcessed event after tab routing 2026-05-02 12:57:49 +02:00
JonKazama-Hellion 92bb368d2b feat(auto-tell-tabs): add GetTellHistoryWithSender query and ChunkUtil sender helper 2026-05-02 12:52:58 +02:00
JonKazama-Hellion 07f47f32e3 feat(auto-tell-tabs): bump configuration version to 9 with migration notice 2026-05-02 12:40:22 +02:00
JonKazama-Hellion 141fcbf074 feat(auto-tell-tabs): filter temp tabs from config save and load (defense-in-depth) 2026-05-02 12:37:06 +02:00
JonKazama-Hellion 32c410e8e2 feat(auto-tell-tabs): add IsGreeted flag and Tab.Matches sender filter for temp tabs 2026-05-02 12:30:30 +02:00
JonKazama-Hellion 824037e55f feat(auto-tell-tabs): add configuration fields for auto tell tabs 2026-05-02 12:27:55 +02:00
JonKazama-Hellion 173cb76bea fix: ignore brainstorming and spec workspace directories 2026-05-02 12:20:55 +02:00
JonKazama-Hellion 2736551505 Bump to 0.3.1 with the upstream emote regression fix
Quick patch release. The previous commit on this branch is a
straight cherry-pick of Infi's upstream "Fix a regression from
API 15 updates" (ff899ff), which restores BetterTTV emote
deserialisation by switching the Emote and Top100 DTOs from
public fields to public properties so System.Text.Json picks up
the [JsonPropertyName] attributes correctly.

Without this, a fresh Hellion Chat install would happily fetch
emote JSON from BetterTTV but every entry would deserialise to
empty defaults, leaving the cache silently empty.

Manifest version bumps from 0.3.0 to 0.3.1 in csproj, the
HellionChat.yaml manifest plus its changelog, the custom-repo
repo.json (assembly version, testing assembly version and the
three download links), and the README version banner. The 0.3.0
changelog entry stays underneath in chronological order.

Build (Release) verified clean.
2026-05-02 04:11:13 +02:00
Infi 0679a0e57a Fix a regression from API 15 updates
(cherry picked from commit ff899ffe54ef76bcdf9c13b5690957c5b5e17637)
2026-05-02 04:10:22 +02:00
JonKazama-Hellion 02cbfff748 merge Update and Fixes for Branding and Security Fixes Merge branch 'feature/audit-fixes-phase-2' 2026-05-02 04:02:28 +02:00
JonKazama-Hellion 9c86619c9f Bump to 0.3.0 with the audit, brand and command-rename changelog
Phase 2 of the audit follow-ups, the Hellion Online Media brand
sweep and the rename of the slash commands all land in one release.
The slash command rename is breaking for users who had macros bound
to /chat2, /chat2Viewer or /clearlog2, which is the main reason
this is a 0.3.0 rather than a 0.2.1.

csproj, plugin manifest yaml, custom-repo repo.json (assembly
version, testing assembly version and the three download links)
and the README version banner are all moved over together so the
Dalamud plugin list, the manifest and the install instructions
agree. The README project status checklist is updated to reflect
that Phase 2 is closed; Phase 3 holds the remaining backlog
(MySQL backend, encryption, libnotify, etc).

The yaml and repo.json changelogs gain a 0.3.0 block that walks
through the four substantial groups of changes (slash command
rename, audit hardening, brand sweep, About tab) in plain prose.
The 0.2.0 block stays underneath in chronological order.

Build (Release) verified — ChatTwo/bin/Release/HellionChat/latest.zip
(~17.5 MB) and HellionChat.json regenerate cleanly with no warnings.
The tag itself is created by hand alongside the GitHub release.
2026-05-02 03:59:25 +02:00
JonKazama-Hellion 6b44310e04 Fix the About tab so the translator list is reachable
The translator section sat inside a child window whose height was
computed as "whatever is left in the content region minus a line".
Once the About copy grew with the new mission and rewritten built-on
sections, that remaining space dropped close to zero on smaller
settings windows, so the tree node was rendered but its content was
either invisible or unscrollable from the parent.

Drop the fixed-height child entirely. The settings window already
provides a scroll container around each tab, so rendering the tree
node and the translator list directly into it lets the parent
handle the scroll. The translators get a manual indent push to keep
the visual nesting that the child-frame used to suggest.
2026-05-02 03:54:34 +02:00
JonKazama-Hellion 59332ce9ea Move About-tab copy to HellionStrings and rephrase neutrally
The About tab copy was hand-written in English directly in About.cs,
which left the German users on the upstream Chat 2 wording for the
Hellion-specific blocks. The copy itself also leaned on em-dashes
mid-sentence and on a tone that could read as accusing Chat 2 of
GDPR violations, which was never the intent. This commit moves the
six About-tab sections (Maintainer, Why this fork exists, Built on
Chat 2, License, FFXIV disclaimer, Localization) into HellionStrings
and tightens the wording in both languages.

Tone change is the substantive part. Chat 2's full-history default
is now described as "the right one for most users" rather than a
problem the fork is fixing, and the webinterface removal is framed
as a focus mismatch — Chat 2's webinterface targets remote chat
access from a second device, this fork targets a smaller default
footprint, neither approach is wrong. The personal trigger for the
fork (two million logged messages over two years, mostly /say and
/yell from strangers) stays as it is honest context rather than
criticism.

The same neutralised wording is mirrored in three more places that
described the webinterface removal: the README "Was gegenüber
Chat 2 fehlt" block, the HellionChat.yaml description and changelog,
and the matching repo.json fields. The 0.2.0 changelog no longer
recites the upstream auth-flow internals; "different use case,
substantial rebuild, removed" is enough for users.

Em-dashes were also removed from two body strings that previously
used them as comma replacements (Privacy filter storage-only help
and the retention default description). Heading-level dashes
("Hellion Chat — Welcome", "Export (GDPR Art. 15 — right of
access)") stay because dashes are appropriate as separators in
titles.

The Theme description was already inaccurate — it still talked
about slate-violet tabs and amber highlights even though the brand
sweep moved everything onto Arctic Cyan plus Ember Orange. Updated
to describe the current palette honestly.
2026-05-02 03:50:08 +02:00
JonKazama-Hellion 462530dec5 Add a mission statement to the About tab
The About tab already credits Chat 2 and the maintainers, but it
never said why this fork exists in the first place. New users
discovering Hellion Chat through the Dalamud plugin list could
reasonably read it as a replacement attempt rather than what it is:
a niche alternative for users who care about chat persistence and
data minimisation.

The new "Why this fork exists" block sits between the Maintainer
and the Built-on-Chat-2 sections so the reading order goes from
"who" to "why" to "from what". It states three things plainly:

  - Hellion Chat is not trying to replace Chat 2
  - The trigger was the maintainer's own database (two years,
    two million messages, mostly public-chat from strangers)
  - Source is open under the same EUPL-1.2 licence; the upstream
    authors are welcome to look, take ideas, ask, or ignore

The tone matches the rest of the About tab — direct, no marketing
voice — and stays in English with the other legal-ish copy so a
single source covers every locale.
2026-05-02 03:40:57 +02:00
JonKazama-Hellion 8e964ca498 Align HellionStyle with the Hellion Online Media brand palette
The plugin theme drifted from the website palette over time: cyan
sat at #00B8D4 instead of the brand #00BED2, the warm highlights
were industrial amber rather than Ember Orange, and active tabs and
title bars were rendered in slate violet — a shade not part of the
brand at all.

This commit moves every HellionStyle slot onto the Arctic Cyan +
Ember Glow tokens documented in the website's BRANDING.md:

  - Primary cyan slots (Button, CheckMark, Slider, Separator) now
    use brand-color / brand-color-light / brand-color-dark
  - Window title bars and the active tab use brand-color-dark as
    Identity teal — slightly varied so it reads as identity rather
    than as another action surface
  - Unfocused-active tabs drop to a deeper teal so the unfocused
    window's tab is still visible without pulling focus
  - Resize grips and scrollbar grabs lift into Ember Orange on hover
    and active states, replacing industrial amber
  - Window, child, popup, frame and header surfaces follow the
    brand background ladder (#070B12, #0C1220, #141E30, #1A2538,
    #22303F)
  - Borders use the brand cyan at 40% alpha (matches --border-brand
    on the website) instead of neutral steel grey

The slate violet tertiary palette is gone. Brand tokens are
declared once and the slot constants alias them, so a future brand
shift only needs to touch the Identity / Accent / Primary stages.
2026-05-02 03:25:29 +02:00
JonKazama-Hellion 1f2cb000a2 Rename slash commands from /chat2 to /hellion family
Hellion Chat is an independent fork with its own assembly name and
plugin slot, but it kept registering the upstream /chat2* slash
commands. That mixed naming caused two friction points: users could
not tell from the in-game help which plugin owned the command, and
running both Hellion Chat and the upstream Chat 2 side by side would
collide on the registration.

Five commands change:

  /chat2          -> /hellion           (settings + chat toggle)
  /chat2Viewer    -> /hellionView       (database viewer)
  /chat2Debugger  -> /hellionDebugger   (internal, not shown in help)
  /chat2SeString  -> /hellionSeString   (internal, debug-only)
  /clearlog2      -> /clearhellion      (clear chat log)

Help strings ("Perform various actions with Chat 2.", "Clear the
Chat 2 chat log") are reworded to match. ImGui internal window IDs
(###chat2-settings, ###chat2-dbviewer) are left untouched on purpose
so existing user layouts for those windows do not snap back to
default. Resource files do not reference any of these command names,
so no localisation work needed.
2026-05-02 03:08:25 +02:00
JonKazama-Hellion 4f25c2756b Tighten DbViewer paging — int constant and matching SQL parameter name
Audit findings M-1 and M-2. Two small consistency issues in the
upstream DbViewer paging path that we now own as a fork:

  - RowPerPage is a row count and should be an int. The upstream
    declaration was 1000.0f, which forced an implicit float divide
    in Math.Ceiling and an implicit float-to-integer conversion when
    SQLite bound the LIMIT parameter. Switching the constant to int
    and casting Count to double right at the division keeps the
    ceiling math intact while making the type story honest.

  - GetPagedDateRange's SQL uses the placeholder $OffsetCount, but
    the matching AddWithValue call passed the unprefixed name
    "OffsetCount". Microsoft.Data.Sqlite tolerates this today, so
    paging still worked; another provider or a stricter future
    version would not. Re-aligned the parameter name with the SQL.

No behavioural change for users — paging continues to return 1000
rows per page. The fixes are kept on the fork rather than offered
upstream because the project's recent triage history makes a
non-trivial PR turnaround unlikely.
2026-05-02 02:56:40 +02:00
JonKazama-Hellion de0d2c80cd Serialise retention sweeps so the auto and manual paths cannot overlap
Audit findings M-3 and M-4. The 24h auto-sweep launched from
Plugin's constructor and the manual button in the Privacy tab were
both starting a background thread that called DeleteByRetentionPolicy
on the shared MessageStore connection without coordinating. With
unfortunate timing — manual click moments after a fresh plugin load
— two sweeps would race for the same connection and the second
would just re-do work the first one already did, while still
overwriting RetentionLastRunAt.

Move the running flag and a lock object to Plugin so both paths see
the same gate. Each entry point takes the lock long enough to check
and set the flag, then runs the actual delete on its background
thread without holding the lock (other DB operations already happen
without locking; spreading the lock further would suggest a
guarantee we do not actually provide). The Privacy tab keeps a
read-only property that surfaces the shared flag for its UI disable
state — ImGui is single-threaded and bool reads are atomic, so the
lock-free read is fine.
2026-05-02 02:52:34 +02:00
JonKazama-Hellion 2ce30383d9 Refuse to write emote cache files outside the cache directory
Audit finding H-1. Defense-in-depth fix for EmoteCache.LoadAsync,
which interpolated the BetterTTV-supplied Id and ImageType straight
into a Path.Join. HTTPS protects the wire today, but a compromised
upstream that hands back Id values like "../foo" would land outside
EmoteCacheV1, anywhere under pluginConfigs that the plugin can write.

Resolve the candidate path with Path.GetFullPath, then assert it
starts with the cache directory plus a directory separator (so
"EmoteCacheV1Sibling" cannot match "EmoteCacheV1"). Throw
InvalidOperationException on mismatch — the surrounding load
already swallows exceptions and logs them, so a tampered entry
becomes a visible error in the log instead of a silent miss.
2026-05-02 02:50:29 +02:00
JonKazama-Hellion a857714064 Clarify the privacy filter only governs storage, not the live chat log
Audit finding M-5. The master switch description told users what the
filter does to the database, but nothing in the UI ruled out the
common misreading "if I disable a channel, it will also disappear
from the chat log". A help-text line under the toggle now states
explicitly that the filter is storage-only and points at the
in-game chat tab filters for hiding channels visually. EN and DE
strings added together.
2026-05-02 02:49:45 +02:00
43 changed files with 3464 additions and 1249 deletions
+2
View File
@@ -372,6 +372,8 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
#Specs und Plan datein
/.superpowers/
TestResults TestResults
*.db-shm *.db-shm
*.db-wal *.db-wal
+56 -21
View File
@@ -1,18 +1,49 @@
# AI assistance disclosure # AI assistance disclosure
Per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/), This fork uses AI assistance per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/)
this fork uses AI assistance at the **Pair** level. Pair means the maintainer at the **Pair** level.
plans the architecture, decides what gets built, reviews each change and
tests against the running game; Claude (Anthropic) helps explain Dalamud
APIs, suggests patterns, drafts code on request, and reviews approaches.
Neither side acts autonomously: nothing ships without the maintainer's
review, and Claude can't run the game.
The level varies by area and over time. Some commits are mostly hand-written A note up front: Hellion Chat is currently in a rebuild and adjustment
with the AI used as a sounding board, others lean more on Claude for an API phase, and there are no plans to submit it to the Dalamud team for review
walkthrough or a code draft that the maintainer then reads, edits and while it stays standalone. If the plugin stays out of the official repo I
integrates. The maintainer's commitment is to be able to explain why every technically wouldn't need to disclose any of this, but I'd rather be
piece of Hellion code is the way it is — not "I typed every character." upfront about how it's built.
Hellion Chat is my entry point into game modding and plugin development. I
have never written a plugin for a game before. I work alone, so I get help
where I need it. That's not something I want to hide.
## How I actually work
I plan the architecture, decide what gets built, and own every design
decision. For each change I:
- Read the code Claude drafts before I integrate it
- Test with my own tooling and in the running game
- Read the Dalamud log output to verify behaviour
- Run security and privacy audits on anything that touches user data
One of the main reasons I use AI is consistency. I want the Hellion code to
match the style of the upstream Chat 2 codebase and stay readable for
anyone who opens the repo, not just for me. Claude helps me catch when I'm
drifting from upstream conventions or writing something that only makes
sense in my own head.
The balance is shifting toward more hand-written work as I get more
comfortable with Dalamud and plugin development in general.
## What AI is used for
- API explanations (Dalamud, ImGui, .NET specifics I haven't worked with before)
- Code drafts that I read, edit, and integrate
- Pattern suggestions and code review
- Keeping the style aligned with the upstream Chat 2 codebase
## What AI isn't used for
- **Visual assets.** Logos, icons, banners, and screenshots are human-drawn
or taken from the running game.
- **German translations.** Written by me as a native speaker.
## What's where ## What's where
@@ -22,20 +53,24 @@ produced with AI assistance. Hellion-specific code lives in
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`, `Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs` plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
and `Plugin.cs`. These were developed with Pair-level assistance as and `Plugin.cs`. These were developed with Pair-level assistance as
described above; the share of human vs. AI authorship varies file by file described above.
and is expected to keep shifting toward more hand-written work as the
maintainer's plugin-dev experience grows.
## What AI is not used for ## If AI-assisted development is a dealbreaker for you
- **Visual assets.** Logos, icons, banners, screenshots are human-drawn or Fair enough. There are solid alternatives that don't rely on AI in their
taken from the running game. development:
- **German translations.** Written by the maintainer (native speaker).
- [Chat 2](https://github.com/Infiziert90/ChatTwo), the original upstream
this fork is based on
- [XIV Instant Messenger](https://github.com/NightmareXIV/XIVInstantMessenger),
a different approach to chat in FFXIV
Both are good projects. Use what fits you best.
## Tooling ## Tooling
- Claude (Anthropic) via Claude Code CLI as the main pair partner. - Claude (Anthropic) via Claude Code CLI
- Context7 / Microsoft Learn for current Dalamud and .NET documentation. - Context7 / Microsoft Learn for current Dalamud and .NET documentation
## Contact ## Contact
+167
View File
@@ -0,0 +1,167 @@
using System;
using System.IO;
using System.Linq;
using ChatTwo.Code;
using ChatTwo.Util;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ChatTwo.Tests;
// Hellion Chat — Auto-Tell-Tabs history-preload coverage.
//
// These tests exercise MessageStore.GetTellHistoryWithSender, the query the
// AutoTellTabsService uses to populate a freshly spawned temp tab with the
// last conversations with that player.
//
// NOTE: like the rest of ChatTwo.Tests today, these will fail at runtime
// until the project's Dalamud.dll runtime dependency is sorted out (see
// Phase-2 backlog item "Test-Projekt fixen"). Compile-time the suite builds
// fine via DALAMUD_HOME, so the tests guard against API drift even before
// they can be executed locally.
[TestClass]
[TestSubject(typeof(MessageStore))]
public class AutoTellTabsHistoryTest
{
public TestContext TestContext { get; set; }
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_FiltersByNameAndWorld()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99001;
var now = DateTimeOffset.UtcNow;
// Two tells with the target sender, one with a different sender on
// the same world, one with the same name on a different world. Only
// the first two should make it into the result.
var asukaLichIn = TellMessage("Asuka", 76, receiver, now.AddMinutes(-30), ChatType.TellIncoming);
var asukaLichOut = TellMessage("Asuka", 76, receiver, now.AddMinutes(-20), ChatType.TellOutgoing);
var broboLich = TellMessage("Brobo", 76, receiver, now.AddMinutes(-10), ChatType.TellIncoming);
var asukaOmega = TellMessage("Asuka", 90, receiver, now.AddMinutes(-5), ChatType.TellIncoming);
store.UpsertMessage(asukaLichIn);
store.UpsertMessage(asukaLichOut);
store.UpsertMessage(broboLich);
store.UpsertMessage(asukaOmega);
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 50);
Assert.AreEqual(2, result.Count);
// Result is oldest-first so a tab can append messages chronologically.
Assert.AreEqual(asukaLichIn.Id, result[0].Id);
Assert.AreEqual(asukaLichOut.Id, result[1].Id);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_RespectsLimit()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99002;
var now = DateTimeOffset.UtcNow;
for (var i = 0; i < 30; i++)
{
var msg = TellMessage("Asuka", 76, receiver, now.AddMinutes(-i - 1), ChatType.TellIncoming);
store.UpsertMessage(msg);
}
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 5);
Assert.AreEqual(5, result.Count);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_ZeroLimitReturnsEmpty()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99003;
var msg = TellMessage("Asuka", 76, receiver, DateTimeOffset.UtcNow, ChatType.TellIncoming);
store.UpsertMessage(msg);
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 0);
Assert.AreEqual(0, result.Count);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_IgnoresOtherReceivers()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong ourReceiver = 99004;
const ulong otherReceiver = 99005;
var now = DateTimeOffset.UtcNow;
// Tell on the local player's account.
var ours = TellMessage("Asuka", 76, ourReceiver, now.AddMinutes(-1), ChatType.TellIncoming);
// Same sender, but logged against a different local character —
// common when the user has alts. Must not surface.
var foreign = TellMessage("Asuka", 76, otherReceiver, now, ChatType.TellIncoming);
store.UpsertMessage(ours);
store.UpsertMessage(foreign);
var result = store.GetTellHistoryWithSender(ourReceiver, "Asuka", 76, limit: 50);
Assert.AreEqual(1, result.Count);
Assert.AreEqual(ours.Id, result[0].Id);
}
private static Message TellMessage(
string senderName,
uint senderWorld,
ulong receiver,
DateTimeOffset dateTime,
ChatType chatType)
{
var senderSeString = new SeStringBuilder()
.Add(new PlayerPayload(senderName, senderWorld))
.AddText(senderName)
.Add(RawPayload.LinkTerminator)
.Build();
var contentSeString = new SeStringBuilder()
.AddText("test message")
.Build();
var senderChunks = ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, chatType).ToList();
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, chatType).ToList();
var chatCode = new ChatCode((XivChatType)chatType, XivChatRelationKind.LocalPlayer, XivChatRelationKind.LocalPlayer);
return new Message(
Guid.NewGuid(),
receiver,
0,
dateTime,
chatCode,
senderChunks,
contentChunks,
senderSeString,
contentSeString,
Guid.Empty);
}
}
+358
View File
@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
namespace ChatTwo;
// Hellion Chat — Auto-Tell-Tabs.
//
// Spawns a session-only tab per /tell partner so a club greeter can track
// multiple parallel conversations without losing context. Subscribes to
// MessageManager.MessageProcessed for live tells and to ClientState.Logout
// for the cleanup pass; everything else hangs off these two entry points.
//
// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault).
internal sealed class AutoTellTabsService : IDisposable
{
private readonly Plugin _plugin;
private readonly MessageManager _messageManager;
private readonly MessageStore _store;
private readonly object _tempTabsLock = new();
private bool _initialized;
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
{
_plugin = plugin;
_messageManager = messageManager;
_store = store;
}
internal int ActiveTempTabCount
{
get
{
lock (_tempTabsLock)
{
return Plugin.Config.Tabs.Count(t => t.IsTempTab);
}
}
}
internal void Initialize()
{
if (_initialized)
{
return;
}
_messageManager.MessageProcessed += HandleTell;
Plugin.ClientState.Logout += OnLogout;
_initialized = true;
}
public void Dispose()
{
if (!_initialized)
{
return;
}
Plugin.ClientState.Logout -= OnLogout;
_messageManager.MessageProcessed -= HandleTell;
_initialized = false;
}
internal void HandleTell(Message message)
{
if (!Plugin.Config.EnableAutoTellTabs)
{
return;
}
if (message.Code.Type != ChatType.TellIncoming && message.Code.Type != ChatType.TellOutgoing)
{
return;
}
var partner = ExtractTellPartner(message);
if (partner == null)
{
// Real message without a player payload — e.g. GM tells, which
// we deliberately skip. The diagnostics make future regressions
// (FFXIV changing tell payload shape, new edge cases) findable
// without having to crank up debug logging at the source.
Plugin.Log.Warning(
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " +
$"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " +
$"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " +
$"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}");
return;
}
lock (_tempTabsLock)
{
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
if (existing != null)
{
// Tab already exists; Tab.Matches has already routed this
// message via the MessageManager pipeline (see Task 2 sender
// filter).
return;
}
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
{
DropOldestTempTab();
}
SpawnTempTab(partner.Value, message);
}
}
private (string Name, uint World)? ExtractTellPartner(Message message)
{
if (message.Code.Type == ChatType.TellIncoming)
{
// Incoming tell: the sender is the conversation partner. The
// PlayerPayload normally rides on a chunk's Link slot, but for
// some tell types FFXIV only puts it in the raw SeString —
// fall back to that before giving up.
var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender)
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
if (fromSender != null)
{
return (fromSender.PlayerName, fromSender.World.RowId);
}
return null;
}
// Outgoing tell: the local player is the sender, the partner shows
// up either as a payload in the content (for tells typed via the
// Chat 2 input bar) or as the channel's tracked tell target (set by
// the SetContextTellTarget game hook). Same SeString fallback.
var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content)
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
if (fromContent != null)
{
return (fromContent.PlayerName, fromContent.World.RowId);
}
var current = _plugin.CurrentTab.CurrentChannel.TellTarget
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
if (current != null && current.IsSet())
{
return (current.Name, current.World);
}
return null;
}
private Tab? FindTempTab(string name, uint world)
{
return Plugin.Config.Tabs.FirstOrDefault(t =>
t.IsTempTab
&& t.TellTarget != null
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
&& t.TellTarget.World == world);
}
private void DropOldestTempTab()
{
// Greeted tabs are dropped before un-greeted ones (the user said
// "I'm done with that conversation"), and within each bucket we
// pick the oldest LastActivity. This protects active conversations
// and unfinished greetings while still freeing up a slot.
var victim = Plugin.Config.Tabs
.Select((tab, idx) => (Tab: tab, Index: idx))
.Where(t => t.Tab.IsTempTab)
.OrderByDescending(t => t.Tab.IsGreeted)
.ThenBy(t => t.Tab.LastActivity)
.FirstOrDefault();
if (victim.Tab == null)
{
return;
}
Plugin.Config.Tabs.RemoveAt(victim.Index);
// Re-anchor the active tab so the user does not silently end up on
// a different conversation when their tab gets dropped or shifted.
if (victim.Index <= _plugin.LastTab)
{
_plugin.WantedTab = 0;
}
}
private void SpawnTempTab((string Name, uint World) partner, Message currentMessage)
{
var tab = BuildTempTab(partner.Name, partner.World);
// Preload first so the tab opens with chronological history above
// the current message — and so a slow DB query never causes a
// visible "empty tab, then history pops in" effect on screen.
PreloadHistory(tab, partner.Name, partner.World);
tab.AddMessage(currentMessage, unread: true);
Plugin.Config.Tabs.Add(tab);
}
private static Tab BuildTempTab(string playerName, uint worldRowId)
{
return new Tab
{
Name = FormatTabName(playerName, worldRowId),
IsTempTab = true,
AllSenderMessages = true,
TellTarget = new TellTarget(playerName, worldRowId, 0, TellReason.Direct),
Channel = InputChannel.Tell,
DisplayTimestamp = true,
UnreadMode = UnreadMode.Unseen,
HideWhenInactive = false,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
},
};
}
private static string FormatTabName(string playerName, uint worldRowId)
{
if (Sheets.WorldSheet.TryGetRow(worldRowId, out var worldRow))
{
return $"{playerName}@{worldRow.Name}";
}
// World sheet lookup miss is rare (only for FFXIV worlds Dalamud has
// not yet seen). Fall back to the raw RowId so the user still has a
// unique, readable label.
return $"{playerName}@World{worldRowId}";
}
private void PreloadHistory(Tab tab, string senderName, uint senderWorld)
{
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
if (preloadCount <= 0)
{
return;
}
try
{
var history = _store.GetTellHistoryWithSender(
_messageManager.CurrentContentId,
senderName,
senderWorld,
preloadCount);
if (history.Count == 0)
{
// No prior tells with this player — leave the tab to start
// empty so the user does not see a "history loaded" marker
// sitting alone above the very first message.
return;
}
// The history list is already oldest-first, so a plain AddPrune
// loop produces the chronological order the user expects to see
// when the tab opens.
foreach (var message in history)
{
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
}
// Visible separator between the loaded history and the live
// tell that triggered this spawn. Goes in last so it sorts
// after the historical messages but before the current one.
tab.Messages.AddPrune(
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
MessageManager.MessageDisplayLimit);
}
catch (Exception ex)
{
// Non-fatal: the tab still spawns, but the user gets a visible
// notice instead of silently missing history. The error logs
// once with full stack trace for diagnosis.
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
tab.Messages.AddPrune(
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
MessageManager.MessageDisplayLimit);
}
}
private static Message MakeSystemMarker(string text)
{
var seString = new SeStringBuilder().AddText(text).Build();
var chunks = ChunkUtil.ToChunks(seString, ChunkSource.Content, ChatType.System).ToList();
var code = new ChatCode((XivChatType)ChatType.System, 0, 0);
return Message.FakeMessage(chunks, code);
}
internal void MarkGreeted(Tab tab)
{
SetGreeted(tab, true);
}
internal void UnmarkGreeted(Tab tab)
{
SetGreeted(tab, false);
}
internal bool IsGreeted(Tab tab)
{
return tab.IsGreeted;
}
private void SetGreeted(Tab tab, bool greeted)
{
if (tab == null)
{
return;
}
lock (_tempTabsLock)
{
// Frame-race guard (E5): the sidebar might still render a tab
// that has already been removed by LRU drop or logout cleanup.
// Silently skip the toggle so we don't mutate stale state.
if (!Plugin.Config.Tabs.Contains(tab))
{
return;
}
tab.IsGreeted = greeted;
}
}
private void OnLogout(int type, int code)
{
lock (_tempTabsLock)
{
// Snapshot whether the active tab is about to be removed, BEFORE
// we mutate the list — index lookups would lie to us afterwards.
var lastIndex = _plugin.LastTab;
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
// Force a switch to tab 0 if the active tab was a temp tab OR
// if drops before the active index pushed LastTab out of range.
// Otherwise the user keeps their current persistent tab.
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
if (currentWasTempTab || !stillValid)
{
_plugin.WantedTab = 0;
}
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
0.1.0 is our bootstrap release; the underlying Chat 2 base is 0.1.0 is our bootstrap release; the underlying Chat 2 base is
called out in the yaml changelog so users can see what it called out in the yaml changelog so users can see what it
derives from. --> derives from. -->
<Version>0.2.0</Version> <Version>0.5.1</Version>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<!-- HellionChat fork: assembly is renamed so Dalamud uses <!-- HellionChat fork: assembly is renamed so Dalamud uses
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo, pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
+66 -6
View File
@@ -6,6 +6,7 @@ using ChatTwo.Util;
using Dalamud; using Dalamud;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@@ -33,7 +34,7 @@ public class ConfigKeyBind
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
private const int LatestVersion = 8; private const int LatestVersion = 10;
public int Version { get; set; } = LatestVersion; public int Version { get; set; } = LatestVersion;
@@ -81,6 +82,25 @@ public class Configuration : IPluginConfiguration
// to fall back to the user's chosen system or Dalamud font. // to fall back to the user's chosen system or Dalamud font.
public bool UseHellionFont = true; public bool UseHellionFont = true;
// Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
// /tell spawns a session-only tab dedicated to that conversation
// partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
public bool EnableAutoTellTabs = true;
// Hard cap on simultaneously open auto tell tabs. Range enforced by the
// settings slider (150). LRU drop favors greeted tabs first.
public int AutoTellTabsLimit = 15;
// When true the sidebar shows only a thin separator before the temp
// tabs; when false a section header "Active Tells (n)" is rendered.
public bool AutoTellTabsCompactDisplay;
// Number of prior tells to preload from the message store when an
// auto tell tab is spawned. Range 0100; 0 disables preload.
public int AutoTellTabsHistoryPreload = 20;
// Show the greeter "marked-as-greeted" toggle button next to each
// temp tab and dim the tab name when set. Off by default because the
// workflow is specific to club-greeter use cases — most users just
// want the auto tabs themselves without the extra UI affordance.
public bool AutoTellTabsShowGreetedToggle;
public int GetRetentionDays(ChatType type) public int GetRetentionDays(ChatType type)
{ {
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
@@ -112,7 +132,12 @@ public class Configuration : IPluginConfiguration
public bool MoreCompactPretty; public bool MoreCompactPretty;
public bool HideSameTimestamps; public bool HideSameTimestamps;
public bool ShowNoviceNetwork; public bool ShowNoviceNetwork;
public bool SidebarTabView; // Hellion Chat — vertical sidebar tab layout reads better than the
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
// greeter typically tracks 515 simultaneous conversations). Bestand
// users keep their saved value untouched — only fresh installs pick
// up the new default.
public bool SidebarTabView = true;
public bool PrintChangelog = true; public bool PrintChangelog = true;
public bool OnlyPreviewIf; public bool OnlyPreviewIf;
public int PreviewMinimum = 1; public int PreviewMinimum = 1;
@@ -122,7 +147,7 @@ public class Configuration : IPluginConfiguration
public LanguageOverride LanguageOverride = LanguageOverride.None; public LanguageOverride LanguageOverride = LanguageOverride.None;
public bool CanMove = true; public bool CanMove = true;
public bool CanResize = true; public bool CanResize = true;
public bool ShowTitleBar; public bool ShowTitleBar = true;
public bool ShowPopOutTitleBar = true; public bool ShowPopOutTitleBar = true;
public bool DatabaseBattleMessages; public bool DatabaseBattleMessages;
public bool LoadPreviousSession; public bool LoadPreviousSession;
@@ -132,7 +157,7 @@ public class Configuration : IPluginConfiguration
public bool CollapseKeepUniqueLinks; public bool CollapseKeepUniqueLinks;
public bool PlaySounds = true; public bool PlaySounds = true;
public bool KeepInputFocus = true; public bool KeepInputFocus = true;
public int MaxLinesToRender = 10_000; // 1-10000 public int MaxLinesToRender = 5_000; // 1-10000
public bool Use24HourClock; public bool Use24HourClock;
public bool ShowEmotes = true; public bool ShowEmotes = true;
@@ -230,7 +255,17 @@ public class Configuration : IPluginConfiguration
TooltipOffset = other.TooltipOffset; TooltipOffset = other.TooltipOffset;
WindowAlpha = other.WindowAlpha; WindowAlpha = other.WindowAlpha;
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
Tabs = other.Tabs.Select(t => t.Clone()).ToList();
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
// never present in a disk-loaded copy. Keep the live temp tabs of
// *this* configuration alive across an UpdateFrom so a settings
// save (or sidebar-mode toggle) does not silently destroy the
// user's open tell conversations. Persistent tabs from `other`
// still get the regular clone-replace treatment.
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList();
Tabs.AddRange(liveTempTabs);
OverrideStyle = other.OverrideStyle; OverrideStyle = other.OverrideStyle;
ChosenStyle = other.ChosenStyle; ChosenStyle = other.ChosenStyle;
ChatTabForward = other.ChatTabForward; ChatTabForward = other.ChatTabForward;
@@ -249,6 +284,12 @@ public class Configuration : IPluginConfiguration
HellionThemeEnabled = other.HellionThemeEnabled; HellionThemeEnabled = other.HellionThemeEnabled;
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
UseHellionFont = other.UseHellionFont; UseHellionFont = other.UseHellionFont;
EnableAutoTellTabs = other.EnableAutoTellTabs;
AutoTellTabsLimit = other.AutoTellTabsLimit;
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
} }
} }
@@ -324,9 +365,27 @@ public class Tab
[NonSerialized] public Guid Identifier = Guid.NewGuid(); [NonSerialized] public Guid Identifier = Guid.NewGuid();
// Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
// sidebar to mark a tell partner as already greeted in the current
// session. NonSerialized because the temp tab itself is session-only.
[NonSerialized] public bool IsGreeted;
public bool Matches(Message message) public bool Matches(Message message)
{ {
return message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels); if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
{
return false;
}
// Auto-tell temp tabs are bound to a single conversation partner;
// every other tell that matches the channel filter must NOT land
// here, otherwise all temp tabs would mirror "Tell Exclusive".
if (IsTempTab && TellTarget?.IsSet() == true)
{
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
}
return true;
} }
public void AddMessage(Message message, bool unread = true) public void AddMessage(Message message, bool unread = true)
@@ -375,6 +434,7 @@ public class Tab
IsTempTab = IsTempTab, IsTempTab = IsTempTab,
AllSenderMessages = AllSenderMessages, AllSenderMessages = AllSenderMessages,
TellTarget = TellTarget.From(TellTarget), TellTarget = TellTarget.From(TellTarget),
IsGreeted = IsGreeted,
}; };
} }
+22 -8
View File
@@ -32,23 +32,23 @@ public static class EmoteCache
private struct Top100() private struct Top100()
{ {
[JsonPropertyName("emote")] [JsonPropertyName("emote")]
public Emote Emote = default; public Emote Emote { get; set; }
[JsonPropertyName("id")] [JsonPropertyName("id")]
public string Id = string.Empty; public required string Id { get; set; }
} }
[Serializable] [Serializable]
public struct Emote() public struct Emote()
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public string Id = string.Empty; public required string Id { get; set; }
[JsonPropertyName("code")] [JsonPropertyName("code")]
public string Code = string.Empty; public required string Code { get; set; }
[JsonPropertyName("imageType")] [JsonPropertyName("imageType")]
public string ImageType = string.Empty; public required string ImageType { get; set; }
} }
public enum LoadingState public enum LoadingState
@@ -66,7 +66,7 @@ public static class EmoteCache
public static string[] SortedCodeArray = []; public static string[] SortedCodeArray = [];
public static async void LoadData() public static async Task LoadData()
{ {
if (State is not LoadingState.Unloaded) if (State is not LoadingState.Unloaded)
return; return;
@@ -105,6 +105,11 @@ public static class EmoteCache
} }
catch (Exception ex) catch (Exception ex)
{ {
// Reset to Unloaded so a later trigger (e.g. the user reopening
// the Emotes tab after the network recovers) can retry. Without
// this the State stays on Loading and the early-out at the top
// of LoadData blocks every further attempt until plugin reload.
State = LoadingState.Unloaded;
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized"); Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
} }
} }
@@ -168,10 +173,19 @@ public static class EmoteCache
internal async Task<byte[]> LoadAsync(Emote emote) internal async Task<byte[]> LoadAsync(Emote emote)
{ {
var dir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"); // BetterTTV-supplied Id and ImageType are interpolated straight
// into the filename. HTTPS protects the wire, but a compromised
// upstream could still hand us "../foo" and write into the
// pluginConfigs root (or worse). Resolve the candidate path and
// refuse anything that escapes the cache directory.
var dir = Path.GetFullPath(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"));
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
var filePath = Path.Join(dir, $"{emote.Id}.{emote.ImageType}"); var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar) ? dir : dir + Path.DirectorySeparatorChar;
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
throw new InvalidOperationException($"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}");
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
RawData = await File.ReadAllBytesAsync(filePath); RawData = await File.ReadAllBytesAsync(filePath);
+198 -15
View File
@@ -3,12 +3,12 @@ author: JonKazama-Hellion
punchline: Chat 2 with privacy controls aligned to EU, US and JP rules punchline: Chat 2 with privacy controls aligned to EU, US and JP rules
description: |- description: |-
Hellion Chat is built on top of Chat 2 with one removal and a stack Hellion Chat is built on top of Chat 2 with one removal and a stack
of privacy controls on top. The /chat2 command, tabs, channel of privacy controls on top. Tabs, channel filters, RGB colours,
filters, RGB colours, emotes, screenshot mode, IPC integration and emotes, screenshot mode, IPC integration and the chat replacement
the chat replacement window itself work the same. The optional window itself work the same. The optional webinterface that Chat 2
webinterface that Chat 2 ships is intentionally not part of this ships is intentionally not part of this fork because it serves a
fork because it could not be hardened to the privacy guarantees different use case from the smaller default footprint Hellion Chat
Hellion Chat makes by default. is built around.
On top of that, Hellion Chat adds privacy and data-handling controls On top of that, Hellion Chat adds privacy and data-handling controls
designed to align with the modern data protection rules that apply designed to align with the modern data protection rules that apply
@@ -40,17 +40,200 @@ tags:
- Replacement - Replacement
- Privacy - Privacy
changelog: |- changelog: |-
**Hellion Chat 0.5.1 — Backlog Sweep**
Pure hardening and polish. No new features. Eight backlog items
from the v0.5.0 codebase review collected into one patch:
- Cleanup preview now flags itself as out-of-date when the user
edits the whitelist after the last refresh, and the refresh
button is visually emphasised in that state
- Greeted Auto-Tell-Tabs now also dim their selection and hover
backgrounds in the sidebar, not just the text
- Performance section in the General tab moves to the standard
HelpMarker tooltip pattern instead of a wall-of-text description
- Tabs and Database settings tabs pull their display name from
HellionStrings instead of the upstream Language bundle, so all
eight tabs share one i18n source
- FontChooser results are now marshalled onto the framework thread
via Plugin.Framework.Run instead of being written to settings
state directly from the threadpool
- EmoteCache.LoadData drops async void and the four CS8618 build
warnings the build has been carrying since v0.4.0
- All MessageStore SQL paths that fed dynamic value lists into
interpolated SQL now use named parameter bindings via a new
BindIntList helper. Same behaviour, defence against future
user-input regressions
Configuration version is unchanged at 10. No migration. Existing
installs upgrade silently.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.5.0 — Settings UX polish**
The settings window has been pulled apart and rebuilt around eight
themed tabs instead of the twelve organic ones it grew into.
Settings now sit where they belong and the wall-of-text descriptions
have been replaced with hover help markers across every section.
What changed in this release:
- Twelve tabs collapsed into eight: General, Appearance, Window,
Chat, Tabs, Privacy, Database and Information
- Theme and font controls moved out of the Privacy tab into
Appearance where they belong
- Auto-Tell-Tabs settings, message preview and emote controls now
live under one Chat tab with collapsible sections
- About and Changelog merged into a single Information tab
- Disabled settings keep their tooltip help marker visible so you
can still read why an option is greyed out
- Section headings start collapsed by default, the same pattern
used for the Auto-Tell-Tabs preload section in 0.4.0
Configuration version bumps from 9 to 10 as a wipe migration. The
old config file is copied to HellionChat.json.pre-v10-backup before
the new defaults are written, so you can restore your previous
setup by hand if anything looks off. A one-shot notification on
first start explains the reset.
No changes to message storage, retention sweep, the privacy filter
or the export pipeline. Tabs and chat history are untouched by the
migration.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.4.0 — Auto-Tell-Tabs**
Auto-Tell-Tabs lets you turn each /tell into a session-only tab
dedicated to that conversation partner. The original use case is
the FFXIV club greeter who has to track 515 parallel "hi, welcome"
exchanges; everyone else can disable the feature in one click and
go back to a single Tell Exclusive tab.
What lands in this release:
- Auto-spawn temp tab "Name@World" on /tell (incoming and outgoing)
- Tab limit (default 15, range 150) with LRU drop that prefers
greeted tabs first, then sorts by last activity
- History preload from the local message store (default 20 tells,
range 0100) with a "— Earlier conversations —" separator above
the live tell that triggered the spawn
- Optional "mark as greeted" toggle button (off by default,
greeter-specific) that dims the tab name and lets you flip the
status
- Section header "Active Tells (n)" or compact-mode separator in
the sidebar between persistent tabs and the temp tabs
- Settings UI under Chat (toggle / limit / compact / greeted-toggle)
and Privacy (history preload count), with hover-tooltip help
markers replacing the previous wall-of-text descriptions for the
new sections
- Save and load filters strip temp tabs from the on-disk config so
a crash or a sidebar-mode toggle never persists or wipes them
Compatibility note: if XIV Messanger or another plugin is
suppressing direct messages, disable its "Suppress DMs" option so
Hellion Chat can receive tells and open the auto tabs.
Configuration version bumps from 8 to 9. Existing users get a one-
shot notification on the first start, defaults are seeded by
property initializers, persistent tabs are untouched.
The vertical sidebar tab view becomes the default for fresh
installs; existing users keep their saved preference.
Inspired by the per-sender tab pattern in XIV InstantMessenger
(Limiana, AGPL-3.0). No code was ported across the licence
boundary; only the architectural concept influenced this design.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.3.1 — Upstream emote regression fix**
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
from API 15 updates" which changes the BetterTTV emote DTOs
(Emote and Top100) from public fields to public properties.
System.Text.Json under the API 15 toolchain only honours the
[JsonPropertyName] attribute on properties, so the previous
field-based version deserialised every fetched emote into empty
default values. Result: BetterTTV emotes were silently broken
on fresh installs. The fix is six lines and applies cleanly on
top of our defensive null-check from earlier; the EmoteCache
path-traversal hardening from 0.3.0 stays as it is.
Authorship of the fix is preserved with git cherry-pick -x, so
Infi shows up as the author on the commit. Thanks to him for
catching it in the upstream codebase.
**Hellion Chat 0.3.0 — Audit hardening, brand sweep and rebrand of slash commands**
This release closes the remaining audit follow-ups from the
0.2.0 cleanup and finishes turning Hellion Chat into a properly
branded fork rather than a Chat 2 with a different name.
Slash commands have been renamed across the board so they no
longer collide with the upstream plugin and tell you which
plugin owns them at a glance:
- /chat2 becomes /hellion
- /chat2Viewer becomes /hellionView
- /clearlog2 becomes /clearhellion
- /chat2Debugger becomes /hellionDebugger (internal)
- /chat2SeString becomes /hellionSeString (internal)
This is a breaking change for anyone with macros bound to the
old command names. The upstream Chat 2 commands keep working
if you also have that plugin installed.
Privacy and storage hardening based on the post-0.2.0 audit:
- Privacy filter master switch now states explicitly that the
filter only governs storage, not the live chat log
- Emote cache refuses to write outside its own directory if a
third-party API ever returns a path that escapes
- Retention sweep is serialised so the 24h auto-sweep and the
manual button cannot launch in parallel and race for the
SQLite connection
- DbViewer paging uses an int constant and the matching SQL
parameter name (the upstream code passed a float and a name
without the parameter prefix; both worked in practice but
were inconsistent)
Visual identity now matches the Hellion Online Media website:
- Theme palette switched to Arctic Cyan plus Ember Orange,
matching the website's BRANDING.md tokens
- Active tabs and window title bars use a brand-color-dark teal
variation as identity colour, replacing the previous slate
violet that did not appear in the brand
- Resize grips and scrollbar grabs picked up Ember Orange
instead of industrial amber on hover and active states
About tab rewritten and properly localised:
- New "Why this fork exists" block sets out the mission in
neutral terms, framing Chat 2's full-history default as the
right one for most users while explaining the narrower
default footprint this fork chose
- All Hellion-specific About copy now lives in HellionStrings
in EN and DE, so German users see the Hellion sections in
German rather than the upstream English fallback
- Webinterface absence is described as a focus mismatch
(different use case, substantial rebuild) rather than as
a security issue with the upstream code
- Translator list at the bottom of the About tab is reachable
again on smaller settings windows
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.2.0 — Webinterface removed** **Hellion Chat 0.2.0 — Webinterface removed**
Following an internal security and consistency audit the upstream The upstream webinterface has been removed in its entirety. It
webinterface has been removed in its entirety. Hardening it to the serves a different use case from the smaller default footprint
privacy guarantees Hellion Chat makes by default would have meant this fork is built around, namely remote access to chat from a
rewriting the auth flow (the upstream code uses a five-digit second device. Aligning it with the data minimisation defaults
numeric code from System.Random), changing the default bind address Hellion Chat ships with would have meant a substantial rebuild.
(currently every interface), reworking cookie handling and adding Removing it was the cleaner path for this particular fork.
the privacy filter to the live message stream that the webinterface
was broadcasting around it. The cumulative cost did not match the
niche use case for a fork that wants less network surface, not more.
What changed in this release: What changed in this release:
+9
View File
@@ -50,6 +50,13 @@ internal class MessageManager : IAsyncDisposable
} }
} }
// Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed
// message has been routed to all matching persistent tabs and stored
// in the database. The AutoTellTabsService subscribes to spawn or
// refresh temp tabs without having to wedge itself into ProcessMessage
// directly.
public event Action<Message>? MessageProcessed;
internal unsafe MessageManager(Plugin plugin) internal unsafe MessageManager(Plugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
@@ -266,6 +273,8 @@ internal class MessageManager : IAsyncDisposable
if (tab.Matches(message)) if (tab.Matches(message))
tab.AddMessage(message, unread); tab.AddMessage(message, unread);
} }
MessageProcessed?.Invoke(message);
} }
internal class NameFormatting internal class NameFormatting
+150 -32
View File
@@ -239,6 +239,9 @@ internal class MessageStore : IDisposable
private bool ColumnExists(string table, string column) private bool ColumnExists(string table, string column)
{ {
// PRAGMA does not accept SQLite parameter bindings. The table name is
// a compile-time constant fed in from internal call sites, so the
// interpolation cannot be reached from any user-controlled path.
using var cmd = Connection.CreateCommand(); using var cmd = Connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({table});"; cmd.CommandText = $"PRAGMA table_info({table});";
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
@@ -298,8 +301,10 @@ internal class MessageStore : IDisposable
{ {
Plugin.Log.Information($"Setting version {version}"); Plugin.Log.Information($"Setting version {version}");
using var cmd = Connection.CreateCommand(); using var cmd = Connection.CreateCommand();
// Parameters aren't supported for PRAGMA queries, and you can't set the // PRAGMA does not accept SQLite parameter bindings, and there is no
// version with a pragma_ function. // pragma_ function variant that can set the version either. The
// version is a compile-time int from the migration sequence, never
// user input.
cmd.CommandText = $"PRAGMA user_version = {version};"; cmd.CommandText = $"PRAGMA user_version = {version};";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@@ -346,31 +351,44 @@ internal class MessageStore : IDisposable
throw new ArgumentOutOfRangeException(nameof(chatTypeDaysMap), "Negative retention is not allowed."); throw new ArgumentOutOfRangeException(nameof(chatTypeDaysMap), "Negative retention is not allowed.");
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var clauses = new List<string>();
foreach (var (type, days) in chatTypeDaysMap)
{
var cutoff = nowMs - days * 86400000L;
clauses.Add($"(ChatType = {type} AND Date < {cutoff})");
}
// Catch-all for channels without an explicit override. "0" is treated if (chatTypeDaysMap.Count == 0 && defaultDays <= 0)
// as "do not delete by default" — without an explicit user override,
// unmapped channels stay forever instead of getting wiped immediately.
if (defaultDays > 0)
{
var cutoff = nowMs - defaultDays * 86400000L;
var explicitTypes = chatTypeDaysMap.Count > 0
? string.Join(",", chatTypeDaysMap.Keys)
: "-1"; // empty list would produce invalid SQL
clauses.Add($"(ChatType NOT IN ({explicitTypes}) AND Date < {cutoff})");
}
if (clauses.Count == 0)
return 0; return 0;
long deleted; long deleted;
using (var cmd = Connection.CreateCommand()) using (var cmd = Connection.CreateCommand())
{ {
var clauses = new List<string>();
var index = 0;
foreach (var (type, days) in chatTypeDaysMap)
{
var cutoff = nowMs - days * 86400000L;
var typeParam = $"$type{index}";
var cutoffParam = $"$cutoff{index}";
cmd.Parameters.AddWithValue(typeParam, type);
cmd.Parameters.AddWithValue(cutoffParam, cutoff);
clauses.Add($"(ChatType = {typeParam} AND Date < {cutoffParam})");
index++;
}
// Catch-all for channels without an explicit override. "0" is
// treated as "do not delete by default" — without an explicit
// user override, unmapped channels stay forever instead of
// getting wiped immediately.
if (defaultDays > 0)
{
var defaultCutoff = nowMs - defaultDays * 86400000L;
cmd.Parameters.AddWithValue("$defaultCutoff", defaultCutoff);
var explicitPlaceholders = chatTypeDaysMap.Count > 0
? BindIntList(cmd, "explicit", chatTypeDaysMap.Keys)
: "-1"; // empty list would produce invalid SQL
clauses.Add($"(ChatType NOT IN ({explicitPlaceholders}) AND Date < $defaultCutoff)");
}
if (clauses.Count == 0)
return 0;
cmd.CommandText = $"DELETE FROM messages WHERE {string.Join(" OR ", clauses)};"; cmd.CommandText = $"DELETE FROM messages WHERE {string.Join(" OR ", clauses)};";
cmd.CommandTimeout = 600; cmd.CommandTimeout = 600;
deleted = cmd.ExecuteNonQuery(); deleted = cmd.ExecuteNonQuery();
@@ -395,11 +413,11 @@ internal class MessageStore : IDisposable
throw new InvalidOperationException("CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."); throw new InvalidOperationException("CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe.");
} }
var inList = string.Join(",", allowedTypes);
long deleted; long deleted;
using (var cmd = Connection.CreateCommand()) using (var cmd = Connection.CreateCommand())
{ {
cmd.CommandText = $"DELETE FROM messages WHERE ChatType NOT IN ({inList});"; var placeholders = BindIntList(cmd, "ct", allowedTypes);
cmd.CommandText = $"DELETE FROM messages WHERE ChatType NOT IN ({placeholders});";
cmd.CommandTimeout = 600; cmd.CommandTimeout = 600;
deleted = cmd.ExecuteNonQuery(); deleted = cmd.ExecuteNonQuery();
} }
@@ -512,15 +530,16 @@ internal class MessageStore : IDisposable
DateTimeOffset? from, DateTimeOffset? from,
DateTimeOffset? to) DateTimeOffset? to)
{ {
var cmd = Connection.CreateCommand();
var clauses = new List<string> { "deleted = false" }; var clauses = new List<string> { "deleted = false" };
if (chatTypes is { Count: > 0 }) if (chatTypes is { Count: > 0 })
clauses.Add($"ChatType IN ({string.Join(",", chatTypes)})"); clauses.Add($"ChatType IN ({BindIntList(cmd, "exct", chatTypes)})");
if (from is not null) if (from is not null)
clauses.Add("Date >= $From"); clauses.Add("Date >= $From");
if (to is not null) if (to is not null)
clauses.Add("Date <= $To"); clauses.Add("Date <= $To");
var cmd = Connection.CreateCommand();
cmd.CommandText = @" cmd.CommandText = @"
SELECT SELECT
Id, Id,
@@ -602,6 +621,84 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader()); return new MessageEnumerator(cmd.ExecuteReader());
} }
/// <summary>
/// Hellion Chat — Auto-Tell-Tabs history preload.
///
/// Returns up to <paramref name="limit"/> tells exchanged with the named
/// player, oldest-first, ready to be added to a freshly spawned auto
/// tell tab. The Sender column is a serialized chunk blob, so SQL on its
/// own cannot filter by player identity; we narrow with SQL on Receiver
/// + ChatType (cheap, indexed) and let the client do the final
/// PlayerPayload comparison on the result set.
///
/// <paramref name="sqlScanLimit"/> caps how many recent tells we scan
/// before giving up. 500 covers around 10 days for an active greeter
/// and stays well under the 20 ms budget required to keep the spawn on
/// the message-processing worker thread.
/// </summary>
internal IReadOnlyList<Message> GetTellHistoryWithSender(
ulong receiver,
string senderName,
uint senderWorld,
int limit,
int sqlScanLimit = 500)
{
if (limit <= 0)
{
return [];
}
using var cmd = Connection.CreateCommand();
cmd.CommandText = @"
SELECT
Id,
Receiver,
ContentId,
Date,
ChatType,
SourceKind,
TargetKind,
Sender,
Content,
SenderSource,
ContentSource,
ExtraChatChannel
FROM messages
WHERE deleted = false
AND Receiver = $Receiver
AND ChatType IN ($TellIncoming, $TellOutgoing)
ORDER BY Date DESC
LIMIT $ScanLimit;
";
cmd.CommandTimeout = 60;
cmd.Parameters.AddWithValue("$Receiver", receiver);
cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming);
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
var collected = new List<Message>();
using var enumerator = new MessageEnumerator(cmd.ExecuteReader());
foreach (var message in enumerator)
{
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
{
continue;
}
collected.Add(message);
if (collected.Count >= limit)
{
break;
}
}
// SQL was DESC (newest-first) so we hit the limit on the most
// recent matching tells. Reverse to oldest-first for chronological
// display in the tab.
collected.Reverse();
return collected;
}
/// <summary> /// <summary>
/// Marks a message as deleted so it won't get returned in queries. /// Marks a message as deleted so it won't get returned in queries.
/// </summary> /// </summary>
@@ -615,16 +712,17 @@ internal class MessageStore : IDisposable
internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null) internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
{ {
using var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"]; List<string> whereClauses = ["deleted = false"];
if (receiver != null) if (receiver != null)
whereClauses.Add("Receiver = $Receiver"); whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before"); whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})"); whereClauses.Add($"ChatType IN ({BindIntList(cmd, "cdr", channels.Select(c => (int)c))})");
var whereClause = "WHERE " + string.Join(" AND ", whereClauses); var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
using var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get // Select last N messages by date DESC, but reverse the order to get
// them in ascending order. // them in ascending order.
cmd.CommandText = @" cmd.CommandText = @"
@@ -644,16 +742,17 @@ internal class MessageStore : IDisposable
internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null) internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
{ {
var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"]; List<string> whereClauses = ["deleted = false"];
if (receiver != null) if (receiver != null)
whereClauses.Add("Receiver = $Receiver"); whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before"); whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})"); whereClauses.Add($"ChatType IN ({BindIntList(cmd, "gdr", channels.Select(c => (int)c))})");
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}"; var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get // Select last N messages by date DESC, but reverse the order to get
// them in ascending order. // them in ascending order.
cmd.CommandText = @" cmd.CommandText = @"
@@ -685,16 +784,17 @@ internal class MessageStore : IDisposable
internal MessageEnumerator GetPagedDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null, int page = 0) internal MessageEnumerator GetPagedDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null, int page = 0)
{ {
var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"]; List<string> whereClauses = ["deleted = false"];
if (receiver != null) if (receiver != null)
whereClauses.Add("Receiver = $Receiver"); whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before"); whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})"); whereClauses.Add($"ChatType IN ({BindIntList(cmd, "pdr", channels.Select(c => (int)c))})");
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}"; var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get // Select last N messages by date DESC, but reverse the order to get
// them in ascending order. // them in ascending order.
cmd.CommandText = @" cmd.CommandText = @"
@@ -724,10 +824,28 @@ internal class MessageStore : IDisposable
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds()); cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds()); cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page); cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
cmd.Parameters.AddWithValue("OffsetCount", DbViewer.RowPerPage); cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
return new MessageEnumerator(cmd.ExecuteReader()); return new MessageEnumerator(cmd.ExecuteReader());
} }
// Build "$prefix0,$prefix1,..." placeholder list and bind values to
// the command. SQLite has no native array parameter, so we generate
// the list at runtime and bind each entry under its own name. Used
// for IN-clauses and similar dynamic-arity SQL fragments.
private static string BindIntList(SqliteCommand cmd, string prefix, IEnumerable<int> values)
{
var names = new List<string>();
var index = 0;
foreach (var value in values)
{
var name = $"${prefix}{index}";
cmd.Parameters.AddWithValue(name, value);
names.Add(name);
index++;
}
return string.Join(",", names);
}
} }
internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, IDisposable, IAsyncDisposable internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, IDisposable, IAsyncDisposable
+99 -53
View File
@@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO;
using ChatTwo.Ipc; using ChatTwo.Ipc;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Ui; using ChatTwo.Ui;
@@ -57,6 +58,7 @@ public sealed class Plugin : IDalamudPlugin
internal Commands Commands { get; } internal Commands Commands { get; }
internal GameFunctions.GameFunctions Functions { get; } internal GameFunctions.GameFunctions Functions { get; }
internal MessageManager MessageManager { get; } internal MessageManager MessageManager { get; }
internal AutoTellTabsService AutoTellTabsService { get; }
internal IpcManager Ipc { get; } internal IpcManager Ipc { get; }
internal ExtraChat ExtraChat { get; } internal ExtraChat ExtraChat { get; }
internal TypingIpc TypingIpc { get; } internal TypingIpc TypingIpc { get; }
@@ -64,6 +66,18 @@ public sealed class Plugin : IDalamudPlugin
internal int DeferredSaveFrames = -1; internal int DeferredSaveFrames = -1;
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
// the manual button in the Privacy tab both run on background threads;
// without this gate, hitting the manual button moments after a fresh
// plugin start would launch two sweeps in parallel and the second one
// would just re-do work the first one already finished. The lock guards
// the flag — the flag check itself bails before we touch the database.
// Volatile because the ImGui thread reads the flag outside the lock to
// gate the manual button; without it the JIT may cache the value in a
// register and miss the background-thread update.
internal readonly object RetentionSweepLock = new();
internal volatile bool RetentionSweepRunning;
internal DateTime GameStarted { get; } internal DateTime GameStarted { get; }
// Tab management needs to happen outside the chatlog window class for access reasons // Tab management needs to happen outside the chatlog window class for access reasons
@@ -91,75 +105,70 @@ public sealed class Plugin : IDalamudPlugin
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
#pragma warning disable CS0618 // Type or member is obsolete // Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig
// TODO Remove after 01.07.2026 // already strips temp tabs before persistence, but a previous
// Migrate old channel values // crash or external write could have left them in the JSON.
if (Config.Version <= 5) // Drop them on load to guarantee the session-only invariant.
Config.Tabs.RemoveAll(t => t.IsTempTab);
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
// layout starts from defaults instead of mapping every previous setting
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
if (Config.Version < 10)
{ {
foreach (var tab in Config.Tabs) var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
if (pluginConfigsDir is not null)
{ {
if (tab.ChatCodes.Count > 0) var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
{ var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
tab.SelectedChannels = tab.ChatCodes.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
tab.ChatCodes.Clear();
}
if (Config.InactivityHideChannels.Count > 0) try
{ {
Config.InactivityHideChannelsV2 = Config.InactivityHideChannels.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value)); if (File.Exists(liveConfigPath))
Config.InactivityHideChannels.Clear(); {
File.Copy(liveConfigPath, backupPath, overwrite: true);
}
}
catch (Exception ex)
{
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
} }
Config.Version = 6;
SaveConfig();
} }
}
#pragma warning restore CS0618 // Type or member is obsolete
// Hellion Chat v6→v7: seed Privacy-First defaults. Config = new Configuration
if (Config.Version <= 6) {
{ Version = 10,
Config.PrivacyFilterEnabled = true; FirstRunCompleted = true,
Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist]; };
Config.PrivacyPersistUnknownChannels = false;
// Existing ChatTwo users skip the first-run wizard — the
// migration toast already explains what changed and they
// can reopen the wizard from Settings → Privacy if they
// want to pick a different profile.
Config.FirstRunCompleted = true;
Config.Version = 7;
SaveConfig(); SaveConfig();
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{ {
Title = HellionStrings.Migration_Notification_Title, Title = HellionStrings.SettingsRefactor_Migration_Title,
Content = HellionStrings.Migration_Notification_Content, Content = HellionStrings.SettingsRefactor_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(15), InitialDuration = TimeSpan.FromSeconds(25),
});
}
// Hellion Chat v7→v8: webinterface removed in 0.2.0. Old config
// entries (WebinterfacePassword, AuthStore, etc.) get dropped on
// the next save because their properties no longer exist on the
// Configuration class. The bump is recorded so the notification
// only fires once.
if (Config.Version <= 7)
{
Config.Version = 8;
SaveConfig();
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = HellionStrings.Migration_Webinterface_Removed_Title,
Content = HellionStrings.Migration_Webinterface_Removed_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(20),
}); });
} }
// Hellion default tab layout for first-run and v10-wipe.
// General catches player chat plus active gameplay events; the
// System tab takes the technical noise so it does not bury real
// conversation. Beginner tab only appears when the Novice
// Network is enabled in Audio and Notifications, otherwise it
// would just sit empty.
if (Config.Tabs.Count == 0) if (Config.Tabs.Count == 0)
{
Config.Tabs.Add(TabsUtil.VanillaGeneral); Config.Tabs.Add(TabsUtil.VanillaGeneral);
Config.Tabs.Add(TabsUtil.HellionSystem);
Config.Tabs.Add(TabsUtil.HellionFreeCompany);
Config.Tabs.Add(TabsUtil.HellionParty);
if (Config.ShowNoviceNetwork)
Config.Tabs.Add(TabsUtil.HellionBeginner);
Config.Tabs.Add(TabsUtil.HellionLinkshell);
Config.Tabs.Add(TabsUtil.VanillaTellExclusive);
}
LanguageChanged(Interface.UiLanguage); LanguageChanged(Interface.UiLanguage);
ImGuiUtil.Initialize(this); ImGuiUtil.Initialize(this);
@@ -175,6 +184,14 @@ public sealed class Plugin : IDalamudPlugin
MessageManager = new MessageManager(this); // Does it require UI? MessageManager = new MessageManager(this); // Does it require UI?
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
// MessageManager's MessageProcessed event for live tells and
// to ClientState.Logout for the cleanup pass. Created after
// MessageManager so the constructor can hand off the live
// store and event source.
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
AutoTellTabsService.Initialize();
// Hellion Chat — daily retention sweep, off-thread so it never // Hellion Chat — daily retention sweep, off-thread so it never
// blocks plugin load. Skips itself when disabled or already ran // blocks plugin load. Skips itself when disabled or already ran
// within the past 24 hours. // within the past 24 hours.
@@ -224,7 +241,7 @@ public sealed class Plugin : IDalamudPlugin
Interface.UiBuilder.OpenMainUi += OpenMainUi; Interface.UiBuilder.OpenMainUi += OpenMainUi;
if (Config.ShowEmotes) if (Config.ShowEmotes)
Task.Run(EmoteCache.LoadData); _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
#if !DEBUG #if !DEBUG
// Avoid 300ms hitch when sending first message by preloading the // Avoid 300ms hitch when sending first message by preloading the
@@ -265,6 +282,10 @@ public sealed class Plugin : IDalamudPlugin
TypingIpc?.Dispose(); TypingIpc?.Dispose();
ExtraChat?.Dispose(); ExtraChat?.Dispose();
Ipc?.Dispose(); Ipc?.Dispose();
// Dispose the Auto-Tell-Tabs service before MessageManager so it
// can cleanly unsubscribe from the MessageProcessed event before
// its source goes away.
AutoTellTabsService?.Dispose();
MessageManager?.DisposeAsync().AsTask().Wait(); MessageManager?.DisposeAsync().AsTask().Wait();
Functions?.Dispose(); Functions?.Dispose();
Commands?.Dispose(); Commands?.Dispose();
@@ -405,6 +426,16 @@ public sealed class Plugin : IDalamudPlugin
new Thread(() => new Thread(() =>
{ {
// Bail out cheaply if a manual sweep is already in flight; the
// lock around the actual work would queue us up otherwise and
// we would just re-do whatever the manual run already did.
lock (RetentionSweepLock)
{
if (RetentionSweepRunning)
return;
RetentionSweepRunning = true;
}
try try
{ {
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays); var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
@@ -429,6 +460,11 @@ public sealed class Plugin : IDalamudPlugin
{ {
Log.Error(e, "Retention sweep failed"); Log.Error(e, "Retention sweep failed");
} }
finally
{
lock (RetentionSweepLock)
RetentionSweepRunning = false;
}
}) { IsBackground = true }.Start(); }) { IsBackground = true }.Start();
} }
@@ -467,7 +503,17 @@ public sealed class Plugin : IDalamudPlugin
internal void SaveConfig() internal void SaveConfig()
{ {
// Hellion Chat — Auto-Tell-Tabs are session-only. Strip them out
// before serialization so a crash mid-session can never persist
// them. We snapshot the full tab list first and restore it after
// the save, preserving the user's order and open conversations.
var snapshot = Config.Tabs.ToList();
Config.Tabs.RemoveAll(t => t.IsTempTab);
Interface.SavePluginConfig(Config); Interface.SavePluginConfig(Config);
Config.Tabs.Clear();
Config.Tabs.AddRange(snapshot);
} }
internal void LanguageChanged(string langCode) internal void LanguageChanged(string langCode)
+108 -6
View File
@@ -44,6 +44,8 @@ internal class HellionStrings
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title)); internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name)); internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description)); internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
internal static string Privacy_FilterEnabled_StorageOnly_Help => Get(nameof(Privacy_FilterEnabled_StorageOnly_Help));
internal static string Privacy_Filter_Tree_Heading => Get(nameof(Privacy_Filter_Tree_Heading));
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help)); internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst)); internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll)); internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
@@ -62,6 +64,8 @@ internal class HellionStrings
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading)); internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro)); internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro));
internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote)); internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote));
internal static string Cleanup_Preview_Stale => Get(nameof(Cleanup_Preview_Stale));
internal static string Retention_Help_SavedNote => Get(nameof(Retention_Help_SavedNote));
internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview)); internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview));
internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview)); internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview));
internal static string Cleanup_TotalStored => Get(nameof(Cleanup_TotalStored)); internal static string Cleanup_TotalStored => Get(nameof(Cleanup_TotalStored));
@@ -97,11 +101,6 @@ internal class HellionStrings
internal static string Retention_Success => Get(nameof(Retention_Success)); internal static string Retention_Success => Get(nameof(Retention_Success));
internal static string Retention_Error => Get(nameof(Retention_Error)); internal static string Retention_Error => Get(nameof(Retention_Error));
internal static string Migration_Notification_Title => Get(nameof(Migration_Notification_Title));
internal static string Migration_Notification_Content => Get(nameof(Migration_Notification_Content));
internal static string Migration_Webinterface_Removed_Title => Get(nameof(Migration_Webinterface_Removed_Title));
internal static string Migration_Webinterface_Removed_Content => Get(nameof(Migration_Webinterface_Removed_Content));
internal static string Wizard_Title => Get(nameof(Wizard_Title)); internal static string Wizard_Title => Get(nameof(Wizard_Title));
internal static string Wizard_Intro => Get(nameof(Wizard_Intro)); internal static string Wizard_Intro => Get(nameof(Wizard_Intro));
internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading)); internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading));
@@ -133,11 +132,114 @@ internal class HellionStrings
internal static string Export_Empty => Get(nameof(Export_Empty)); internal static string Export_Empty => Get(nameof(Export_Empty));
internal static string Export_Error => Get(nameof(Export_Error)); internal static string Export_Error => Get(nameof(Export_Error));
internal static string Theme_Heading => Get(nameof(Theme_Heading));
internal static string Theme_Enabled_Name => Get(nameof(Theme_Enabled_Name)); internal static string Theme_Enabled_Name => Get(nameof(Theme_Enabled_Name));
internal static string Theme_Enabled_Description => Get(nameof(Theme_Enabled_Description)); internal static string Theme_Enabled_Description => Get(nameof(Theme_Enabled_Description));
internal static string Theme_WindowOpacity_Label => Get(nameof(Theme_WindowOpacity_Label)); internal static string Theme_WindowOpacity_Label => Get(nameof(Theme_WindowOpacity_Label));
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help)); internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name)); internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description)); internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
internal static string About_Maintainer_Heading => Get(nameof(About_Maintainer_Heading));
internal static string About_Maintainer_Body => Get(nameof(About_Maintainer_Body));
internal static string About_Maintainer_Website_Label => Get(nameof(About_Maintainer_Website_Label));
internal static string About_Mission_Heading => Get(nameof(About_Mission_Heading));
internal static string About_Mission_P1 => Get(nameof(About_Mission_P1));
internal static string About_Mission_P2 => Get(nameof(About_Mission_P2));
internal static string About_Mission_P3 => Get(nameof(About_Mission_P3));
internal static string About_BuiltOn_Heading => Get(nameof(About_BuiltOn_Heading));
internal static string About_BuiltOn_P1 => Get(nameof(About_BuiltOn_P1));
internal static string About_BuiltOn_P2 => Get(nameof(About_BuiltOn_P2));
internal static string About_BuiltOn_Upstream_Label => Get(nameof(About_BuiltOn_Upstream_Label));
internal static string About_License_Heading => Get(nameof(About_License_Heading));
internal static string About_License_P1 => Get(nameof(About_License_P1));
internal static string About_License_P2 => Get(nameof(About_License_P2));
internal static string About_License_P3 => Get(nameof(About_License_P3));
internal static string About_SE_Heading => Get(nameof(About_SE_Heading));
internal static string About_SE_P1 => Get(nameof(About_SE_P1));
internal static string About_SE_P2 => Get(nameof(About_SE_P2));
internal static string About_Localization_Heading => Get(nameof(About_Localization_Heading));
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
internal static string About_Localization_P2 => Get(nameof(About_Localization_P2));
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
// Hellion Chat — Auto-Tell-Tabs runtime strings
internal static string AutoTellTabs_SectionHeader => Get(nameof(AutoTellTabs_SectionHeader));
internal static string AutoTellTabs_HistorySeparator => Get(nameof(AutoTellTabs_HistorySeparator));
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
internal static string ChatLog_AutoTellTabs_Enable_Name => Get(nameof(ChatLog_AutoTellTabs_Enable_Name));
internal static string ChatLog_AutoTellTabs_Enable_Description => Get(nameof(ChatLog_AutoTellTabs_Enable_Description));
internal static string ChatLog_AutoTellTabs_Limit_Name => Get(nameof(ChatLog_AutoTellTabs_Limit_Name));
internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description));
internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name));
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description));
internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
// Hellion Chat — Auto-Tell-Tabs Privacy settings tab
internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title));
internal static string Privacy_AutoTellTabs_Preload_Name => Get(nameof(Privacy_AutoTellTabs_Preload_Name));
internal static string Privacy_AutoTellTabs_Preload_Description => Get(nameof(Privacy_AutoTellTabs_Preload_Description));
internal static string Privacy_AutoTellTabs_Preload_Hint => Get(nameof(Privacy_AutoTellTabs_Preload_Hint));
// Hellion Chat — Settings UX Polish v10 wipe migration
internal static string SettingsRefactor_Migration_Title => Get(nameof(SettingsRefactor_Migration_Title));
internal static string SettingsRefactor_Migration_Content => Get(nameof(SettingsRefactor_Migration_Content));
// Hellion Chat — Settings UX Polish 8-tab structure
internal static string Settings_Tab_General => Get(nameof(Settings_Tab_General));
internal static string Settings_Tab_Appearance => Get(nameof(Settings_Tab_Appearance));
internal static string Settings_Tab_Window => Get(nameof(Settings_Tab_Window));
internal static string Settings_Tab_Chat => Get(nameof(Settings_Tab_Chat));
internal static string Settings_Tab_Tabs => Get(nameof(Settings_Tab_Tabs));
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
// Hellion Chat — General-Tab section headings
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
internal static string Settings_General_Performance_Heading => Get(nameof(Settings_General_Performance_Heading));
internal static string Settings_General_Language_Heading => Get(nameof(Settings_General_Language_Heading));
// Hellion Chat — Appearance-Tab section headings
internal static string Settings_Appearance_Theme_Heading => Get(nameof(Settings_Appearance_Theme_Heading));
internal static string Settings_Appearance_Fonts_Heading => Get(nameof(Settings_Appearance_Fonts_Heading));
internal static string Settings_Appearance_Colours_Heading => Get(nameof(Settings_Appearance_Colours_Heading));
internal static string Settings_Appearance_Timestamps_Heading => Get(nameof(Settings_Appearance_Timestamps_Heading));
// Hellion Chat — Window-Tab section headings
internal static string Settings_Window_Hide_Heading => Get(nameof(Settings_Window_Hide_Heading));
internal static string Settings_Window_InactivityHide_Heading => Get(nameof(Settings_Window_InactivityHide_Heading));
internal static string Settings_Window_Frame_Heading => Get(nameof(Settings_Window_Frame_Heading));
internal static string Settings_Window_Tooltips_Heading => Get(nameof(Settings_Window_Tooltips_Heading));
// Hellion Chat — Chat-Tab section headings
internal static string Settings_Chat_AutoTellTabs_Heading => Get(nameof(Settings_Chat_AutoTellTabs_Heading));
internal static string Settings_Chat_Behaviour_Heading => Get(nameof(Settings_Chat_Behaviour_Heading));
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
// Hellion Chat — Database-Tab section headings
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
internal static string Settings_Database_Stats_Heading => Get(nameof(Settings_Database_Stats_Heading));
// Hellion Chat — Information-Tab section headings
internal static string Settings_Information_VersionInfo_Heading => Get(nameof(Settings_Information_VersionInfo_Heading));
internal static string Settings_Information_About_Heading => Get(nameof(Settings_Information_About_Heading));
internal static string Settings_Information_Changelog_Heading => Get(nameof(Settings_Information_Changelog_Heading));
// Hellion Chat — Default tab presets (channel-themed)
internal static string Tabs_Presets_System => Get(nameof(Tabs_Presets_System));
internal static string Tabs_Presets_FreeCompany => Get(nameof(Tabs_Presets_FreeCompany));
internal static string Tabs_Presets_Party => Get(nameof(Tabs_Presets_Party));
internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner));
internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell));
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
} }
+280 -16
View File
@@ -21,6 +21,12 @@
<data name="Privacy_FilterEnabled_Description" xml:space="preserve"> <data name="Privacy_FilterEnabled_Description" xml:space="preserve">
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value> <value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value>
</data> </data>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
<value>Der Filter steuert nur, was in die lokale Datenbank geschrieben wird. Im Chat-Log siehst du weiterhin jede Nachricht live, ausgeschlossene Kanäle werden nur nicht mehr gespeichert. Wenn du Kanäle auch aus der sichtbaren Anzeige entfernen willst, nutze die normalen Chat-Tab-Filter im Spiel.</value>
</data>
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
<value>Privacy-Filter und Whitelist</value>
</data>
<data name="Privacy_Whitelist_Help" xml:space="preserve"> <data name="Privacy_Whitelist_Help" xml:space="preserve">
<value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value> <value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value>
</data> </data>
@@ -72,6 +78,12 @@
<data name="Cleanup_Help_SavedNote" xml:space="preserve"> <data name="Cleanup_Help_SavedNote" xml:space="preserve">
<value>Das Aufräumen nutzt deine GESPEICHERTE Whitelist (Plugin.Config), nicht ungespeicherte Änderungen oben. Klicke zuerst Speichern, wenn du deine aktuellen Änderungen anwenden willst.</value> <value>Das Aufräumen nutzt deine GESPEICHERTE Whitelist (Plugin.Config), nicht ungespeicherte Änderungen oben. Klicke zuerst Speichern, wenn du deine aktuellen Änderungen anwenden willst.</value>
</data> </data>
<data name="Retention_Help_SavedNote" xml:space="preserve">
<value>Der manuelle Lauf nutzt deine GESPEICHERTE Retention-Policy, nicht die Slider-Werte oben. Klicke zuerst Speichern, wenn der Lauf deine aktuellen Änderungen anwenden soll.</value>
</data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Vorschau veraltet, deine Whitelist hat sich seit dem letzten Aktualisieren geändert. Klicke Aktualisieren, um neu zu berechnen.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve"> <data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Vorschau aktualisieren</value> <value>Vorschau aktualisieren</value>
</data> </data>
@@ -171,18 +183,6 @@
<data name="Retention_Error" xml:space="preserve"> <data name="Retention_Error" xml:space="preserve">
<value>Aufbewahrungs-Bereinigung fehlgeschlagen, siehe /xllog</value> <value>Aufbewahrungs-Bereinigung fehlgeschlagen, siehe /xllog</value>
</data> </data>
<data name="Migration_Notification_Title" xml:space="preserve">
<value>Hellion Chat</value>
</data>
<data name="Migration_Notification_Content" xml:space="preserve">
<value>Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen.</value>
</data>
<data name="Migration_Webinterface_Removed_Title" xml:space="preserve">
<value>Hellion Chat 0.2.0</value>
</data>
<data name="Migration_Webinterface_Removed_Content" xml:space="preserve">
<value>Das Webinterface wurde in dieser Version entfernt, weil es nicht auf das Datenschutz-Niveau gehärtet werden konnte das Hellion Chat standardmäßig zusichert. Falls du es genutzt hast, schau bitte in die README für Hintergründe.</value>
</data>
<data name="Wizard_Title" xml:space="preserve"> <data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Willkommen</value> <value>Hellion Chat — Willkommen</value>
</data> </data>
@@ -270,14 +270,11 @@
<data name="Export_Error" xml:space="preserve"> <data name="Export_Error" xml:space="preserve">
<value>Export fehlgeschlagen, siehe /xllog</value> <value>Export fehlgeschlagen, siehe /xllog</value>
</data> </data>
<data name="Theme_Heading" xml:space="preserve">
<value>Erscheinungsbild</value>
</data>
<data name="Theme_Enabled_Name" xml:space="preserve"> <data name="Theme_Enabled_Name" xml:space="preserve">
<value>Hellion-Theme für alle Plugin-Fenster verwenden</value> <value>Hellion-Theme für alle Plugin-Fenster verwenden</value>
</data> </data>
<data name="Theme_Enabled_Description" xml:space="preserve"> <data name="Theme_Enabled_Description" xml:space="preserve">
<value>Industrielle HUD-Palette mit cyan-blauen Aktionsfarben, schiefer-violetten Tabs und Bernstein-Akzenten für aktive Zustände, global angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value> <value>Hellion-Online-Media-Palette aus Arctic Cyan und Ember Orange, angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value>
</data> </data>
<data name="Theme_WindowOpacity_Label" xml:space="preserve"> <data name="Theme_WindowOpacity_Label" xml:space="preserve">
<value>Fenster-Deckkraft</value> <value>Fenster-Deckkraft</value>
@@ -291,4 +288,271 @@
<data name="Theme_UseHellionFont_Description" xml:space="preserve"> <data name="Theme_UseHellionFont_Description" xml:space="preserve">
<value>Rendert Chat und UI in Exo 2 (SIL Open Font License 1.1), die mit dem Plugin ausgeliefert wird. Deaktivieren, um auf die unter Einstellungen → Schrift gewählte Schriftart zurückzufallen.</value> <value>Rendert Chat und UI in Exo 2 (SIL Open Font License 1.1), die mit dem Plugin ausgeliefert wird. Deaktivieren, um auf die unter Einstellungen → Schrift gewählte Schriftart zurückzufallen.</value>
</data> </data>
<data name="About_Maintainer_Heading" xml:space="preserve">
<value>Maintainer</value>
</data>
<data name="About_Maintainer_Body" xml:space="preserve">
<value>Ich pflege Hellion Chat über Hellion Online Media. Auf der Website findest du die Kontaktdaten für lizenzrechtliche, rechtliche oder geschäftliche Fragen.</value>
</data>
<data name="About_Maintainer_Website_Label" xml:space="preserve">
<value>Website:</value>
</data>
<data name="About_Mission_Heading" xml:space="preserve">
<value>Warum es diesen Fork gibt</value>
</data>
<data name="About_Mission_P1" xml:space="preserve">
<value>Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie, die für Filter, Suche und Replay zur Verfügung steht. Dieser Default ist für die meisten Nutzer der richtige. Dieser Fork wählt einen anderen Ansatz: einen kleineren Default-Footprint, mit zusätzlichen Stellschrauben für Nutzer, die weniger fremden Chat auf der Festplatte behalten möchten.</value>
</data>
<data name="About_Mission_P2" xml:space="preserve">
<value>Der Wunsch nach diesem engeren Default war persönlich. Nach zwei Jahren mit Chat 2 lag meine Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von Fremden in Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie gerne. Mein eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut.</value>
</data>
<data name="About_Mission_P3" xml:space="preserve">
<value>Ich strebe keine große Zielgruppe an, und der Fork steht nicht in Konkurrenz zu Chat 2. Der Code liegt offen unter derselben EUPL-1.2-Lizenz wie das Original. Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder das Projekt einfach ignorieren. Alles drei ist für mich in Ordnung.</value>
</data>
<data name="About_BuiltOn_Heading" xml:space="preserve">
<value>Aufbauend auf Chat 2</value>
</data>
<data name="About_BuiltOn_P1" xml:space="preserve">
<value>Hellion Chat ist ein Fork von Chat 2 von Infi und Anna (ascclemens). Das Chat-Replacement-Fenster, die IPC-Integration, die Render-Engine und der komplette Storage-Kern stammen aus dem Original.</value>
</data>
<data name="About_BuiltOn_P2" xml:space="preserve">
<value>Das Webinterface ist das einzige größere Teil, das ich entfernt habe. Es ist für den Remote-Zugriff auf den Chat von einem zweiten Gerät gebaut, also für einen anderen Fokus als der kleinere Default-Footprint, den dieser Fork verfolgt. Es an diese Defaults anzupassen hätte einen erheblichen Umbau bedeutet, also war die Entfernung der saubere Weg für genau diesen Fork.</value>
</data>
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
<value>Upstream-Repository:</value>
</data>
<data name="About_License_Heading" xml:space="preserve">
<value>Lizenz</value>
</data>
<data name="About_License_P1" xml:space="preserve">
<value>Hellion Chat und Chat 2 stehen beide unter der European Union Public Licence v1.2 (EUPL-1.2).</value>
</data>
<data name="About_License_P2" xml:space="preserve">
<value>© 2023 bis 2026, die Chat-2-Autoren (Infi, Anna und die Upstream-Mitwirkenden).</value>
</data>
<data name="About_License_P3" xml:space="preserve">
<value>© 2026 Hellion Online Media für die Erweiterungen in diesem Fork.</value>
</data>
<data name="About_SE_Heading" xml:space="preserve">
<value>FINAL FANTASY XIV-Hinweis</value>
</data>
<data name="About_SE_P1" xml:space="preserve">
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. Alle Rechte vorbehalten.</value>
</data>
<data name="About_SE_P2" xml:space="preserve">
<value>Hellion Chat ist ein inoffizielles Fan-Plugin. Es steht in keiner Verbindung zu Square Enix und wird von ihnen weder unterstützt, gesponsert noch genehmigt.</value>
</data>
<data name="About_Localization_Heading" xml:space="preserve">
<value>Lokalisierung</value>
</data>
<data name="About_Localization_P1" xml:space="preserve">
<value>Die deutschen Übersetzungen der Hellion-spezifischen Strings stammen von mir. Weitere Sprachen sind aktuell nicht verfügbar.</value>
</data>
<data name="About_Localization_P2" xml:space="preserve">
<value>Die Übersetzerliste weiter unten gehört zu den Chat-2-Strings auf Crowdin. Diese Freiwilligen haben Chat 2 übersetzt, nicht die Hellion-Erweiterungen.</value>
</data>
<data name="About_Translators_TreeNode" xml:space="preserve">
<value>Chat-2-Community-Übersetzer (Upstream)</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Runtime-Strings) -->
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Aktive Tells</value>
</data>
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
<value>— Frühere Unterhaltungen —</value>
</data>
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
<value>Verlauf konnte nicht geladen werden.</value>
</data>
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
<value>Als begrüßt markiert. Klicken um die Markierung zu entfernen.</value>
</data>
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
<value>Als begrüßt markieren.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
<value>Bei jedem /tell automatisch einen Tab pro Gesprächspartner öffnen</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
<value>Sobald du einen /tell empfängst oder sendest, wird automatisch ein temporärer Tab für diesen Spieler geöffnet. Die Tabs verschwinden beim Logout.</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
<value>Kompakte Anzeige</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
<value>Zeigt nur einen dünnen Separator zwischen normalen Tabs und Auto-Tell-Tabs, ohne Sektions-Header.</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
<value>„Als begrüßt markieren"-Button anzeigen</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>Fügt neben jedem Auto-Tell-Tab einen Klick-Button hinzu, um einen Gesprächspartner als bereits begrüßt zu markieren — der Tab-Name wird dann gedimmt. Nützlich für Club-Greeter, die parallel viele Konversationen führen. Standardmäßig aus.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
</data>
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
<value>Hinweis: Falls XIV Messanger oder ein ähnliches Plugin Tells unterdrückt, dort die Option „Suppress DMs" deaktivieren, damit Hellion Chat Tells empfangen und die Auto-Tabs öffnen kann.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Datenschutz-Einstellungstab) -->
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Tell-Verlauf in Auto-Tabs</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
<value>Anzahl der vorgeladenen Tells</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
<value>Wie viele frühere Tell-Nachrichten beim Öffnen eines Auto-Tell-Tabs aus der Datenbank geladen werden. 0 deaktiviert die Vorladung.</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Greift nur, wenn Auto-Tell-Tabs im Chat-Tab aktiviert sind.</value>
</data>
<!-- Hellion Chat — Settings UX Polish v10 Wipe-Migration -->
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
<value>Settings umstrukturiert</value>
</data>
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
<value>Hellion Chat 0.5.0 hat die Settings in thematische Tabs umstrukturiert. Deine Chat-Datenbank und dein Nachrichtenverlauf bleiben unverändert. Settings wurden auf Defaults zurückgesetzt. Falls du das Privacy-Profil neu wählen willst, findest du den Reopen-Button im Datenschutz-Tab. Ein Backup der vorherigen Config liegt unter HellionChat.json.pre-v10-backup neben der aktiven Config-Datei.</value>
</data>
<!-- Hellion Chat — Settings UX Polish 8-Tab-Struktur -->
<data name="Settings_Tab_General" xml:space="preserve">
<value>Allgemein</value>
</data>
<data name="Settings_Tab_Appearance" xml:space="preserve">
<value>Aussehen</value>
</data>
<data name="Settings_Tab_Window" xml:space="preserve">
<value>Fenster</value>
</data>
<data name="Settings_Tab_Chat" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Tab_Tabs" xml:space="preserve">
<value>Kanäle</value>
</data>
<data name="Settings_Tab_Database" xml:space="preserve">
<value>Datenbank</value>
</data>
<data name="Settings_Tab_Information" xml:space="preserve">
<value>Über</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Allgemein-Tabs -->
<data name="Settings_General_Input_Heading" xml:space="preserve">
<value>Eingabe</value>
</data>
<data name="Settings_General_Audio_Heading" xml:space="preserve">
<value>Audio &amp; Benachrichtigungen</value>
</data>
<data name="Settings_General_Performance_Heading" xml:space="preserve">
<value>Performance</value>
</data>
<data name="Settings_General_Language_Heading" xml:space="preserve">
<value>Sprache &amp; Eingabe-Hilfen</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Aussehen-Tabs -->
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
<value>Schriftarten</value>
</data>
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
<value>Chat-Farben</value>
</data>
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
<value>Zeitstempel</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Fenster-Tabs -->
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
<value>Verstecken</value>
</data>
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
<value>Inaktivitäts-Verstecken</value>
</data>
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
<value>Fenster-Rahmen</value>
</data>
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
<value>Tooltips</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Chat-Tabs -->
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
<value>Nachrichten-Verhalten</value>
</data>
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
<value>Vorschau</value>
</data>
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
<value>Emotes</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Speicherung</value>
</data>
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
<value>Übersicht</value>
</data>
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
<value>Wartung</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Information-Tabs -->
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
<value>Versionsinfo</value>
</data>
<data name="Settings_Information_About_Heading" xml:space="preserve">
<value>Über HellionChat</value>
</data>
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
<value>Changelog</value>
</data>
<!-- Hellion Chat — Default-Tab-Presets (kanalspezifisch) -->
<data name="Tabs_Presets_System" xml:space="preserve">
<value>System</value>
</data>
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
<value>Free Company</value>
</data>
<data name="Tabs_Presets_Party" xml:space="preserve">
<value>Gruppe</value>
</data>
<data name="Tabs_Presets_Beginner" xml:space="preserve">
<value>Neulinge</value>
</data>
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
<value>Linkshell</value>
</data>
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value>
</data>
</root> </root>
+281 -17
View File
@@ -21,6 +21,12 @@
<data name="Privacy_FilterEnabled_Description" xml:space="preserve"> <data name="Privacy_FilterEnabled_Description" xml:space="preserve">
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value> <value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value>
</data> </data>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
<value>The filter only controls what is written to the local database. The chat log itself keeps showing every message live, disallowed channels just stop being saved. Use the channel hide options in your in-game chat tabs if you want to remove channels from the visible chat.</value>
</data>
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
<value>Privacy filter and whitelist</value>
</data>
<data name="Privacy_Whitelist_Help" xml:space="preserve"> <data name="Privacy_Whitelist_Help" xml:space="preserve">
<value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value> <value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value>
</data> </data>
@@ -72,6 +78,12 @@
<data name="Cleanup_Help_SavedNote" xml:space="preserve"> <data name="Cleanup_Help_SavedNote" xml:space="preserve">
<value>Cleanup uses your SAVED whitelist (Plugin.Config), not unsaved edits above. Click Save first if you want to apply your current edits.</value> <value>Cleanup uses your SAVED whitelist (Plugin.Config), not unsaved edits above. Click Save first if you want to apply your current edits.</value>
</data> </data>
<data name="Retention_Help_SavedNote" xml:space="preserve">
<value>The manual sweep uses your SAVED retention policy, not the slider values above. Click Save first if you want the run to apply your current edits.</value>
</data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Preview is out of date — your whitelist has changed since the last refresh. Click Refresh to recalculate.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve"> <data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Refresh preview</value> <value>Refresh preview</value>
</data> </data>
@@ -121,7 +133,7 @@
<value>Auto-delete messages after a per-channel retention window</value> <value>Auto-delete messages after a per-channel retention window</value>
</data> </data>
<data name="Retention_Enabled_Description" xml:space="preserve"> <data name="Retention_Enabled_Description" xml:space="preserve">
<value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default — the plugin never deletes history without your explicit consent.</value> <value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default. The plugin never deletes history without your explicit consent.</value>
</data> </data>
<data name="Retention_Default_Label" xml:space="preserve"> <data name="Retention_Default_Label" xml:space="preserve">
<value>Default retention (days, 0 = never)</value> <value>Default retention (days, 0 = never)</value>
@@ -171,18 +183,6 @@
<data name="Retention_Error" xml:space="preserve"> <data name="Retention_Error" xml:space="preserve">
<value>Retention sweep failed, see /xllog</value> <value>Retention sweep failed, see /xllog</value>
</data> </data>
<data name="Migration_Notification_Title" xml:space="preserve">
<value>Hellion Chat</value>
</data>
<data name="Migration_Notification_Content" xml:space="preserve">
<value>Privacy filter activated by default. Settings → Privacy to adjust.</value>
</data>
<data name="Migration_Webinterface_Removed_Title" xml:space="preserve">
<value>Hellion Chat 0.2.0</value>
</data>
<data name="Migration_Webinterface_Removed_Content" xml:space="preserve">
<value>The webinterface has been removed in this version because it could not be hardened to the privacy guarantees Hellion Chat makes by default. If you used it, please consult the README for context.</value>
</data>
<data name="Wizard_Title" xml:space="preserve"> <data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Welcome</value> <value>Hellion Chat — Welcome</value>
</data> </data>
@@ -270,14 +270,11 @@
<data name="Export_Error" xml:space="preserve"> <data name="Export_Error" xml:space="preserve">
<value>Export failed, see /xllog</value> <value>Export failed, see /xllog</value>
</data> </data>
<data name="Theme_Heading" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Theme_Enabled_Name" xml:space="preserve"> <data name="Theme_Enabled_Name" xml:space="preserve">
<value>Use the Hellion theme across all plugin windows</value> <value>Use the Hellion theme across all plugin windows</value>
</data> </data>
<data name="Theme_Enabled_Description" xml:space="preserve"> <data name="Theme_Enabled_Description" xml:space="preserve">
<value>Industrial HUD palette with cyan-teal action accents, slate-violet tabs and amber active highlights, applied globally to chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value> <value>Hellion Online Media palette of Arctic Cyan plus Ember Orange, applied across the chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value>
</data> </data>
<data name="Theme_WindowOpacity_Label" xml:space="preserve"> <data name="Theme_WindowOpacity_Label" xml:space="preserve">
<value>Window opacity</value> <value>Window opacity</value>
@@ -291,4 +288,271 @@
<data name="Theme_UseHellionFont_Description" xml:space="preserve"> <data name="Theme_UseHellionFont_Description" xml:space="preserve">
<value>Renders chat and UI in Exo 2 (SIL Open Font License 1.1) which ships with the plugin. Disable to fall back to whatever font you picked under Settings → Fonts.</value> <value>Renders chat and UI in Exo 2 (SIL Open Font License 1.1) which ships with the plugin. Disable to fall back to whatever font you picked under Settings → Fonts.</value>
</data> </data>
<data name="About_Maintainer_Heading" xml:space="preserve">
<value>Maintainer</value>
</data>
<data name="About_Maintainer_Body" xml:space="preserve">
<value>I maintain Hellion Chat through Hellion Online Media. The website has the contact details for licensing, legal or business questions.</value>
</data>
<data name="About_Maintainer_Website_Label" xml:space="preserve">
<value>Website:</value>
</data>
<data name="About_Mission_Heading" xml:space="preserve">
<value>Why this fork exists</value>
</data>
<data name="About_Mission_P1" xml:space="preserve">
<value>Hellion Chat is not trying to replace Chat 2. Chat 2 ships a complete chat experience with full history available for filtering, search and replay. That default is the right one for most users. This fork takes a different stance: a smaller default footprint, with extra knobs for users who want to keep less third-party chat on disk.</value>
</data>
<data name="About_Mission_P2" xml:space="preserve">
<value>The reason I wanted that narrower default was personal. After two years on Chat 2 my database had grown past two million messages, most of them /say, /shout and /yell from strangers in Limsa. That data is exactly what makes Chat 2's full-history view powerful and most users are happy to keep it. For my own taste I wanted a smaller default. So I built this fork.</value>
</data>
<data name="About_Mission_P3" xml:space="preserve">
<value>I am not chasing a big audience and the fork is not in competition with Chat 2. The code is open under the same EUPL-1.2 licence as the upstream plugin. Infi, Anna or anyone else are welcome to read it, borrow ideas, ask questions, or ignore the project. All three are fine by me.</value>
</data>
<data name="About_BuiltOn_Heading" xml:space="preserve">
<value>Built on Chat 2</value>
</data>
<data name="About_BuiltOn_P1" xml:space="preserve">
<value>Hellion Chat is a fork of Chat 2 by Infi and Anna (ascclemens). The chat replacement window, the IPC integration, the rendering engine and the entire storage core come from upstream Chat 2.</value>
</data>
<data name="About_BuiltOn_P2" xml:space="preserve">
<value>The webinterface is the only major piece I removed. It is built for remote access to chat from a second device, which is a different focus than the smaller default footprint this fork is built around. Aligning it with these defaults would have meant a substantial rebuild, so removing it was the cleaner path for this particular fork.</value>
</data>
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
<value>Upstream repository:</value>
</data>
<data name="About_License_Heading" xml:space="preserve">
<value>License</value>
</data>
<data name="About_License_P1" xml:space="preserve">
<value>Hellion Chat and Chat 2 both ship under the European Union Public Licence v1.2 (EUPL-1.2).</value>
</data>
<data name="About_License_P2" xml:space="preserve">
<value>© 2023 to 2026, the Chat 2 authors (Infi, Anna and the upstream contributors).</value>
</data>
<data name="About_License_P3" xml:space="preserve">
<value>© 2026 Hellion Online Media for the additions made in this fork.</value>
</data>
<data name="About_SE_Heading" xml:space="preserve">
<value>FINAL FANTASY XIV disclaimer</value>
</data>
<data name="About_SE_P1" xml:space="preserve">
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved.</value>
</data>
<data name="About_SE_P2" xml:space="preserve">
<value>Hellion Chat is an unofficial, fan-made plugin. It has no affiliation with Square Enix and is not endorsed, sponsored or approved by them.</value>
</data>
<data name="About_Localization_Heading" xml:space="preserve">
<value>Localization</value>
</data>
<data name="About_Localization_P1" xml:space="preserve">
<value>The German translations of the Hellion-specific strings come from me. Other languages are not provided yet.</value>
</data>
<data name="About_Localization_P2" xml:space="preserve">
<value>The translator list below covers the upstream Chat 2 strings on Crowdin. Those volunteers translated Chat 2, not the Hellion additions.</value>
</data>
<data name="About_Translators_TreeNode" xml:space="preserve">
<value>Chat 2 community translators (upstream)</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (runtime strings) -->
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Active Tells</value>
</data>
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
<value>— Earlier conversations —</value>
</data>
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
<value>History could not be loaded.</value>
</data>
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
<value>Marked as greeted. Click to remove the marker.</value>
</data>
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
<value>Mark as greeted.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
<value>Open a tab automatically for each tell partner</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
<value>When you receive or send a /tell, a temporary tab dedicated to that player is opened automatically. Tabs vanish on logout.</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
<value>Maximum number of auto tell tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
<value>When the limit is reached, greeted tabs with the oldest activity are dropped first. The change applies on the next /tell.</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
<value>Compact display</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
<value>Show only a thin separator between persistent tabs and auto tell tabs, without the section header.</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
<value>Show "mark as greeted" button</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>Adds a click-to-toggle button next to each auto tell tab to mark a partner as already greeted, dimming the tab name when set. Useful for club greeters tracking many parallel conversations; off by default.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>The number of preloaded tells is configured in the Privacy tab.</value>
</data>
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
<value>Heads-up: if XIV Messanger or a similar plugin is suppressing direct messages, turn its "Suppress DMs" option off so Hellion Chat can receive tells and open the auto tabs.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Privacy settings tab) -->
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Tell history in auto tabs</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
<value>Number of preloaded tells</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
<value>How many earlier tell messages are loaded from the database when an auto tell tab is opened. 0 disables the preload.</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Only takes effect when auto tell tabs are enabled in the Chat tab.</value>
</data>
<!-- Hellion Chat — Settings UX Polish v10 wipe migration -->
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
<value>Settings reorganised</value>
</data>
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
<value>Hellion Chat 0.5.0 reorganised the settings into themed tabs. Your chat database and your message history stay untouched. Settings have been reset to defaults; if you want to pick a privacy profile again, the reopen button is in the Privacy tab. A backup of your previous config is at HellionChat.json.pre-v10-backup next to the live config file.</value>
</data>
<!-- Hellion Chat — Settings UX Polish 8-tab structure -->
<data name="Settings_Tab_General" xml:space="preserve">
<value>General</value>
</data>
<data name="Settings_Tab_Appearance" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Settings_Tab_Window" xml:space="preserve">
<value>Window</value>
</data>
<data name="Settings_Tab_Chat" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Tab_Tabs" xml:space="preserve">
<value>Tabs</value>
</data>
<data name="Settings_Tab_Database" xml:space="preserve">
<value>Database</value>
</data>
<data name="Settings_Tab_Information" xml:space="preserve">
<value>Information</value>
</data>
<!-- Hellion Chat — General-Tab section headings -->
<data name="Settings_General_Input_Heading" xml:space="preserve">
<value>Input</value>
</data>
<data name="Settings_General_Audio_Heading" xml:space="preserve">
<value>Audio &amp; Notifications</value>
</data>
<data name="Settings_General_Performance_Heading" xml:space="preserve">
<value>Performance</value>
</data>
<data name="Settings_General_Language_Heading" xml:space="preserve">
<value>Language &amp; Input Helpers</value>
</data>
<!-- Hellion Chat — Appearance-Tab section headings -->
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
<value>Fonts</value>
</data>
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
<value>Chat Colours</value>
</data>
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
<value>Timestamps</value>
</data>
<!-- Hellion Chat — Window-Tab section headings -->
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
<value>Inactivity Hide</value>
</data>
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
<value>Window Frame</value>
</data>
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
<value>Tooltips</value>
</data>
<!-- Hellion Chat — Chat-Tab section headings -->
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
<value>Message Behaviour</value>
</data>
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
<value>Preview</value>
</data>
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
<value>Emotes</value>
</data>
<!-- Hellion Chat — Database-Tab section headings -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Storage</value>
</data>
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
<value>Overview</value>
</data>
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
<value>Maintenance</value>
</data>
<!-- Hellion Chat — Information-Tab section headings -->
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
<value>Version Info</value>
</data>
<data name="Settings_Information_About_Heading" xml:space="preserve">
<value>About HellionChat</value>
</data>
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
<value>Changelog</value>
</data>
<!-- Hellion Chat — Default tab presets (channel-themed) -->
<data name="Tabs_Presets_System" xml:space="preserve">
<value>System</value>
</data>
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
<value>Free Company</value>
</data>
<data name="Tabs_Presets_Party" xml:space="preserve">
<value>Party</value>
</data>
<data name="Tabs_Presets_Beginner" xml:space="preserve">
<value>Beginner</value>
</data>
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
<value>Linkshell</value>
</data>
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value>
</data>
</root> </root>
+91 -7
View File
@@ -99,8 +99,8 @@ public sealed class ChatLogWindow : Window
SetUpTextCommandChannels(); SetUpTextCommandChannels();
SetUpAllCommands(); SetUpAllCommands();
Plugin.Commands.Register("/clearlog2", "Clear the Chat 2 chat log").Execute += ClearLog; Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
Plugin.Commands.Register("/chat2").Execute += ToggleChat; Plugin.Commands.Register("/hellion").Execute += ToggleChat;
Plugin.ClientState.Login += Login; Plugin.ClientState.Login += Login;
Plugin.ClientState.Logout += Logout; Plugin.ClientState.Logout += Logout;
@@ -115,8 +115,8 @@ public sealed class ChatLogWindow : Window
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip); Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Logout -= Logout;
Plugin.ClientState.Login -= Login; Plugin.ClientState.Login -= Login;
Plugin.Commands.Register("/chat2").Execute -= ToggleChat; Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
Plugin.Commands.Register("/clearlog2").Execute -= ClearLog; Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
} }
private void Logout(int _, int __) private void Logout(int _, int __)
@@ -451,7 +451,9 @@ public sealed class ChatLogWindow : Window
Flags |= ImGuiWindowFlags.NoTitleBar; Flags |= ImGuiWindowFlags.NoTitleBar;
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked) if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
BgAlpha = Plugin.Config.WindowAlpha / 100f; BgAlpha = Plugin.Config.HellionThemeEnabled
? Plugin.Config.HellionThemeWindowOpacity
: Plugin.Config.WindowAlpha / 100f;
LastViewport = ImGui.GetWindowViewport().Handle; LastViewport = ImGui.GetWindowViewport().Handle;
WasDocked = ImGui.IsWindowDocked(); WasDocked = ImGui.IsWindowDocked();
@@ -1188,7 +1190,13 @@ public sealed class ChatLogWindow : Window
if (tab.DisplayTimestamp) if (tab.DisplayTimestamp)
{ {
var localTime = message.Date.ToLocalTime(); var localTime = message.Date.ToLocalTime();
var timestamp = localTime.ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("de-DE")); // Force the format explicitly per setting. Relying on the
// current culture meant a German system locale always
// produced 24h regardless of the toggle, so the checkbox
// looked dead.
var timestamp = Plugin.Config.Use24HourClock
? localTime.ToString("HH:mm", CultureInfo.InvariantCulture)
: localTime.ToString("h:mm tt", CultureInfo.InvariantCulture);
if (isTable) if (isTable)
{ {
if (!Plugin.Config.HideSameTimestamps || timestamp != lastTimestamp) if (!Plugin.Config.HideSameTimestamps || timestamp != lastTimestamp)
@@ -1303,14 +1311,90 @@ public sealed class ChatLogWindow : Window
if (child) if (child)
{ {
var previousTab = Plugin.CurrentTab; var previousTab = Plugin.CurrentTab;
// Hellion Chat — auto-tell-tabs section divider rendered
// exactly once before the first temp tab, with a live unit
// counter pulled directly from the tab list.
var tempTabHeaderRendered = false;
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++) for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
{ {
var tab = Plugin.Config.Tabs[tabI]; var tab = Plugin.Config.Tabs[tabI];
if (tab.PopOut) if (tab.PopOut)
continue; continue;
if (tab.IsTempTab && !tempTabHeaderRendered)
{
ImGui.Separator();
if (!Plugin.Config.AutoTellTabsCompactDisplay)
{
ImGui.TextDisabled($"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})");
}
tempTabHeaderRendered = true;
}
var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})";
var clicked = ImGui.Selectable($"{tab.Name}{unread}###log-tab-{tabI}", Plugin.LastTab == tabI || Plugin.WantedTab == tabI); var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}";
var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI;
var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
if (showGreetedAffordance)
{
// Greeted toggle sits left of the selectable so the
// click areas stay separate. The icon also doubles
// as the visual "I'm done with this person" cue.
// Compact frame padding keeps the icon dezent next
// to the tab name instead of a chunky button block.
var greetedIcon = tab.IsGreeted ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.Check;
var greetedTooltip = tab.IsGreeted
? HellionStrings.AutoTellTabs_GreetedTooltip
: HellionStrings.AutoTellTabs_UnGreetedTooltip;
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(2, 1)))
using (ImRaii.PushColor(ImGuiCol.Button, 0))
{
if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip))
{
if (tab.IsGreeted)
{
Plugin.AutoTellTabsService.UnmarkGreeted(tab);
}
else
{
Plugin.AutoTellTabsService.MarkGreeted(tab);
}
}
}
ImGui.SameLine();
}
bool clicked;
if (showGreetedAffordance && tab.IsGreeted)
{
// Dim the tab name once the user marked the partner
// as greeted, so a glance at the sidebar tells them
// who still needs attention. Selectable has no idle
// background slot in ImGui, so the dim only applies
// to the selected and hovered states — the text dim
// alone signals greeted in the idle state.
var headerBase = ImGui.GetColorU32(ImGuiCol.Header);
var hoverBase = ImGui.GetColorU32(ImGuiCol.HeaderHovered);
var dimHeader = (headerBase & 0xFF000000u) | ((headerBase & 0x00FEFEFEu) >> 1);
var dimHover = (hoverBase & 0xFF000000u) | ((hoverBase & 0x00FEFEFEu) >> 1);
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)))
using (ImRaii.PushColor(ImGuiCol.Header, dimHeader))
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, dimHover))
{
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
}
}
else
{
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
}
DrawTabContextMenu(tab, tabI); DrawTabContextMenu(tab, tabI);
if (!clicked && Plugin.WantedTab != tabI) if (!clicked && Plugin.WantedTab != tabI)
+4 -4
View File
@@ -22,7 +22,7 @@ namespace ChatTwo.Ui;
public class DbViewer : Window public class DbViewer : Window
{ {
public const float RowPerPage = 1000.0f; public const int RowPerPage = 1000;
private readonly Plugin Plugin; private readonly Plugin Plugin;
@@ -76,19 +76,19 @@ public class DbViewer : Window
RespectCloseHotkey = false; RespectCloseHotkey = false;
DisableWindowSounds = true; DisableWindowSounds = true;
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute += Toggle; Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
} }
public void Dispose() public void Dispose()
{ {
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute -= Toggle; Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
} }
private void Toggle(string _, string __) => Toggle(); private void Toggle(string _, string __) => Toggle();
public override void Draw() public override void Draw()
{ {
var totalPages = (int)Math.Ceiling(Count / RowPerPage); var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
if (totalPages < 1) if (totalPages < 1)
totalPages = 1; totalPages = 1;
+2 -2
View File
@@ -28,12 +28,12 @@ public class DebuggerWindow : Window
RespectCloseHotkey = false; RespectCloseHotkey = false;
DisableWindowSounds = true; DisableWindowSounds = true;
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute += Toggle; Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
} }
public void Dispose() public void Dispose()
{ {
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute -= Toggle; Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
} }
private void Toggle(string _, string __) => Toggle(); private void Toggle(string _, string __) => Toggle();
+63 -47
View File
@@ -21,63 +21,79 @@ internal static class HellionStyle
{ {
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs // Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui // Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
// expects. // expects. Hex values are sourced from the Hellion Online Media brand
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
// Primary — cyan-teal for actionable controls (buttons, checks, sliders). // Primary — Arctic Cyan, used for every interactive control (buttons,
private const uint PrimaryRgba = 0x00B8D4FF; // checks, sliders, separators when hovered). Three brand stages plus a
private const uint PrimaryHoverRgba = 0x26C6DAFF; // hover that lifts to brand-color-light and a press that drops to
private const uint PrimaryActiveRgba = 0x00838FFF; // brand-color-dark.
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
// Secondary — industrial amber, used as a warm highlight for active // Identity — brand-color-dark teal for window title bars and the
// states (tab borders, resize grips, scrollbar grabs). // active tab. Sits visibly below the primary cyan on buttons so the
private const uint SecondaryRgba = 0xFFB300FF; // user sees "where am I" (deep teal) versus "what can I click"
private const uint SecondaryHoverRgba = 0xFFC940FF; // (brand cyan) without leaving the cyan family.
private const uint SecondaryActiveRgba = 0xC68400FF; private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
// Tertiary — slate violet, reserved for title bars and the active tab // Accent — Ember Orange for warm highlights on grips and scrollbar
// background so identity beats out the cyan accent without competing // pulls. Replaces the previous industrial amber so the plugin matches
// with it on action controls. // the website's CTA palette. AccentActive is reserved for any future
private const uint TertiaryRgba = 0x7B61FFFF; // pressed-state on accent surfaces; the current slots only need
private const uint TertiaryHoverRgba = 0x9580FFFF; // AccentRgba and AccentHoverRgba.
private const uint TertiaryActiveRgba = 0x5E45D9FF; private const uint AccentRgba = 0xF97316FF; // accent-color
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
// Surfaces — deep slate window/frame backgrounds, steel borders. // Surfaces — Hellion brand background ladder. Window darkest, frame
private const uint WindowBgRgba = 0x0E1A20FF; // hover ladder climbs into surface tones. Matches the website's
private const uint ChildBgRgba = 0x102027FF; // background / background-medium / background-light / surface vars.
private const uint PopupBgRgba = 0x102027FF; private const uint WindowBgRgba = 0x070B12FF; // background
private const uint FrameBgRgba = 0x162831FF; private const uint ChildBgRgba = 0x0C1220FF; // background-medium
private const uint FrameBgHoverRgba = 0x1F3540FF; private const uint PopupBgRgba = 0x0C1220FF; // background-medium
private const uint FrameBgActiveRgba = 0x274250FF; private const uint FrameBgRgba = 0x141E30FF; // background-light
private const uint BorderRgba = 0x37474FFF; private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
private const uint BorderRgba = 0x00BED266;
private const uint BorderShadowRgba = 0x00000000; private const uint BorderShadowRgba = 0x00000000;
// Headers / collapsing-headers / tree nodes / selectables. // Headers / collapsing-headers / tree nodes / selectables — same
private const uint HeaderRgba = 0x1B2C36FF; // surface ladder as frames so panels feel consistent.
private const uint HeaderHoverRgba = 0x263A45FF; private const uint HeaderRgba = 0x141E30FF;
private const uint HeaderActiveRgba = 0x324A57FF; private const uint HeaderHoverRgba = 0x1A2538FF;
private const uint HeaderActiveRgba = 0x22303FFF;
// Title bars — tertiary identity for the active state. // Title bars — Identity teal on active so the focused window reads
private const uint TitleBgRgba = 0x0E1A20FF; // as "yours" without using accent or primary slots.
private const uint TitleBgActiveRgba = 0x5E45D9FF; private const uint TitleBgRgba = 0x070B12FF;
private const uint TitleBgCollapsedRgba = 0x0A1318FF; private const uint TitleBgActiveRgba = IdentityRgba;
private const uint TitleBgCollapsedRgba = 0x05080EFF;
// Tabs — tertiary tint, secondary highlight while hovered/unfocused. // Tabs — neutral inactive, Identity-light on hover, Identity teal on
private const uint TabRgba = 0x162831FF; // active. Unfocused-active uses the deeper Identity stage so an
private const uint TabHoveredRgba = 0x9580FFFF; // unfocused window's active tab still reads but does not pull focus.
private const uint TabActiveRgba = 0x7B61FFFF; private const uint TabRgba = 0x141E30FF;
private const uint TabUnfocusedRgba = 0x12222AFF; private const uint TabHoveredRgba = IdentityHoverRgba;
private const uint TabUnfocusedActiveRgba = 0x5E45D9FF; private const uint TabActiveRgba = IdentityRgba;
private const uint TabUnfocusedRgba = 0x0C1220FF;
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
// Scrollbar — slate base, secondary amber on grab. // Scrollbar — Ember on grab so the pull stands out without competing
private const uint ScrollbarBgRgba = 0x0E1A20FF; // with the cyan action buttons. Idle grab is a subtle surface tone,
private const uint ScrollbarGrabRgba = 0x37474FFF; // hover/active climb into accent.
private const uint ScrollbarGrabHoveredRgba = 0xFFC940FF; private const uint ScrollbarBgRgba = 0x070B12FF;
private const uint ScrollbarGrabActiveRgba = 0xFFB300FF; private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
private const uint ScrollbarGrabActiveRgba = AccentRgba;
// Resize grip — secondary amber for the active corner pull. // Resize grip — same Ember treatment as the scrollbar.
private const uint ResizeGripRgba = 0x37474FFF; private const uint ResizeGripRgba = 0x141E30FF;
private const uint ResizeGripHoveredRgba = 0xFFC940FF; private const uint ResizeGripHoveredRgba = AccentHoverRgba;
private const uint ResizeGripActiveRgba = 0xFFB300FF; private const uint ResizeGripActiveRgba = AccentRgba;
// Separator and check mark / slider follow the primary cyan. // Separator and check mark / slider follow the primary cyan.
+10 -2
View File
@@ -70,8 +70,16 @@ internal class Popout : Window
if (!ChatLogWindow.PopOutDocked[Idx]) if (!ChatLogWindow.PopOutDocked[Idx])
{ {
var alpha = Tab.IndependentOpacity ? Tab.Opacity : Plugin.Config.WindowAlpha; if (Tab.IndependentOpacity)
BgAlpha = alpha / 100f; {
BgAlpha = Tab.Opacity / 100f;
}
else
{
BgAlpha = Plugin.Config.HellionThemeEnabled
? Plugin.Config.HellionThemeWindowOpacity
: Plugin.Config.WindowAlpha / 100f;
}
} }
} }
+2 -2
View File
@@ -30,14 +30,14 @@ public class SeStringDebugger : Window
DisableWindowSounds = true; DisableWindowSounds = true;
#if DEBUG #if DEBUG
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute += Toggle; Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
#endif #endif
} }
public void Dispose() public void Dispose()
{ {
#if DEBUG #if DEBUG
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute -= Toggle; Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
#endif #endif
} }
+15 -17
View File
@@ -9,7 +9,7 @@ using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui; namespace ChatTwo.Ui;
public sealed class SettingsWindow : Window public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
{ {
private readonly Plugin Plugin; private readonly Plugin Plugin;
@@ -33,18 +33,14 @@ public sealed class SettingsWindow : Window
Tabs = Tabs =
[ [
new Display(Mutable), new General(Plugin, Mutable),
new ChatLog(Plugin, Mutable), new Appearance(Plugin, Mutable),
new Emote(Plugin, Mutable), new SettingsTabs.Window(Plugin, Mutable),
new Preview(Mutable), new Chat(Plugin, Mutable),
new Fonts(Mutable), new SettingsTabs.Tabs(Plugin, Mutable),
new ChatColours(Plugin, Mutable),
new Tabs(Plugin, Mutable),
new SettingsTabs.Privacy(Plugin, Mutable), new SettingsTabs.Privacy(Plugin, Mutable),
new Database(Plugin, Mutable), new Database(Plugin, Mutable),
new Miscellaneous(Mutable), new Information(Mutable),
new Changelog(Mutable),
new About()
]; ];
RespectCloseHotkey = false; RespectCloseHotkey = false;
@@ -52,14 +48,14 @@ public sealed class SettingsWindow : Window
Initialise(); Initialise();
Plugin.Commands.Register("/chat2", "Perform various actions with Chat 2.").Execute += Command; Plugin.Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute += Command;
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle; Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
} }
public void Dispose() public void Dispose()
{ {
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle; Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
Plugin.Commands.Register("/chat2").Execute -= Command; Plugin.Commands.Register("/hellion").Execute -= Command;
} }
private void Command(string command, string args) private void Command(string command, string args)
@@ -114,14 +110,16 @@ public sealed class SettingsWindow : Window
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(Language.Settings_SaveAndClose)) { if (ImGui.Button(Language.Settings_SaveAndClose))
{
save = true; save = true;
IsOpen = false; IsOpen = false;
} }
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(Language.Settings_Discard)) { if (ImGui.Button(Language.Settings_Discard))
{
IsOpen = false; IsOpen = false;
} }
@@ -135,7 +133,7 @@ public sealed class SettingsWindow : Window
{ {
var buttonWidth = ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2; var buttonWidth = ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
var buttonWidth2 = ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2; var buttonWidth2 = ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2); ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2 - ImGui.GetStyle().ItemSpacing.X);
if (ImGui.Button(buttonLabel2)) if (ImGui.Button(buttonLabel2))
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii"); Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
@@ -179,7 +177,7 @@ public sealed class SettingsWindow : Window
GameFunctions.GameFunctions.SetChatInteractable(true); GameFunctions.GameFunctions.SetChatInteractable(true);
if (Plugin.Config.ShowEmotes) if (Plugin.Config.ShowEmotes)
Task.Run(EmoteCache.LoadData); _ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
Initialise(); Initialise();
} }
-127
View File
@@ -1,127 +0,0 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class About : ISettingsTab
{
public string Name => string.Format(Language.Options_About_Tab, Plugin.PluginName) + "###tabs-about";
private readonly List<string> Translators =
[
"q673135110", "Akizem", "d0tiKs",
"Moonlight_Everlit", "Dark32", "andreycout",
"Button_", "Cali666", "cassandra308",
"lokinmodar", "jtabox", "AkiraYorumoto",
"MKhayle", "elena.space", "imlisa",
"andrei5125", "ShivaMaheshvara", "aislinn87",
"nishinatsu051", "lichuyuan", "Risu64",
"yummypillow", "witchymary", "Yuzumi",
"zomsakura", "Sirayuki"
];
internal About()
{
Translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal));
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Authors);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Interface.Manifest.Author);
ImGui.TextUnformatted(Language.Options_About_Discord);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, "@j.j_kazama");
ImGui.TextUnformatted(Language.Options_About_Version);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Interface.Manifest.AssemblyVersion.ToString(3));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues");
ImGuiHelpers.ScaledDummy(10.0f);
// Hellion-specific maintainer / attribution / license / SE-
// disclaimer block. Hand-rolled in English here rather than via
// HellionStrings — the legal-ish copy stays close to the EUPL-1.2
// wording and the SE disclaimer is the same in every locale.
ImGui.TextColored(ImGuiColors.ParsedGold, "Maintainer");
ImGui.TextUnformatted("Hellion Chat is maintained by Hellion Online Media (Florian Wathling).");
ImGui.TextUnformatted("Website:");
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
ImGui.TextUnformatted("For licensing, legal or contact inquiries please reach out via the website above.");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, "Built on Chat 2");
ImGui.TextUnformatted("Hellion Chat is a fork of Chat 2 by Infi and Anna (ascclemens).");
ImGui.TextUnformatted("Every chat replacement feature, the IPC integration, the rendering engine and the storage core come from upstream Chat 2.");
ImGui.TextUnformatted("The upstream webinterface is intentionally not part of Hellion Chat — it could not be hardened to the privacy guarantees this fork makes by default.");
ImGui.TextUnformatted("Upstream repository:");
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, "License");
ImGui.TextUnformatted("Hellion Chat and Chat 2 are licensed under the European Union Public License v1.2 (EUPL-1.2).");
ImGui.TextUnformatted("© 20232026 the Chat 2 authors (Infi, Anna and the upstream contributors).");
ImGui.TextUnformatted("© 2026 Hellion Online Media — for the Hellion Chat additions.");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.DalamudOrange, "FINAL FANTASY XIV disclaimer");
ImGui.TextUnformatted("FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved.");
ImGui.TextUnformatted("Hellion Chat is an unofficial, fan-made plugin and is not affiliated with, endorsed, sponsored or approved by Square Enix.");
ImGui.Spacing();
ImGui.TextColored(ImGuiColors.ParsedGold, "Localization");
ImGui.TextUnformatted("German translations of Hellion-specific UI strings (HellionStrings.de.resx) are written by the Hellion Online Media maintainer.");
ImGui.TextUnformatted("All other locales for Hellion-specific strings are not currently provided.");
ImGui.TextUnformatted("The translator list below covers the upstream Chat 2 community translators on Crowdin — their work covers the inherited Chat 2 strings, not the Hellion additions.");
ImGui.Spacing();
var height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize("A").Y - ImGui.GetStyle().ItemSpacing.Y * 2;
using (var aboutChild = ImRaii.Child("about", new Vector2(-1, height)))
{
if (aboutChild)
{
using var treeNode = ImRaii.TreeNode("Chat 2 community translators (upstream)");
if (treeNode)
{
using var translatorChild = ImRaii.Child("translators");
if (translatorChild)
{
foreach (var translator in Translators)
ImGui.TextUnformatted(translator);
}
}
}
}
ImGui.Spacing();
}
}
+292
View File
@@ -0,0 +1,292 @@
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Appearance : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => HellionStrings.Settings_Tab_Appearance + "###tabs-appearance";
internal Appearance(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
public void Draw(bool changed)
{
DrawThemeSection();
ImGui.Spacing();
DrawFontsSection();
ImGui.Spacing();
DrawColoursSection();
ImGui.Spacing();
DrawTimestampsSection();
}
private void DrawThemeSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Theme_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
// Clamp 0.51.0 stays consistent with Privacy.cs which already
// shipped this slider; lower values would let chat windows
// disappear behind game UI.
using (ImRaii.Disabled(!Mutable.HellionThemeEnabled))
{
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
var opacity = Mutable.HellionThemeWindowOpacity;
if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f"))
{
Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f);
}
ImGuiUtil.HelpMarker(HellionStrings.Theme_WindowOpacity_Help);
}
ImGui.Spacing();
ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle);
ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc);
if (Mutable.OverrideStyle)
{
DrawStyleCombo();
}
// The Bestand-Slider WindowAlpha targets the chat log window's
// background only. The Hellion theme opacity above already covers
// every plugin window globally, so the two sliders fight each
// other when the theme is active. Disable the legacy slider in
// that case to make Hellion theme the single source of truth.
using (ImRaii.Disabled(Mutable.HellionThemeEnabled))
{
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
}
}
}
private void DrawStyleCombo()
{
var styles = StyleModel.GetConfiguredStyles();
if (styles == null)
{
ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable);
return;
}
var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected;
using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle);
if (!combo)
{
return;
}
foreach (var style in styles)
{
if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name))
{
Mutable.ChosenStyle = style.Name;
}
}
}
private void DrawFontsSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Fonts_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
if (ImGui.Checkbox(HellionStrings.Theme_UseHellionFont_Name, ref Mutable.UseHellionFont))
{
// Mutex with the Bestand custom-font stack. Leaving FontsEnabled
// checked alongside UseHellionFont made both checkboxes look
// active even though the lower stack was greyed out, which
// confused the user during the v0.5.0 walkthrough.
if (Mutable.UseHellionFont)
Mutable.FontsEnabled = false;
}
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
ImGui.Spacing();
using var fontDisabled = ImRaii.Disabled(Mutable.UseHellionFont);
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
ImGui.Spacing();
var unused = false;
if (!Mutable.FontsEnabled)
{
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
}
else
{
var globalChooser = ImGuiUtil.FontChooser(Language.Options_Font_Name, Mutable.GlobalFontV2, false, ref unused);
globalChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
{
Plugin.Framework.Run(() => Mutable.GlobalFontV2 = r.Result);
}
});
ImGui.SameLine();
if (ImGui.Button("Reset##global"))
{
Mutable.GlobalFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_Font_Description, Plugin.PluginName));
ImGuiUtil.WarningText(Language.Options_Font_Warning);
ImGui.Spacing();
// LocaleNames being null means it is likely a game font which all support JP symbols.
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref unused, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
japaneseChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
{
Plugin.Framework.Run(() => Mutable.JapaneseFontV2 = r.Result);
}
});
ImGui.SameLine();
if (ImGui.Button("Reset##japanese"))
{
Mutable.JapaneseFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f };
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_JapaneseFont_Description, Plugin.PluginName));
ImGui.Spacing();
var italicChooser = ImGuiUtil.FontChooser(Language.Options_ItalicFont_Name, Mutable.ItalicFontV2, true, ref Mutable.ItalicEnabled);
italicChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
{
Plugin.Framework.Run(() => Mutable.ItalicFontV2 = r.Result);
}
});
ImGui.SameLine();
if (ImGui.Button("Reset##italic"))
{
Mutable.ItalicEnabled = false;
Mutable.ItalicFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_Italic_Description, Plugin.PluginName));
ImGui.Spacing();
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
{
ImGuiUtil.HelpMarker(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName));
var range = (int)Mutable.ExtraGlyphRanges;
foreach (var extra in Enum.GetValues<ExtraGlyphRanges>())
{
ImGui.CheckboxFlags(extra.Name(), ref range, (int)extra);
}
Mutable.ExtraGlyphRanges = (ExtraGlyphRanges)range;
}
ImGui.Spacing();
}
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
ImGuiUtil.HelpMarker(Language.Options_SymbolsFontSize_Description);
ImGui.Spacing();
}
}
private void DrawColoursSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Colours_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
foreach (var (_, types) in ChatTypeExt.SortOrder)
{
foreach (var type in types)
{
if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset))
{
Mutable.ChatColours.Remove(type);
}
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import))
{
var gameColour = Plugin.Functions.Chat.GetChannelColor(type);
Mutable.ChatColours[type] = gameColour ?? type.DefaultColor() ?? 0;
}
ImGui.SameLine();
var vec = Mutable.ChatColours.TryGetValue(type, out var colour)
? ColourUtil.RgbaToVector3(colour)
: ColourUtil.RgbaToVector3(type.DefaultColor() ?? 0);
if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs))
{
Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec);
}
}
}
ImGui.Spacing();
}
}
private void DrawTimestampsSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_PrettierTimestamps_Name, ref Mutable.PrettierTimestamps);
ImGuiUtil.HelpMarker(Language.Options_PrettierTimestamps_Description);
if (Mutable.PrettierTimestamps)
{
ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty);
ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description);
ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps);
ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description);
}
ImGui.Checkbox(Language.Options_Use24HourClock_Name, ref Mutable.Use24HourClock);
ImGuiUtil.HelpMarker(Language.Options_Use24HourClock_Description);
}
}
}
-51
View File
@@ -1,51 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Changelog : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Changelog_Tab + "###tabs-changelog";
internal Changelog(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.TextUnformatted(Language.Options_Warning_NotImplemented);
ImGuiUtil.OptionCheckbox(ref Mutable.PrintChangelog, Language.Options_PrintChangelog_Name, Language.Options_PrintChangelog_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
var changelog = Plugin.Interface.Manifest.Changelog;
if (changelog != null)
{
ImGui.TextUnformatted(Language.Options_Changelog_Header);
ImGui.TextUnformatted($"Version {Plugin.Interface.Manifest.AssemblyVersion.ToString(3)}");
ImGui.Spacing();
foreach (var sentence in changelog.Split("\n"))
{
if (sentence == string.Empty)
{
ImGui.NewLine();
continue;
}
var condition = sentence.StartsWith('-') || sentence.StartsWith(" -");
using var indent = ImRaii.PushIndent(10.0f, true, condition);
ImGui.TextUnformatted(sentence);
}
}
ImGui.Spacing();
}
}
+234
View File
@@ -0,0 +1,234 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
// Chat-Tab — vier eigenständige Sektionen: Auto-Tell-Tabs, Behaviour,
// Preview, Emotes. Der Emotes-Block ist 1:1 aus der Bestand-Datei
// Emote.cs übernommen; die Datei wird in Plan-Task 11 (Settings UX
// Polish v0.5.0) entfernt, sobald alle Tabs migriert sind.
internal sealed class Chat : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
private SearchSelector.SelectorPopupOptions WordPopupOptions;
internal Chat(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
WordPopupOptions = RefillSheet();
}
private SearchSelector.SelectorPopupOptions RefillSheet()
{
return new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
};
}
public void Draw(bool changed)
{
DrawAutoTellTabsSection();
ImGui.Spacing();
DrawBehaviourSection();
ImGui.Spacing();
DrawPreviewSection();
ImGui.Spacing();
DrawEmotesSection();
}
private void DrawAutoTellTabsSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_AutoTellTabs_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Enable_Name, ref Mutable.EnableAutoTellTabs);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description);
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
var limit = Mutable.AutoTellTabsLimit;
if (ImGui.SliderInt(HellionStrings.ChatLog_AutoTellTabs_Limit_Name, ref limit, 1, 50))
{
Mutable.AutoTellTabsLimit = limit;
}
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint);
ImGui.Spacing();
ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
}
}
private void DrawBehaviourSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Behaviour_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_CollapseDuplicateMessages_Name, ref Mutable.CollapseDuplicateMessages);
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMessages_Description);
if (Mutable.CollapseDuplicateMessages)
{
ImGui.Checkbox(Language.Options_CollapseDuplicateMsgUniqueLink_Name, ref Mutable.CollapseKeepUniqueLinks);
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
}
}
}
private void DrawPreviewSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Preview_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Preview_Name, Mutable.PreviewPosition.Name()))
{
if (combo)
{
foreach (var position in Enum.GetValues<PreviewPosition>())
{
if (ImGui.Selectable(position.Name(), Mutable.PreviewPosition == position))
{
Mutable.PreviewPosition = position;
}
}
}
}
ImGuiUtil.HelpMarker(Language.Options_Preview_Description);
if (ImGuiUtil.InputIntVertical(Language.Options_PreviewMinimum_Name, Language.Options_PreviewMinimum_Description, ref Mutable.PreviewMinimum))
{
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
}
ImGui.Checkbox(Language.Options_PreviewOnlyIf_Name, ref Mutable.OnlyPreviewIf);
ImGuiUtil.HelpMarker(Language.Options_PreviewOnlyIf_Description);
}
}
private void DrawEmotesSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Emotes_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_ShowEmotes_Name, ref Mutable.ShowEmotes);
ImGuiUtil.HelpMarker(Language.Options_ShowEmotes_Desc);
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
ImGui.Spacing();
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
{
WordPopupOptions = RefillSheet();
}
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
using (Plugin.FontManager.FontAwesome.Push())
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
{
Mutable.BlockedEmotes.Add(newWord);
}
using (var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (table)
{
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
ImGui.TableHeadersRow();
var copiedList = Mutable.BlockedEmotes.ToArray();
foreach (var word in copiedList)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
ImGui.TableNextColumn();
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
{
Mutable.BlockedEmotes.Remove(word);
}
}
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_EmoteStats);
ImGui.Spacing();
if (EmoteCache.State is EmoteCache.LoadingState.Done)
{
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
}
else
{
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
}
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (emoteTable)
{
ImGui.TableSetupColumn("##word1");
ImGui.TableSetupColumn("##word2");
ImGui.TableSetupColumn("##word3");
ImGui.TableSetupColumn("##word4");
ImGui.TableSetupColumn("##word5");
foreach (var word in EmoteCache.SortedCodeArray)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
}
}
}
}
}
}
-69
View File
@@ -1,69 +0,0 @@
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class ChatColours : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => Language.Options_ChatColours_Tab + "###tabs-chat-colours";
internal ChatColours(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
#if DEBUG
// Users can set colours for ExtraChat linkshells in the ExtraChat plugin directly.
var sortable = ChatTypeExt.SortOrder
.SelectMany(entry => entry.Item2)
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
.ToHashSet();
var total = Enum.GetValues<ChatType>()
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
.ToHashSet();
if (sortable.Count != total.Count)
{
Plugin.Log.Warning($"There are {sortable.Count} sortable channels, but there are {total.Count} total channels.");
total.ExceptWith(sortable);
foreach (var missing in total)
Plugin.Log.Information($"Missing {missing}");
}
#endif
}
public void Draw(bool changed)
{
foreach (var (_, types) in ChatTypeExt.SortOrder)
{
foreach (var type in types)
{
if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset))
Mutable.ChatColours.Remove(type);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import))
{
var gameColour = Plugin.Functions.Chat.GetChannelColor(type);
Mutable.ChatColours[type] = gameColour ?? type.DefaultColor() ?? 0;
}
ImGui.SameLine();
var vec = Mutable.ChatColours.TryGetValue(type, out var colour)
? ColourUtil.RgbaToVector3(colour)
: ColourUtil.RgbaToVector3(type.DefaultColor() ?? 0);
if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs))
Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec);
}
}
ImGui.Spacing();
}
}
-119
View File
@@ -1,119 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class ChatLog : ISettingsTab
{
private readonly Plugin Plugin;
private Configuration Mutable { get; }
public string Name => Language.Options_ChatLog_Tab + "###tabs-chatlog";
internal ChatLog(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
public void Draw(bool changed)
{
using (ImRaii.TextWrapPos(0.0f))
{
ImGuiUtil.OptionCheckbox(ref Mutable.KeepInputFocus, Language.Options_KeepInputFocus_Name, Language.Options_KeepInputFocus_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.PlaySounds, Language.Options_PlaySounds_Name, Language.Options_PlaySounds_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.SidebarTabView, Language.Options_SidebarTabView_Name, string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowNoviceNetwork, Language.Options_ShowNoviceNetwork_Name, Language.Options_ShowNoviceNetwork_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowHideButton, Language.Options_ShowHideButton_Name, Language.Options_ShowHideButton_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.NativeItemTooltips, Language.Options_NativeItemTooltips_Name, string.Format(Language.Options_NativeItemTooltips_Description, Plugin.PluginName));
ImGui.Spacing();
if (Mutable.NativeItemTooltips)
{
ImGuiUtil.DragFloatVertical(Language.Options_TooltipOffset_Name, Language.Options_TooltipOffset_Desc, ref Mutable.TooltipOffset, 1, 0f, 400f, $"{Mutable.TooltipOffset:N0}px", ImGuiSliderFlags.AlwaysClamp);
ImGui.Spacing();
}
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
ImGui.Spacing();
if (ImGuiUtil.InputIntVertical(Language.Options_MaxLinesToShow_Name, Language.Options_MaxLinesToShow_Description, ref Mutable.MaxLinesToRender))
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CanMove, Language.Options_CanMove_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CanResize, Language.Options_CanResize_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowTitleBar, Language.Options_ShowTitleBar_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowPopOutTitleBar, Language.Options_ShowPopOutTitleBar_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.OverrideStyle, Language.Options_OverrideStyle_Name, Language.Options_OverrideStyle_Name_Desc);
ImGui.Spacing();
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward);
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_AdjustPosition_Name);
ImGui.SetNextItemWidth(-1);
var pos = Plugin.ChatLogWindow.LastWindowPos;
if (ImGui.DragFloat2($"##{Language.Options_AdjustPosition_Name}", ref pos, 1, 0, float.MaxValue, "%.0fpx"))
Plugin.ChatLogWindow.Position = pos;
ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning);
ImGui.Spacing();
}
if (!Mutable.OverrideStyle)
return;
var styles = StyleModel.GetConfiguredStyles();
if (styles == null)
{
ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable);
ImGui.Spacing();
return;
}
var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected;
using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle);
if (combo)
{
foreach (var style in styles)
if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name))
Mutable.ChosenStyle = style.Name;
}
ImGui.Spacing();
}
}
+77 -51
View File
@@ -16,7 +16,7 @@ internal sealed class Database : ISettingsTab
private Plugin Plugin { get; } private Plugin Plugin { get; }
private Configuration Mutable { get; } private Configuration Mutable { get; }
public string Name => Language.Options_Database_Tab + "###tabs-database"; public string Name => HellionStrings.Settings_Tab_Database + "###tabs-database";
internal Database(Plugin plugin, Configuration mutable) internal Database(Plugin plugin, Configuration mutable)
{ {
@@ -33,56 +33,76 @@ internal sealed class Database : ISettingsTab
public void Draw(bool changed) public void Draw(bool changed)
{ {
// Shift-on-open keeps the Advanced tools available without a permanent
// toggle in the UI, mirroring upstream Chat 2 behaviour.
if (changed) if (changed)
ShowAdvanced = ImGui.GetIO().KeyShift; ShowAdvanced = ImGui.GetIO().KeyShift;
ImGuiUtil.OptionCheckbox(ref Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description); DrawStorageSection();
ImGui.Spacing(); ImGui.Spacing();
DrawViewerSection();
if (ImGuiUtil.OptionCheckbox(ref Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description))
if (Mutable.LoadPreviousSession)
Mutable.FilterIncludePreviousSessions = true;
ImGui.Spacing(); ImGui.Spacing();
DrawStatsSection();
}
if (ImGuiUtil.OptionCheckbox(ref Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description)) private void DrawStorageSection()
if (!Mutable.FilterIncludePreviousSessions) {
Mutable.LoadPreviousSession = false; using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Storage_Heading);
if (!tree.Success)
return;
ImGui.Spacing(); using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
ImGui.Separator();
ImGui.Spacing();
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
if (old.Exists || migratedOld.Exists)
{ {
ImGui.TextUnformatted(Language.Options_Database_Old_Heading); ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages);
ImGui.Spacing(); ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description);
if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip)) if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession))
if (Mutable.LoadPreviousSession)
Mutable.FilterIncludePreviousSessions = true;
ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description);
if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions))
if (!Mutable.FilterIncludePreviousSessions)
Mutable.LoadPreviousSession = false;
ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description);
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
if (old.Exists || migratedOld.Exists)
{ {
try ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
ImGui.Spacing();
if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip))
{ {
if (old.Exists) try
old.Delete(); {
else if (old.Exists)
migratedOld.Delete(); old.Delete();
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success); else
} migratedOld.Delete();
catch (Exception e) WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
{ }
Plugin.Log.Error(e, "Unable to delete old database"); catch (Exception e)
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error); {
Plugin.Log.Error(e, "Unable to delete old database");
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
}
} }
} }
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
} }
}
private void DrawViewerSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Viewer_Heading);
if (!tree.Success)
return;
ImGui.TextUnformatted(Language.Options_Database_Metadata_Heading);
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{ {
// Refresh the database size and message count every 5 seconds to avoid // Refresh the database size and message count every 5 seconds to avoid
@@ -132,28 +152,34 @@ internal sealed class Database : ISettingsTab
WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info); WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info);
} }
} }
}
ImGui.Spacing(); private void DrawStatsSection()
{
if (!ShowAdvanced) if (!ShowAdvanced)
return; return;
using var treeNode = ImRaii.TreeNode(Language.Options_Database_Advanced); using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Stats_Heading);
using var wrap = ImRaii.TextWrapPos(0.0f); if (!tree.Success)
return;
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning); using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()"))
Plugin.MessageManager.Store.PerformMaintenance();
if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()"))
{ {
Plugin.MessageManager.ClearAllTabs(); using var wrap = ImRaii.TextWrapPos(0.0f);
Plugin.MessageManager.FilterAllTabsAsync();
}
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)")) ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
new Thread(() => InsertMessages(10_000)).Start(); if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()"))
ImGui.Spacing(); Plugin.MessageManager.Store.PerformMaintenance();
if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()"))
{
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
}
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
new Thread(() => InsertMessages(10_000)).Start();
}
} }
private void InsertMessages(int count) private void InsertMessages(int count)
-116
View File
@@ -1,116 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Display : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Display_Tab + "###tabs-display";
internal Display(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGuiUtil.OptionCheckbox(ref Mutable.HideChat, Language.Options_HideChat_Name, Language.Options_HideChat_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideDuringCutscenes, Language.Options_HideDuringCutscenes_Name, string.Format(Language.Options_HideDuringCutscenes_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenNotLoggedIn, Language.Options_HideWhenNotLoggedIn_Name, string.Format(Language.Options_HideWhenNotLoggedIn_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenUiHidden, Language.Options_HideWhenUiHidden_Name, string.Format(Language.Options_HideWhenUiHidden_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideInLoadingScreens, Language.Options_HideInLoadingScreens_Name, string.Format(Language.Options_HideInLoadingScreens_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenInactive, Language.Options_HideWhenInactive_Name, Language.Options_HideWhenInactive_Description);
ImGui.Spacing();
if (Mutable.HideWhenInactive)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.InputIntVertical(Language.Options_InactivityHideTimeout_Name,
Language.Options_InactivityHideTimeout_Description, ref Mutable.InactivityHideTimeout, 1, 10);
// Enforce a minimum of 2 seconds to avoid people soft locking
// themselves.
Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout);
ImGui.Spacing();
// This setting conflicts with HideInBattle, so it's disabled.
using (ImRaii.Disabled(Mutable.HideInBattle))
{
ImGuiUtil.OptionCheckbox(ref Mutable.InactivityHideActiveDuringBattle,
Language.Options_InactivityHideActiveDuringBattle_Name,
Language.Options_InactivityHideActiveDuringBattle_Description);
ImGui.Spacing();
}
using var channelTree = ImRaii.TreeNode(Language.Options_InactivityHideChannels_Name);
if (channelTree.Success)
{
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_All_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = TabsUtil.AllChannels();
Mutable.InactivityHideExtraChatAll = true;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.SameLine();
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_None_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = [];
Mutable.InactivityHideExtraChatAll = false;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.Spacing();
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, Mutable.InactivityHideChannelsV2);
ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels,
ref Mutable.InactivityHideExtraChatAll, Mutable.InactivityHideExtraChatChannels);
}
ImGui.Spacing();
}
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.Use24HourClock, Language.Options_Use24HourClock_Name, Language.Options_Use24HourClock_Description);
ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description);
if (Mutable.PrettierTimestamps)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.OptionCheckbox(ref Mutable.MoreCompactPretty, Language.Options_MoreCompactPretty_Name, Language.Options_MoreCompactPretty_Description);
ImGuiUtil.OptionCheckbox(ref Mutable.HideSameTimestamps, Language.Options_HideSameTimestamps_Name, Language.Options_HideSameTimestamps_Description);
}
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseDuplicateMessages, Language.Options_CollapseDuplicateMessages_Name, Language.Options_CollapseDuplicateMessages_Description);
if (Mutable.CollapseDuplicateMessages)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseKeepUniqueLinks, Language.Options_CollapseDuplicateMsgUniqueLink_Name, Language.Options_CollapseDuplicateMsgUniqueLink_Description);
}
ImGui.Spacing();
}
}
-113
View File
@@ -1,113 +0,0 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Emote : ISettingsTab
{
private readonly Plugin Plugin;
private Configuration Mutable { get; }
public string Name => Language.Options_Emote_Tab + "###tabs-emote";
private static SearchSelector.SelectorPopupOptions? WordPopupOptions;
internal Emote(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
WordPopupOptions = new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
};
}
private SearchSelector.SelectorPopupOptions RefillSheet()
{
return new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
};
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGuiUtil.OptionCheckbox(ref Mutable.ShowEmotes, Language.Options_ShowEmotes_Name, Language.Options_ShowEmotes_Desc);
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
ImGui.Spacing();
WordPopupOptions ??= RefillSheet();
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
WordPopupOptions = RefillSheet();
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
using (Plugin.FontManager.FontAwesome.Push())
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
Mutable.BlockedEmotes.Add(newWord);
using(var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (table)
{
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
ImGui.TableHeadersRow();
var copiedList = Mutable.BlockedEmotes.ToArray();
foreach (var word in copiedList)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
ImGui.TableNextColumn();
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
Mutable.BlockedEmotes.Remove(word);
}
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_EmoteStats);
ImGui.Spacing();
if (EmoteCache.State is EmoteCache.LoadingState.Done)
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
else
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (emoteTable)
{
ImGui.TableSetupColumn("##word1");
ImGui.TableSetupColumn("##word2");
ImGui.TableSetupColumn("##word3");
ImGui.TableSetupColumn("##word4");
ImGui.TableSetupColumn("##word5");
foreach (var word in EmoteCache.SortedCodeArray)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
}
}
}
}
}
-97
View File
@@ -1,97 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui.SettingsTabs;
public class Fonts : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Fonts_Tab + "###tabs-fonts";
internal Fonts(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool _)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
ImGui.Spacing();
if (!Mutable.FontsEnabled)
{
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
}
else
{
var globalChooser = ImGuiUtil.FontChooser(Language.Options_Font_Name, Mutable.GlobalFontV2, false, ref _);
globalChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.GlobalFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##global"))
Mutable.GlobalFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
ImGuiUtil.HelpText(string.Format(Language.Options_Font_Description, Plugin.PluginName));
ImGuiUtil.WarningText(Language.Options_Font_Warning);
ImGui.Spacing();
// LocaleNames being null means it is likely a game font which all support JP symbols
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref _, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
japaneseChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.JapaneseFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##japanese"))
Mutable.JapaneseFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f };
ImGuiUtil.HelpText(string.Format(Language.Options_JapaneseFont_Description, Plugin.PluginName));
ImGui.Spacing();
var italicChooser = ImGuiUtil.FontChooser(Language.Options_ItalicFont_Name, Mutable.ItalicFontV2, true, ref Mutable.ItalicEnabled);
italicChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.ItalicFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##italic"))
{
Mutable.ItalicEnabled = false;
Mutable.ItalicFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
}
ImGuiUtil.HelpText(string.Format(Language.Options_Italic_Description, Plugin.PluginName));
ImGui.Spacing();
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
{
ImGuiUtil.HelpText(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName));
var range = (int) Mutable.ExtraGlyphRanges;
foreach (var extra in Enum.GetValues<ExtraGlyphRanges>())
ImGui.CheckboxFlags(extra.Name(), ref range, (int) extra);
Mutable.ExtraGlyphRanges = (ExtraGlyphRanges) range;
}
ImGui.Spacing();
}
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
ImGuiUtil.HelpText(Language.Options_SymbolsFontSize_Description);
ImGui.Spacing();
}
}
+161
View File
@@ -0,0 +1,161 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class General : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => HellionStrings.Settings_Tab_General + "###tabs-general";
internal General(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
public void Draw(bool changed)
{
DrawInputSection();
ImGui.Spacing();
DrawAudioSection();
ImGui.Spacing();
DrawPerformanceSection();
ImGui.Spacing();
DrawLanguageSection();
}
private void DrawInputSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Input_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_KeepInputFocus_Name, ref Mutable.KeepInputFocus);
ImGuiUtil.HelpMarker(Language.Options_KeepInputFocus_Description);
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward);
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
}
}
private void DrawAudioSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Audio_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_PlaySounds_Name, ref Mutable.PlaySounds);
ImGuiUtil.HelpMarker(Language.Options_PlaySounds_Description);
ImGui.Checkbox(Language.Options_ShowNoviceNetwork_Name, ref Mutable.ShowNoviceNetwork);
ImGuiUtil.HelpMarker(Language.Options_ShowNoviceNetwork_Description);
}
}
private void DrawPerformanceSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Performance_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt(Language.Options_MaxLinesToShow_Name, ref Mutable.MaxLinesToRender))
{
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
}
ImGuiUtil.HelpMarker(Language.Options_MaxLinesToShow_Description);
}
}
private void DrawLanguageSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Language_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name()))
{
if (combo.Success)
{
foreach (var language in Enum.GetValues<LanguageOverride>())
{
if (ImGui.Selectable(language.Name()))
{
Mutable.LanguageOverride = language;
}
}
}
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_Language_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name()))
{
if (combo.Success)
{
foreach (var side in Enum.GetValues<CommandHelpSide>())
{
if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side))
{
Mutable.CommandHelpSide = side;
}
}
}
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
{
if (combo.Success)
{
foreach (var mode in Enum.GetValues<KeybindMode>())
{
if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode))
{
Mutable.KeybindMode = mode;
}
if (ImGui.IsItemHovered())
{
ImGuiUtil.Tooltip(mode.Tooltip() ?? "");
}
}
}
}
ImGuiUtil.HelpMarker(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName));
ImGui.Spacing();
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
ImGuiUtil.HelpMarker(Language.Options_SortAutoTranslate_Description);
}
}
}
+188
View File
@@ -0,0 +1,188 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
// Information-Tab vereint die früheren About- und Changelog-Tabs in
// drei kollabierbaren Sektionen. Der About-Inhalt ist 1:1 aus About.cs
// übernommen, die Changelog-Render-Logik aus Changelog.cs.
internal sealed class Information : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => HellionStrings.Settings_Tab_Information + "###tabs-information";
private readonly List<string> Translators =
[
"q673135110", "Akizem", "d0tiKs",
"Moonlight_Everlit", "Dark32", "andreycout",
"Button_", "Cali666", "cassandra308",
"lokinmodar", "jtabox", "AkiraYorumoto",
"MKhayle", "elena.space", "imlisa",
"andrei5125", "ShivaMaheshvara", "aislinn87",
"nishinatsu051", "lichuyuan", "Risu64",
"yummypillow", "witchymary", "Yuzumi",
"zomsakura", "Sirayuki"
];
internal Information(Configuration mutable)
{
Mutable = mutable;
Translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal));
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
DrawVersionInfoSection();
ImGui.Spacing();
DrawAboutSection();
ImGui.Spacing();
DrawChangelogSection();
}
private void DrawVersionInfoSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_VersionInfo_Heading);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Authors);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Interface.Manifest.Author);
ImGui.TextUnformatted(Language.Options_About_Discord);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, "@j.j_kazama");
ImGui.TextUnformatted(Language.Options_About_Version);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Interface.Manifest.AssemblyVersion.ToString(3));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues");
}
}
private void DrawAboutSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_About_Heading);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Maintainer_Heading);
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Body);
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Mission_Heading);
ImGui.TextUnformatted(HellionStrings.About_Mission_P1);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_Mission_P2);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_Mission_P3);
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_BuiltOn_Heading);
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P1);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P2);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_License_Heading);
ImGui.TextUnformatted(HellionStrings.About_License_P1);
ImGui.TextUnformatted(HellionStrings.About_License_P2);
ImGui.TextUnformatted(HellionStrings.About_License_P3);
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.DalamudOrange, HellionStrings.About_SE_Heading);
ImGui.TextUnformatted(HellionStrings.About_SE_P1);
ImGui.TextUnformatted(HellionStrings.About_SE_P2);
ImGui.Spacing();
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Localization_Heading);
ImGui.TextUnformatted(HellionStrings.About_Localization_P1);
ImGui.TextUnformatted(HellionStrings.About_Localization_P2);
ImGui.Spacing();
using (var translatorTree = ImRaii.TreeNode(HellionStrings.About_Translators_TreeNode))
{
if (translatorTree)
{
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
foreach (var translator in Translators)
ImGui.TextUnformatted(translator);
}
}
}
}
private void DrawChangelogSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_Changelog_Heading);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_PrintChangelog_Name, ref Mutable.PrintChangelog);
ImGuiUtil.HelpMarker(Language.Options_PrintChangelog_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
var changelog = Plugin.Interface.Manifest.Changelog;
if (changelog == null)
return;
ImGui.TextUnformatted(Language.Options_Changelog_Header);
ImGui.TextUnformatted($"Version {Plugin.Interface.Manifest.AssemblyVersion.ToString(3)}");
ImGui.Spacing();
foreach (var sentence in changelog.Split("\n"))
{
if (sentence == string.Empty)
{
ImGui.NewLine();
continue;
}
var indented = sentence.StartsWith('-') || sentence.StartsWith(" -");
using var indent = ImRaii.PushIndent(10.0f, true, indented);
ImGui.TextUnformatted(sentence);
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Miscellaneous(Configuration mutable) : ISettingsTab
{
private Configuration Mutable { get; } = mutable;
public string Name => Language.Options_Miscellaneous_Tab + "###tabs-miscellaneous";
public void Draw(bool changed)
{
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name()))
{
if (combo.Success)
{
foreach (var language in Enum.GetValues<LanguageOverride>())
if (ImGui.Selectable(language.Name()))
Mutable.LanguageOverride = language;
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_Language_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name()))
{
if (combo.Success)
{
foreach (var side in Enum.GetValues<CommandHelpSide>())
if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side))
Mutable.CommandHelpSide = side;
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
{
if (combo.Success)
{
foreach (var mode in Enum.GetValues<KeybindMode>())
{
if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode))
Mutable.KeybindMode = mode;
if (ImGui.IsItemHovered())
ImGuiUtil.Tooltip(mode.Tooltip() ?? "");
}
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName));
ImGui.Spacing();
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
ImGuiUtil.HelpText(Language.Options_SortAutoTranslate_Description);
ImGui.Spacing();
}
}
-42
View File
@@ -1,42 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Preview : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => $"{Language.Options_Preview_Tab}###tabs-preview";
internal Preview(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Preview_Name, Mutable.PreviewPosition.Name()))
{
if (combo)
{
foreach (var position in Enum.GetValues<PreviewPosition>())
if (ImGui.Selectable(position.Name(), Mutable.PreviewPosition == position))
Mutable.PreviewPosition = position;
}
}
ImGuiUtil.HelpText(Language.Options_Preview_Description);
ImGui.Spacing();
if (ImGuiUtil.InputIntVertical(Language.Options_PreviewMinimum_Name, Language.Options_PreviewMinimum_Description, ref Mutable.PreviewMinimum))
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.OnlyPreviewIf, Language.Options_PreviewOnlyIf_Name, Language.Options_PreviewOnlyIf_Description);
ImGui.Spacing();
}
}
+165 -102
View File
@@ -3,7 +3,9 @@ using ChatTwo.Export;
using ChatTwo.Privacy; using ChatTwo.Privacy;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@@ -54,8 +56,13 @@ internal sealed class Privacy : ISettingsTab
private long CleanupKeepCount; private long CleanupKeepCount;
private long CleanupDeleteCount; private long CleanupDeleteCount;
private bool CleanupRunning; private bool CleanupRunning;
private bool CleanupPreviewStale;
private HashSet<ChatType>? CleanupPreviewSnapshot;
private bool RetentionRunning; // The retention-running state lives on Plugin so the auto-sweep and
// this manual button see the same flag. UI reads stay lock-free
// because ImGui is single-threaded and bool reads are atomic in .NET.
private bool RetentionRunning => Plugin.RetentionSweepRunning;
// Export form state // Export form state
private int ExportRangeDays = 30; private int ExportRangeDays = 30;
@@ -70,99 +77,7 @@ internal sealed class Privacy : ISettingsTab
Plugin.FirstRunWizard.IsOpen = true; Plugin.FirstRunWizard.IsOpen = true;
ImGui.Spacing(); ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.Theme_Heading); DrawPrivacyFilterSection();
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGuiUtil.OptionCheckbox(
ref Mutable.HellionThemeEnabled,
HellionStrings.Theme_Enabled_Name,
HellionStrings.Theme_Enabled_Description);
using (ImRaii.Disabled(!Mutable.HellionThemeEnabled))
{
ImGui.Spacing();
var opacity = Mutable.HellionThemeWindowOpacity;
if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f"))
Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f);
ImGuiUtil.HelpText(HellionStrings.Theme_WindowOpacity_Help);
}
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref Mutable.UseHellionFont,
HellionStrings.Theme_UseHellionFont_Name,
HellionStrings.Theme_UseHellionFont_Description);
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref Mutable.PrivacyFilterEnabled,
HellionStrings.Privacy_FilterEnabled_Name,
HellionStrings.Privacy_FilterEnabled_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
using (ImRaii.Disabled(!Mutable.PrivacyFilterEnabled))
{
ImGuiUtil.HelpText(HellionStrings.Privacy_Whitelist_Help);
ImGui.Spacing();
if (ImGui.Button(HellionStrings.Privacy_Preset_PrivacyFirst))
Mutable.PrivacyPersistChannels = [..PrivacyDefaults.PrivacyFirstWhitelist];
ImGui.SameLine();
if (ImGui.Button(HellionStrings.Privacy_Preset_ClearAll))
Mutable.PrivacyPersistChannels.Clear();
ImGui.SameLine();
if (ImGui.Button(HellionStrings.Privacy_Preset_SelectAll))
foreach (var group in Groups)
foreach (var t in group.Types)
Mutable.PrivacyPersistChannels.Add(t);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
foreach (var (heading, types) in Groups)
{
using var tree = ImRaii.TreeNode(heading());
if (!tree.Success)
continue;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
foreach (var type in types)
{
var enabled = Mutable.PrivacyPersistChannels.Contains(type);
var label = type.ToString();
if (ImGui.Checkbox($"{label}##privacy-{(int)type}", ref enabled))
{
if (enabled)
Mutable.PrivacyPersistChannels.Add(type);
else
Mutable.PrivacyPersistChannels.Remove(type);
}
}
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref Mutable.PrivacyPersistUnknownChannels,
HellionStrings.Privacy_PersistUnknown_Name,
HellionStrings.Privacy_PersistUnknown_Description);
}
ImGui.Spacing(); ImGui.Spacing();
ImGui.Separator(); ImGui.Separator();
@@ -181,6 +96,109 @@ internal sealed class Privacy : ISettingsTab
ImGui.Spacing(); ImGui.Spacing();
DrawExportSection(); DrawExportSection();
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
DrawAutoTellTabsPreloadSection();
}
private void DrawAutoTellTabsPreloadSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Privacy_AutoTellTabs_Section_Title);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
var preload = Mutable.AutoTellTabsHistoryPreload;
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt(HellionStrings.Privacy_AutoTellTabs_Preload_Name, ref preload, 0, 100))
{
Mutable.AutoTellTabsHistoryPreload = preload;
}
ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description);
ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.Privacy_AutoTellTabs_Preload_Hint);
}
}
private void DrawPrivacyFilterSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Privacy_Filter_Tree_Heading);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGuiUtil.OptionCheckbox(
ref Mutable.PrivacyFilterEnabled,
HellionStrings.Privacy_FilterEnabled_Name,
HellionStrings.Privacy_FilterEnabled_Description);
ImGuiUtil.HelpMarker(HellionStrings.Privacy_FilterEnabled_StorageOnly_Help);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
using (ImRaii.Disabled(!Mutable.PrivacyFilterEnabled))
{
ImGuiUtil.HelpText(HellionStrings.Privacy_Whitelist_Help);
ImGui.Spacing();
if (ImGui.Button(HellionStrings.Privacy_Preset_PrivacyFirst))
Mutable.PrivacyPersistChannels = [..PrivacyDefaults.PrivacyFirstWhitelist];
ImGui.SameLine();
if (ImGui.Button(HellionStrings.Privacy_Preset_ClearAll))
Mutable.PrivacyPersistChannels.Clear();
ImGui.SameLine();
if (ImGui.Button(HellionStrings.Privacy_Preset_SelectAll))
foreach (var group in Groups)
foreach (var t in group.Types)
Mutable.PrivacyPersistChannels.Add(t);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
foreach (var (heading, types) in Groups)
{
using var groupTree = ImRaii.TreeNode(heading());
if (!groupTree.Success)
continue;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
foreach (var type in types)
{
var enabled = Mutable.PrivacyPersistChannels.Contains(type);
var label = type.ToString();
if (ImGui.Checkbox($"{label}##privacy-{(int)type}", ref enabled))
{
if (enabled)
Mutable.PrivacyPersistChannels.Add(type);
else
Mutable.PrivacyPersistChannels.Remove(type);
}
}
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(
ref Mutable.PrivacyPersistUnknownChannels,
HellionStrings.Privacy_PersistUnknown_Name,
HellionStrings.Privacy_PersistUnknown_Description);
}
}
} }
private void DrawExportSection() private void DrawExportSection()
@@ -330,7 +348,7 @@ internal sealed class Privacy : ISettingsTab
var defaultDays = Mutable.RetentionDefaultDays; var defaultDays = Mutable.RetentionDefaultDays;
if (ImGui.InputInt(HellionStrings.Retention_Default_Label, ref defaultDays)) if (ImGui.InputInt(HellionStrings.Retention_Default_Label, ref defaultDays))
Mutable.RetentionDefaultDays = Math.Max(0, defaultDays); Mutable.RetentionDefaultDays = Math.Max(0, defaultDays);
ImGuiUtil.HelpText(HellionStrings.Retention_Default_Help); ImGuiUtil.HelpMarker(HellionStrings.Retention_Default_Help);
ImGui.Spacing(); ImGui.Spacing();
@@ -388,6 +406,9 @@ internal sealed class Privacy : ISettingsTab
ImGui.Spacing(); ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.Retention_Help_SavedNote);
ImGui.Spacing();
using (ImRaii.Disabled(RetentionRunning)) using (ImRaii.Disabled(RetentionRunning))
{ {
if (ImGuiUtil.CtrlShiftButton(HellionStrings.Retention_Apply_Label, HellionStrings.Retention_Apply_Tooltip)) if (ImGuiUtil.CtrlShiftButton(HellionStrings.Retention_Apply_Label, HellionStrings.Retention_Apply_Tooltip))
@@ -408,10 +429,17 @@ internal sealed class Privacy : ISettingsTab
private void StartRetentionRun() private void StartRetentionRun()
{ {
if (RetentionRunning) // Take the shared retention lock so we cannot fight the auto-sweep
return; // for the database connection. If the auto-sweep is already in
// flight we just bail — the user can press the button again once
// it finishes.
lock (Plugin.RetentionSweepLock)
{
if (Plugin.RetentionSweepRunning)
return;
Plugin.RetentionSweepRunning = true;
}
RetentionRunning = true;
var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value); var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value);
var defaultDays = Plugin.Config.RetentionDefaultDays; var defaultDays = Plugin.Config.RetentionDefaultDays;
@@ -443,7 +471,8 @@ internal sealed class Privacy : ISettingsTab
} }
finally finally
{ {
RetentionRunning = false; lock (Plugin.RetentionSweepLock)
Plugin.RetentionSweepRunning = false;
} }
}) { IsBackground = true }.Start(); }) { IsBackground = true }.Start();
} }
@@ -458,6 +487,21 @@ internal sealed class Privacy : ISettingsTab
ImGui.Spacing(); ImGui.Spacing();
// Drift-detection between the snapshot taken at last refresh
// and the current Mutable whitelist. Cleanup itself runs on
// the SAVED policy (Cleanup_Help_SavedNote covers that), but
// the user usually expects "the preview reflects what I just
// ticked" — so we surface the divergence instead of silently
// showing stale numbers.
if (CleanupPreviewSnapshot is not null
&& !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels))
{
CleanupPreviewStale = true;
}
using (var emphasis = CleanupPreviewStale
? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f })
: null)
using (ImRaii.Disabled(CleanupRunning)) using (ImRaii.Disabled(CleanupRunning))
{ {
if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview)) if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview))
@@ -470,10 +514,22 @@ internal sealed class Privacy : ISettingsTab
return; return;
} }
if (CleanupPreviewStale)
{
ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale);
}
ImGui.Spacing(); ImGui.Spacing();
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount)); using (var staleColor = CleanupPreviewStale
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount)); ? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
: null)
{
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
}
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown)) using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
{ {
@@ -529,6 +585,13 @@ internal sealed class Privacy : ISettingsTab
else else
CleanupDeleteCount += count; CleanupDeleteCount += count;
} }
// Snapshot the whitelist as it stood at preview-time so the
// render pass can flag the user about subsequent edits. Only
// updated on success — if the preview throws, the previous
// snapshot stays in place so stale-detection keeps working.
CleanupPreviewSnapshot = new HashSet<ChatType>(Mutable.PrivacyPersistChannels);
CleanupPreviewStale = false;
} }
catch (Exception e) catch (Exception e)
{ {
+5 -2
View File
@@ -10,10 +10,10 @@ namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Tabs : ISettingsTab internal sealed class Tabs : ISettingsTab
{ {
private readonly Plugin Plugin; private Plugin Plugin { get; }
private Configuration Mutable { get; } private Configuration Mutable { get; }
public string Name => Language.Options_Tabs_Tab + "###tabs-tabs"; public string Name => HellionStrings.Settings_Tab_Tabs + "###tabs-tabs";
private int ToOpen = -2; private int ToOpen = -2;
@@ -27,6 +27,9 @@ internal sealed class Tabs : ISettingsTab
{ {
const string addTabPopup = "add-tab-popup"; const string addTabPopup = "add-tab-popup";
ImGuiUtil.HelpText(HellionStrings.Tabs_Presets_Linkshell_Hint);
ImGui.Spacing();
if (ImGuiUtil.IconButton(FontAwesomeIcon.Plus, tooltip: Language.Options_Tabs_Add)) if (ImGuiUtil.IconButton(FontAwesomeIcon.Plus, tooltip: Language.Options_Tabs_Add))
ImGui.OpenPopup(addTabPopup); ImGui.OpenPopup(addTabPopup);
+163
View File
@@ -0,0 +1,163 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Window : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => HellionStrings.Settings_Tab_Window + "###tabs-window";
internal Window(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
public void Draw(bool changed)
{
DrawHideSection();
ImGui.Spacing();
DrawInactivityHideSection();
ImGui.Spacing();
DrawFrameSection();
ImGui.Spacing();
DrawTooltipsSection();
}
private void DrawHideSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_Hide_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_HideChat_Name, ref Mutable.HideChat);
ImGuiUtil.HelpMarker(Language.Options_HideChat_Description);
ImGui.Checkbox(Language.Options_HideDuringCutscenes_Name, ref Mutable.HideDuringCutscenes);
ImGuiUtil.HelpMarker(string.Format(Language.Options_HideDuringCutscenes_Description, Plugin.PluginName));
ImGui.Checkbox(Language.Options_HideWhenNotLoggedIn_Name, ref Mutable.HideWhenNotLoggedIn);
ImGuiUtil.HelpMarker(string.Format(Language.Options_HideWhenNotLoggedIn_Description, Plugin.PluginName));
ImGui.Checkbox(Language.Options_HideWhenUiHidden_Name, ref Mutable.HideWhenUiHidden);
ImGuiUtil.HelpMarker(string.Format(Language.Options_HideWhenUiHidden_Description, Plugin.PluginName));
ImGui.Checkbox(Language.Options_HideInLoadingScreens_Name, ref Mutable.HideInLoadingScreens);
ImGuiUtil.HelpMarker(string.Format(Language.Options_HideInLoadingScreens_Description, Plugin.PluginName));
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
}
}
private void DrawInactivityHideSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_InactivityHide_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_HideWhenInactive_Name, ref Mutable.HideWhenInactive);
ImGuiUtil.HelpMarker(Language.Options_HideWhenInactive_Description);
if (!Mutable.HideWhenInactive)
{
return;
}
ImGuiUtil.InputIntVertical(Language.Options_InactivityHideTimeout_Name, Language.Options_InactivityHideTimeout_Description, ref Mutable.InactivityHideTimeout, 1, 10);
// Untergrenze von 2 Sekunden gegen Selbst-Soft-Lock.
Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout);
using (ImRaii.Disabled(Mutable.HideInBattle))
{
ImGui.Checkbox(Language.Options_InactivityHideActiveDuringBattle_Name, ref Mutable.InactivityHideActiveDuringBattle);
ImGuiUtil.HelpMarker(Language.Options_InactivityHideActiveDuringBattle_Description);
}
using var channelTree = ImRaii.TreeNode(Language.Options_InactivityHideChannels_Name);
if (!channelTree.Success)
{
return;
}
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_All_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = TabsUtil.AllChannels();
Mutable.InactivityHideExtraChatAll = true;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.SameLine();
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_None_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = [];
Mutable.InactivityHideExtraChatAll = false;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.Spacing();
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, Mutable.InactivityHideChannelsV2);
ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels, ref Mutable.InactivityHideExtraChatAll, Mutable.InactivityHideExtraChatChannels);
}
}
private void DrawFrameSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_Frame_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_CanMove_Name, ref Mutable.CanMove);
ImGui.Checkbox(Language.Options_CanResize_Name, ref Mutable.CanResize);
ImGui.Checkbox(Language.Options_ShowTitleBar_Name, ref Mutable.ShowTitleBar);
ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar);
ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton);
ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description);
ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView);
ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
}
}
private void DrawTooltipsSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_Tooltips_Heading);
if (!tree.Success)
{
return;
}
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(Language.Options_NativeItemTooltips_Name, ref Mutable.NativeItemTooltips);
ImGuiUtil.HelpMarker(string.Format(Language.Options_NativeItemTooltips_Description, Plugin.PluginName));
if (Mutable.NativeItemTooltips)
{
ImGuiUtil.DragFloatVertical(Language.Options_TooltipOffset_Name, Language.Options_TooltipOffset_Desc, ref Mutable.TooltipOffset, 1, 0f, 400f, $"{Mutable.TooltipOffset:N0}px", ImGuiSliderFlags.AlwaysClamp);
}
}
}
}
+54
View File
@@ -398,6 +398,60 @@ internal static class ChunkUtil
return builder.ToString(); return builder.ToString();
} }
// Hellion Chat — shared helper for Auto-Tell-Tabs and the MessageStore
// history-preload query. Walks the chunk list once and returns the
// first PlayerPayload it finds, or null when the message has no
// resolved player link (e.g. system messages, GM tells we already
// skipped earlier in the pipeline).
internal static PlayerPayload? TryGetPlayerPayload(IReadOnlyList<Chunk> chunks)
{
foreach (var chunk in chunks)
{
if (chunk.Link is PlayerPayload pp)
{
return pp;
}
}
return null;
}
// Fallback for tells where the PlayerPayload lives in the raw SeString
// payload list rather than on a chunk's Link slot. Same semantics as
// the chunk-walking variant above: returns the first PlayerPayload or
// null if the SeString has none.
internal static PlayerPayload? TryGetPlayerPayload(SeString? seString)
{
if (seString == null)
{
return null;
}
foreach (var payload in seString.Payloads)
{
if (payload is PlayerPayload pp)
{
return pp;
}
}
return null;
}
// True when the message's sender (or, as a fallback, content) carries a
// PlayerPayload that matches the given identity. Used by both the
// Tab.Matches sender filter and the MessageStore tell-history scan.
internal static bool MatchesSender(Message message, string senderName, uint senderWorld)
{
var payload = TryGetPlayerPayload(message.Sender) ?? TryGetPlayerPayload(message.Content);
if (payload == null)
{
return false;
}
if (!string.Equals(payload.PlayerName, senderName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return payload.World.RowId == senderWorld;
}
internal static readonly RawPayload PeriodicRecruitmentLink = new([0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]); internal static readonly RawPayload PeriodicRecruitmentLink = new([0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]);
private static uint GetInteger(BinaryReader input) private static uint GetInteger(BinaryReader input)
+22
View File
@@ -215,6 +215,28 @@ internal static class ImGuiUtil
ImGui.TextUnformatted(text); ImGui.TextUnformatted(text);
} }
// Hellion Chat — compact help affordance: a dimmed "(?)" glyph rendered
// on the same line as the previous item, with the long-form description
// tucked into a hover tooltip. Lets us keep the settings panes scannable
// instead of stacking a wall of HelpText paragraphs under every option.
internal static void HelpMarker(string description)
{
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]))
ImGui.TextUnformatted("(?)");
// AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei
// ausgegrauten Settings false, der User könnte nicht mehr lesen
// warum eine Option nicht aktiv ist. Genau dann braucht er den
// Hover-Tooltip aber am dringendsten.
if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
return;
using var tooltip = ImRaii.Tooltip();
using (ImRaii.TextWrapPos(35.0f * ImGui.GetFontSize()))
ImGui.TextUnformatted(description);
}
internal static void WarningText(string text, bool wrap = true) internal static void WarningText(string text, bool wrap = true)
{ {
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent(); var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
+104 -29
View File
@@ -14,21 +14,20 @@ public static class TabsUtil
return channels; return channels;
} }
// Hellion-tuned General preset. The pure player-talk catch-all plus
// the active-gameplay event streams (loot, crafting, gathering, NPC
// dialogue, party-finder pings). Pure technical noise (System, Error,
// Login/Logout spam, retainer sales, alarms, sign messages) lives in
// the dedicated System tab so it doesn't bury actual conversation.
public static Tab VanillaGeneral => new() public static Tab VanillaGeneral => new()
{ {
Name = Language.Tabs_Presets_General, Name = Language.Tabs_Presets_General,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{ {
// Special // Player chat
[ChatType.Debug] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Urgent] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Notice] = (ChatSourceExt.All, ChatSourceExt.All),
// Chat
[ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Yell] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Yell] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Shout] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Shout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Party] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Party] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossParty] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.CrossParty] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Alliance] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Alliance] = (ChatSourceExt.All, ChatSourceExt.All),
@@ -50,33 +49,13 @@ public static class TabsUtil
[ChatType.Linkshell6] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Linkshell6] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell7] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Linkshell7] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell8] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Linkshell8] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NoviceNetwork] = (ChatSourceExt.All, ChatSourceExt.All), // Active-gameplay events
[ChatType.StandardEmote] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.NpcDialogue] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CustomEmote] = (ChatSourceExt.All, ChatSourceExt.All),
// Announcements
[ChatType.System] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.GatheringSystem] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Error] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Echo] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NoviceNetworkSystem] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.FreeCompanyAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeamAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.FreeCompanyLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeamLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.RetainerSale] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NpcAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Progress] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Crafting] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Crafting] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Gathering] = (ChatSource.LocalPlayer, ChatSource.LocalPlayer), [ChatType.Gathering] = (ChatSource.LocalPlayer, ChatSource.LocalPlayer),
[ChatType.PeriodicRecruitmentNotification] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.PeriodicRecruitmentNotification] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Sign] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.RandomNumber] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Orchestrion] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.MessageBook] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Alarm] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.GlamourNotifications] = (ChatSourceExt.All, ChatSourceExt.All),
} }
}; };
@@ -98,6 +77,102 @@ public static class TabsUtil
AllSenderMessages = true, AllSenderMessages = true,
}; };
// Hellion default-tab presets used by the v10 wipe migration. Names are
// kept in HellionStrings (EN+DE) instead of Language.* so the upstream
// resource files stay untouched. Channel selections cover the channels
// a typical Eorzea raider uses without forcing the user to hand-tick
// each box on first start.
public static Tab HellionFreeCompany => new()
{
Name = HellionStrings.Tabs_Presets_FreeCompany,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.FreeCompany] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.FreeCompanyAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.FreeCompanyLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
},
Channel = InputChannel.FreeCompany,
};
public static Tab HellionParty => new()
{
Name = HellionStrings.Tabs_Presets_Party,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.Party] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossParty] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Alliance] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeam] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeamAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeamLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All),
},
Channel = InputChannel.Party,
};
public static Tab HellionBeginner => new()
{
Name = HellionStrings.Tabs_Presets_Beginner,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.NoviceNetwork] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NoviceNetworkSystem] = (ChatSourceExt.All, ChatSourceExt.All),
},
Channel = InputChannel.NoviceNetwork,
};
public static Tab HellionSystem => new()
{
Name = HellionStrings.Tabs_Presets_System,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.Debug] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Urgent] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Notice] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.System] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Error] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Echo] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.GatheringSystem] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NoviceNetworkSystem] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.NpcAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.FreeCompanyLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.PvpTeamLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.RetainerSale] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Progress] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.RandomNumber] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Orchestrion] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.MessageBook] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Alarm] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Sign] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.GlamourNotifications] = (ChatSourceExt.All, ChatSourceExt.All),
},
};
public static Tab HellionLinkshell => new()
{
Name = HellionStrings.Tabs_Presets_Linkshell,
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
{
[ChatType.Linkshell1] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell2] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell3] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell4] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell5] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell6] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell7] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.Linkshell8] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell1] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell2] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell3] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell4] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell5] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell6] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell7] = (ChatSourceExt.All, ChatSourceExt.All),
[ChatType.CrossLinkshell8] = (ChatSourceExt.All, ChatSourceExt.All),
},
};
public static Dictionary<ChatType, (ChatSource, ChatSource)> MostlyPlayer => new() public static Dictionary<ChatType, (ChatSource, ChatSource)> MostlyPlayer => new()
{ {
// Special // Special
+8 -6
View File
@@ -1,6 +1,6 @@
# Hellion Chat # Hellion Chat
**Version 0.2.0** — DSGVO-bewusste Erweiterung von [Chat 2](https://github.com/Infiziert90/ChatTwo) für FINAL FANTASY XIV / Dalamud. **Version 0.3.1** — DSGVO-bewusste Erweiterung von [Chat 2](https://github.com/Infiziert90/ChatTwo) für FINAL FANTASY XIV / Dalamud.
Hellion Chat baut auf Chat 2 auf und ergänzt es um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle Chat-2-Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank. Hellion Chat baut auf Chat 2 auf und ergänzt es um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle Chat-2-Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
@@ -56,7 +56,7 @@ Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo währen
### Was gegenüber Chat 2 fehlt ### Was gegenüber Chat 2 fehlt
- **Webinterface** wurde in Hellion Chat 0.2.0 entfernt. Der eingebaute HTTP-Server hat unter dem Privacy-Versprechen nicht abgesichert werden können (5-stelliger numerischer Auth-Code aus `System.Random`, Bind auf alle Interfaces per Default, Cookies ohne Security-Flags und ein Server-Sent-Events-Stream der den Privacy-Filter umgangen hat). Wer den Funktionsumfang von Chat 2 vollständig braucht, sollte beim Upstream-Plugin bleiben; Hellion Chat fokussiert auf DSGVO-konforme Persistenz und verzichtet bewusst auf Remote-Zugriffs-Features. - **Webinterface** wurde in Hellion Chat 0.2.0 entfernt. Es bedient einen anderen Anwendungsfall als der Fokus dieses Forks, nämlich Remote-Zugriff auf den Chat von einem zweiten Gerät. An die kleineren Defaults dieses Forks anzupassen hätte einen erheblichen Umbau bedeutet, also ist es ersatzlos entfernt worden. Wer den vollen Funktionsumfang von Chat 2 chte, ist mit dem Upstream-Plugin besser bedient. Hellion Chat fokussiert sich auf einen schmaleren Datenbestand und verzichtet bewusst auf Remote-Zugriffs-Features.
--- ---
@@ -231,7 +231,7 @@ Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig
## Projektstatus ## Projektstatus
**Version 0.2.0** | Stand: Mai 2026 **Version 0.3.1** | Stand: Mai 2026
Alle Bootstrap-Phasen abgeschlossen: Alle Bootstrap-Phasen abgeschlossen:
@@ -244,11 +244,13 @@ Alle Bootstrap-Phasen abgeschlossen:
- [x] About-Tab im Hellion-Branding mit License + Disclaimer - [x] About-Tab im Hellion-Branding mit License + Disclaimer
- [x] AI-Disclosure dokumentiert (Pair-Klassifikation) - [x] AI-Disclosure dokumentiert (Pair-Klassifikation)
- [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02) - [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02)
- [x] Audit-Hardening Phase 2 (Path-Traversal, Retention-Race, DbViewer-Konsistenz, Privacy-Filter-Help-Text)
- [x] Slash-Commands auf `/hellion`-Familie umbenannt
- [x] Theme auf Hellion-Online-Media-Brand-Palette aligned (Arctic Cyan + Ember Orange)
- [x] About-Tab vollständig lokalisiert (EN + DE) mit Mission-Statement und neutraler Tonart
Phase 2 (offen, kein festes Datum): Phase 3 (offen, kein festes Datum):
- [ ] EmoteCache Path-Traversal-Hardening (`Path.GetFullPath` + StartsWith-Check)
- [ ] Race-Hardening für `RetentionLastRunAt` (CompareExchange / Lock)
- [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung - [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung
- [ ] PostgreSQL-Backend - [ ] PostgreSQL-Backend
- [ ] Encryption für sensible Channels (AES-256, lokaler Key) - [ ] Encryption für sensible Channels (AES-256, lokaler Key)
+7 -7
View File
File diff suppressed because one or more lines are too long