Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df5819a88 | |||
| 3fbbe8543f | |||
| 03dfb8e3da | |||
| a987e97610 | |||
| ecd46ed630 | |||
| f2f7599f81 | |||
| ac158907ea | |||
| 9506af49db | |||
| c882eac1ca | |||
| 7a6b44048a | |||
| 0d39d59a04 | |||
| f0e0db55e3 | |||
| f207239d56 | |||
| ccf2ec9f12 | |||
| aff7a5e7ce | |||
| cd84ca2b3f | |||
| 7c645afa1d | |||
| 24c1e0e754 | |||
| 9f6a0807d1 | |||
| 15f83c8b0e | |||
| c7253bdf02 | |||
| cf10c566dd | |||
| acfe838bc6 | |||
| 9e1f559644 | |||
| 2c79a67dae | |||
| 1687271bfd | |||
| cb5457ba2e | |||
| a701f6c103 | |||
| 8cad8651d2 | |||
| 61b547606c | |||
| 059cfa6e28 | |||
| 71d84e4486 | |||
| 92301869ed | |||
| c3d06a9c94 | |||
| 911c870e24 | |||
| 8cda19d993 | |||
| 62621ba855 | |||
| 497c259031 | |||
| 9ad9d2acd2 | |||
| 1b63765caa | |||
| 61764459ed | |||
| 1b7f2c40e6 | |||
| 93d52ae819 | |||
| 48b3d5c6b1 | |||
| e9a9d8a01c | |||
| a155a57f33 | |||
| 90b83a0690 | |||
| f10301c3e4 | |||
| 8571a936a4 | |||
| 3f6144836c | |||
| 53c432a635 | |||
| 340cadf3b9 | |||
| 8d6868aef6 | |||
| 6e8fcc8cc3 | |||
| 57670ffc76 | |||
| 2144eedd76 | |||
| 43daef83de | |||
| 4a9ad426e7 | |||
| 13beda3a8d | |||
| 18c05af4db | |||
| df6e1e1cbd | |||
| 01b1a14511 | |||
| b6af8d559c | |||
| 22dbfc2e24 | |||
| 2f3b01732c | |||
| 88803382dd | |||
| 74c51163c7 | |||
| 877ff4ba18 | |||
| ad2feb5a27 | |||
| 46b63ffdd1 | |||
| 4ba5004322 | |||
| 3584c94523 | |||
| 303729f3d3 | |||
| 12085ff1e2 | |||
| e4593a0fda | |||
| 3fc42963ae | |||
| 7c52e890e6 | |||
| 4d977d5118 | |||
| ddd72a878e | |||
| 66450dd518 | |||
| 7de28ef9b2 | |||
| da3c1f6832 | |||
| e66ae1f5b4 | |||
| 281a1e172f | |||
| 45a5035426 | |||
| e1931fc7d2 | |||
| 2201478a54 | |||
| 50963ccf1b | |||
| fde85e6d69 | |||
| c22b169b73 | |||
| 6839ccaf34 | |||
| fa108c2271 | |||
| 395a0d7c98 | |||
| b76bfb3cfc | |||
| 0512e4729c | |||
| 654f24c609 | |||
| 0e2a14197c | |||
| 52e163a472 | |||
| e086afe2a8 | |||
| c97ce7543b | |||
| cca4571470 | |||
| 444d7f8e2e | |||
| 71ae95d79c | |||
| 9a38f7f094 | |||
| c33e519bb9 | |||
| 14e585ef63 | |||
| d4aa3971c5 | |||
| e9ec587e3b | |||
| 39cd7ab801 | |||
| bb6259e14d | |||
| 757370dd53 | |||
| 3f35b76c54 | |||
| 74bdc4f927 | |||
| eb379d84ef | |||
| 7add74dbbe | |||
| e91c7a3888 | |||
| f8b0804321 | |||
| a9d4e9bd69 | |||
| 7e3e4c8b72 | |||
| 397c84be2c | |||
| 269708150d | |||
| a2977ef75b | |||
| baa4d011e8 | |||
| 4810e8b518 | |||
| 133f5c536f | |||
| 92bb368d2b | |||
| 07f47f32e3 | |||
| 141fcbf074 | |||
| 32c410e8e2 | |||
| 824037e55f | |||
| 173cb76bea |
@@ -0,0 +1,13 @@
|
|||||||
|
# HellionChat is a hobby project and does not solicit funding.
|
||||||
|
#
|
||||||
|
# If you want to support the work that made HellionChat possible,
|
||||||
|
# please consider supporting the upstream Chat 2 maintainers:
|
||||||
|
#
|
||||||
|
# Infiziert90 (Infi): https://ko-fi.com/infiii
|
||||||
|
# Anna Clemens: https://ko-fi.com/lojewalo
|
||||||
|
#
|
||||||
|
# Both Ko-fi pages are also linked in the plugin's settings panel.
|
||||||
|
|
||||||
|
# No platforms enabled — keep this file present so GitHub recognises
|
||||||
|
# the project as having considered funding without showing a Sponsor
|
||||||
|
# button on the repository page.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Something in HellionChat is broken or behaves wrong
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for reporting. Please fill in the fields below so I can
|
||||||
|
reproduce the issue. If this is a security issue, stop here and
|
||||||
|
use the [private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||||
|
instead.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: HellionChat version
|
||||||
|
description: From Settings → Information → Version
|
||||||
|
placeholder: "0.5.4"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
options:
|
||||||
|
- Windows (XIVLauncher)
|
||||||
|
- Linux (XIVLauncher Core)
|
||||||
|
- macOS (XIVLauncher Core / wine)
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
description: Plain description, no log dumps yet
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What you expected
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce
|
||||||
|
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Relevant /xllog excerpt
|
||||||
|
description: Filter for "HellionChat" or "ChatTwo" if the log is huge
|
||||||
|
render: text
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I am running the latest version of HellionChat
|
||||||
|
required: true
|
||||||
|
- label: I have searched existing issues for duplicates
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Security vulnerability
|
||||||
|
url: https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
||||||
|
about: Do not open a public issue for security problems. Use the private advisory instead.
|
||||||
|
|
||||||
|
- name: Upstream Chat 2 issue
|
||||||
|
url: https://github.com/Infiziert90/ChatTwo/issues
|
||||||
|
about: If the issue exists in upstream Chat 2 too, please report it there so the original maintainers see it as well.
|
||||||
|
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.com/users/j.j_kazama
|
||||||
|
about: Quick questions, casual feedback. Bug reports still go through the issue tracker for tracking.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest a feature or enhancement for HellionChat
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for the suggestion. HellionChat focuses on privacy by
|
||||||
|
default and a small, well-scoped feature set. Suggestions that
|
||||||
|
align with that scope are easier to accept than ones that pull
|
||||||
|
the plugin toward "do everything".
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What problem are you trying to solve
|
||||||
|
description: The user-side problem, not the proposed solution yet
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: What you would like HellionChat to do
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives you have considered
|
||||||
|
description: Other plugins, manual workarounds, settings combinations
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: scope
|
||||||
|
attributes:
|
||||||
|
label: Scope estimate from your side
|
||||||
|
options:
|
||||||
|
- "Small (one tab, one toggle, one filter)"
|
||||||
|
- "Medium (a settings section, persistent state, one new file)"
|
||||||
|
- "Large (architectural, touches the message pipeline or the database)"
|
||||||
|
- "I don't know"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues for similar requests
|
||||||
|
required: true
|
||||||
|
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to HellionChat. Please fill in the sections
|
||||||
|
below so the review goes quickly. Delete sections that genuinely do
|
||||||
|
not apply, but do not delete the whole template.
|
||||||
|
|
||||||
|
If this is a security fix, stop here and use a private security
|
||||||
|
advisory instead:
|
||||||
|
https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- One or two sentences. What does this PR change and why. -->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!-- Tick all that apply. -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds behaviour)
|
||||||
|
- [ ] Breaking change (config migration, removed feature, or behaviour
|
||||||
|
change that user-visible defaults rely on)
|
||||||
|
- [ ] Documentation only
|
||||||
|
- [ ] Translation update
|
||||||
|
- [ ] Build, CI or tooling change
|
||||||
|
- [ ] Upstream cherry-pick from Chat 2
|
||||||
|
|
||||||
|
## Linked issue
|
||||||
|
|
||||||
|
<!-- e.g. "Closes #42" or "Refs #42". For trivial typo fixes, "n/a". -->
|
||||||
|
|
||||||
|
## How I tested this
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Built locally with `dotnet build -c Release`
|
||||||
|
- Ran `dotnet test`
|
||||||
|
- Loaded the plugin in-game on Windows / Linux / macOS via XIVLauncher
|
||||||
|
- Specific scenarios I exercised in-game
|
||||||
|
-->
|
||||||
|
|
||||||
|
## User-visible changes
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Anything the end user will notice. New settings, changed defaults,
|
||||||
|
new commands, new translations, removed behaviour. If none, write
|
||||||
|
"none".
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Compatibility notes
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Does this require a configuration migration? If yes, which version
|
||||||
|
bump and is it covered by the existing migration tests?
|
||||||
|
- Does this change the schema in MessageStore?
|
||||||
|
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
||||||
|
- Does this affect the upstream cherry-pick path? See UPSTREAM_SYNC.md.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
||||||
|
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
||||||
|
- [ ] My change matches the existing code style (`.editorconfig`).
|
||||||
|
- [ ] I added or updated tests where the existing test infrastructure
|
||||||
|
made that practical, or I have explained why tests are not
|
||||||
|
applicable.
|
||||||
|
- [ ] I updated the README, in-plugin strings or documentation if my
|
||||||
|
change is user-visible.
|
||||||
|
- [ ] I did not include any AI-generated code without disclosing it
|
||||||
|
in the PR description (see [AI_DISCLOSURE.md](../AI_DISCLOSURE.md)).
|
||||||
|
- [ ] I confirm my contribution is released under the
|
||||||
|
[EUPL-1.2](../LICENSE).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# NuGet package updates for the plugin project. Weekly cadence keeps the
|
||||||
|
# noise down while still catching transitive security advisories within
|
||||||
|
# a few days of disclosure.
|
||||||
|
- package-ecosystem: nuget
|
||||||
|
directory: /ChatTwo
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: "07:00"
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
- nuget
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps)"
|
||||||
|
groups:
|
||||||
|
patches:
|
||||||
|
update-types:
|
||||||
|
- patch
|
||||||
|
minor:
|
||||||
|
update-types:
|
||||||
|
- minor
|
||||||
|
|
||||||
|
# GitHub Actions versions in .github/workflows. Lower cadence because
|
||||||
|
# Action releases ship less frequently and are usually safe to defer
|
||||||
|
# for a month.
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
time: "07:00"
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
- github-actions
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(actions)"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
This release is distributed via the HellionChat custom repository, not the
|
||||||
|
Dalamud main plugin repo. To install:
|
||||||
|
|
||||||
|
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||||
|
2. Add the URL:
|
||||||
|
`https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json`
|
||||||
|
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
||||||
|
|
||||||
|
## Project documents
|
||||||
|
|
||||||
|
- [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build
|
||||||
|
- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends
|
||||||
|
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/THIRD_PARTY_NOTICES.md) — dependencies and licences
|
||||||
|
- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting
|
||||||
|
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
[EUPL-1.2](https://github.com/JonKazama-Hellion/HellionChat/blob/main/LICENSE).
|
||||||
|
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna,
|
||||||
|
also EUPL-1.2.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
# Verifies that every push to main and every PR still builds against the
|
||||||
|
# current Dalamud staging branch. Does not produce release artefacts; the
|
||||||
|
# release workflow handles that on tag.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Minimum permissions for a build-only workflow: read the repo, nothing
|
||||||
|
# else. Closes the CodeQL "Workflow does not contain permissions" alert
|
||||||
|
# and matches the principle-of-least-privilege the security guide
|
||||||
|
# recommends for workflows that don't push or create releases.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (Release)
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Download Dalamud staging
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||||
|
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||||
|
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||||
|
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore ChatTwo/ChatTwo.csproj
|
||||||
|
|
||||||
|
- name: Build (Release)
|
||||||
|
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Upload build output
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: HellionChat-build-${{ github.run_number }}
|
||||||
|
path: ChatTwo/bin/Release/**/HellionChat/**
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 14
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
# Replaces the GitHub default-setup CodeQL scan. The default setup runs
|
||||||
|
# without resolving the Dalamud assemblies (they live in a user-AppData
|
||||||
|
# path) and reports "Low C# analysis quality" because call-target
|
||||||
|
# resolution sits at ~64%. This workflow downloads the Dalamud staging
|
||||||
|
# distribution before the build, runs a manual dotnet build, and then
|
||||||
|
# lets CodeQL analyse the fully-resolved compilation. Quality climbs
|
||||||
|
# back above the 85% thresholds.
|
||||||
|
#
|
||||||
|
# This workflow only consumes trusted inputs: the tag/branch ref via
|
||||||
|
# the standard checkout action, and the Dalamud distribution URL which
|
||||||
|
# is pinned to a goatcorp-controlled GitHub Pages target. No user-
|
||||||
|
# controlled event payload (issue title, PR body, commit message) flows
|
||||||
|
# into a run-step.
|
||||||
|
#
|
||||||
|
# Disable the default setup in the repo before this workflow lands:
|
||||||
|
# Settings -> Code security -> Code scanning -> "CodeQL analysis" tile
|
||||||
|
# -> Switch to advanced.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: '17 6 * * 1'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze-csharp:
|
||||||
|
name: Analyze (csharp)
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Download Dalamud staging
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||||
|
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||||
|
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||||
|
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: csharp
|
||||||
|
build-mode: manual
|
||||||
|
queries: security-extended
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore ChatTwo/ChatTwo.csproj
|
||||||
|
|
||||||
|
- name: Build (Release)
|
||||||
|
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Perform CodeQL analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: /language:csharp
|
||||||
|
|
||||||
|
analyze-actions:
|
||||||
|
name: Analyze (actions)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: actions
|
||||||
|
build-mode: none
|
||||||
|
|
||||||
|
- name: Perform CodeQL analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: /language:actions
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
|
||||||
|
# current Dalamud staging branch, locates the latest.zip produced by
|
||||||
|
# DalamudPackager and attaches it to the matching GitHub Release.
|
||||||
|
#
|
||||||
|
# User-controlled inputs touched by this workflow:
|
||||||
|
# - the tag name (filtered by on.tags = v*, validated again at runtime
|
||||||
|
# against ^v\d+\.\d+\.\d+$ before being used in any string)
|
||||||
|
# All other values are either repo-controlled (paths under
|
||||||
|
# ChatTwo/bin/Release derived from Get-ChildItem) or pinned URLs to
|
||||||
|
# goatcorp / GitHub. Nothing from a webhook event payload (issue/PR
|
||||||
|
# titles, commit messages, etc.) flows into a run-step.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||||
|
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||||
|
# The tag input is validated against the same semver regex as the
|
||||||
|
# auto-trigger before any string interpolation happens.
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Existing tag to (re)release, e.g. v0.6.1'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and attach release ZIP
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||||
|
# On workflow_dispatch, ref defaults to the branch the action was
|
||||||
|
# invoked from; we need to explicitly check out the tag the user
|
||||||
|
# supplied so the build comes from the tagged commit, not main.
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Download Dalamud staging
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||||
|
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||||
|
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||||
|
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||||
|
|
||||||
|
- name: Build (Release)
|
||||||
|
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release
|
||||||
|
|
||||||
|
- name: Locate latest.zip
|
||||||
|
id: locate
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$zip = Get-ChildItem -Path ChatTwo\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
|
||||||
|
if (-not $zip)
|
||||||
|
{
|
||||||
|
throw "latest.zip not found under ChatTwo\bin\Release"
|
||||||
|
}
|
||||||
|
Write-Host "Found: $($zip.FullName)"
|
||||||
|
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
|
|
||||||
|
# Build a release body from the matching changelog block in
|
||||||
|
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||||
|
# workflow if no block exists for the tagged version, which is the
|
||||||
|
# automated counterpart to the "yaml + repo.json + release body
|
||||||
|
# kept in sync" rule.
|
||||||
|
#
|
||||||
|
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
||||||
|
# tag value is treated as a PowerShell variable, not as inline shell
|
||||||
|
# text. The strict regex below rejects anything that is not a clean
|
||||||
|
# semver tag before it is used to build a string.
|
||||||
|
- name: Generate release body
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||||
|
# push:tags carries it in github.ref_name. Either way the value
|
||||||
|
# is treated as a PowerShell variable (env-var pass), not as
|
||||||
|
# inline shell text, and validated against the semver regex
|
||||||
|
# below before any string interpolation.
|
||||||
|
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
$tag = $env:TAG_NAME
|
||||||
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
|
throw "Refusing to generate release body for non-semver tag: $tag"
|
||||||
|
}
|
||||||
|
$version = $tag.Substring(1)
|
||||||
|
|
||||||
|
$yamlPath = "ChatTwo/HellionChat.yaml"
|
||||||
|
$raw = Get-Content -Path $yamlPath -Raw
|
||||||
|
|
||||||
|
$marker = "changelog: |-"
|
||||||
|
$idx = $raw.IndexOf($marker)
|
||||||
|
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
||||||
|
|
||||||
|
# changelog: is the last top-level key in the manifest, so
|
||||||
|
# everything after the marker is the literal block. Strip the
|
||||||
|
# 2-space yaml indent from each line.
|
||||||
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
|
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||||
|
}) -join "`n"
|
||||||
|
|
||||||
|
$header = "**Hellion Chat $version"
|
||||||
|
$start = $changelogBody.IndexOf($header)
|
||||||
|
if ($start -lt 0) {
|
||||||
|
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||||
|
}
|
||||||
|
|
||||||
|
$rest = $changelogBody.Substring($start)
|
||||||
|
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||||
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
|
||||||
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
|
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
|
} elseif ($trailer -ge 0) {
|
||||||
|
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||||
|
} else {
|
||||||
|
$currentBlock = $rest.TrimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static install / docs / licence footer is maintained as a
|
||||||
|
# separate file so the workflow YAML stays clean (no embedded
|
||||||
|
# heredoc that would have to be indented under the run-block).
|
||||||
|
$footerPath = ".github/release-footer.md"
|
||||||
|
if (-not (Test-Path $footerPath)) {
|
||||||
|
throw "Release footer template not found: $footerPath"
|
||||||
|
}
|
||||||
|
$footer = Get-Content -Path $footerPath -Raw
|
||||||
|
|
||||||
|
$body = $currentBlock + "`n" + $footer
|
||||||
|
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
||||||
|
|
||||||
|
Write-Host "Generated release body for $tag :"
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host $body
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
|
||||||
|
- name: Attach to GitHub release
|
||||||
|
uses: softprops/action-gh-release@v3
|
||||||
|
with:
|
||||||
|
# Explicit tag_name so the action targets the correct release in
|
||||||
|
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||||
|
# modes. Without this, dispatch runs would default to the branch
|
||||||
|
# ref (main) and fail to find the release.
|
||||||
|
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
files: ${{ steps.locate.outputs.path }}
|
||||||
|
body_path: release-body.md
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: false
|
||||||
@@ -372,6 +372,11 @@ MigrationBackup/
|
|||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
#Specs und Plan datein
|
||||||
|
/.superpowers/
|
||||||
|
|
||||||
|
#Test Datein
|
||||||
|
ChatTwo.Tests
|
||||||
TestResults
|
TestResults
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
+56
-21
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Code of conduct
|
||||||
|
|
||||||
|
HellionChat is a small hobby project. The contributor base is tiny and
|
||||||
|
the moderation overhead I can afford is equally small, so this document
|
||||||
|
is short and direct.
|
||||||
|
|
||||||
|
## What I expect from contributors
|
||||||
|
|
||||||
|
- Be respectful in issues, pull requests, discussions and any other
|
||||||
|
project space (Discord, email).
|
||||||
|
- Keep feedback focused on the code, the design or the documentation.
|
||||||
|
Critique the work, not the person.
|
||||||
|
- Assume good intent. People come from different backgrounds, time
|
||||||
|
zones and skill levels. A clarifying question is almost always a
|
||||||
|
better first move than an accusation.
|
||||||
|
- Stay on topic. This project is about a Dalamud chat plugin. Off-topic
|
||||||
|
arguments belong elsewhere.
|
||||||
|
- Respect that I maintain this in my spare time. Replies can take a
|
||||||
|
few days. Please do not escalate just because a thread is quiet.
|
||||||
|
|
||||||
|
## What is not welcome
|
||||||
|
|
||||||
|
- Personal attacks, slurs, doxxing, sustained disruption of threads.
|
||||||
|
- Unsolicited private contact after I have asked someone to stop.
|
||||||
|
- Sharing of private conversations without consent.
|
||||||
|
- Any content that would put other contributors or end users at risk.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This applies to every space the project owns or that I run on its
|
||||||
|
behalf: the GitHub repository, GitHub Discussions, project-related
|
||||||
|
Discord conversations and the maintainer email address listed in
|
||||||
|
`SECURITY.md`.
|
||||||
|
|
||||||
|
It also applies when someone is identifiably representing the project
|
||||||
|
in another space, for example posting as a HellionChat maintainer in
|
||||||
|
the Dalamud Discord.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
If something here is being broken, contact me directly. Do not open a
|
||||||
|
public issue.
|
||||||
|
|
||||||
|
- Email: `kontakt@hellion-media.de`
|
||||||
|
- Discord DM: `@j.j_kazama`
|
||||||
|
|
||||||
|
Reports stay private. I will acknowledge within a few weekdays
|
||||||
|
(European business hours) and tell you what I plan to do.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
I am the sole maintainer, so enforcement is a single-person process.
|
||||||
|
Depending on what happened and how the person responds, I will pick
|
||||||
|
the lightest measure that resolves the issue:
|
||||||
|
|
||||||
|
1. Private note asking the behaviour to stop.
|
||||||
|
2. Public correction in the affected thread.
|
||||||
|
3. Edit or removal of the offending content.
|
||||||
|
4. Temporary block from the repository or related spaces.
|
||||||
|
5. Permanent block.
|
||||||
|
|
||||||
|
Severe cases skip the lower steps. I will not negotiate over
|
||||||
|
harassment or threats.
|
||||||
|
|
||||||
|
## Acknowledgement
|
||||||
|
|
||||||
|
This document is intentionally short and project-specific rather than
|
||||||
|
a copy of a longer template. If you need a more formal reference, the
|
||||||
|
[Contributor Covenant](https://www.contributor-covenant.org/) is a
|
||||||
|
widely adopted starting point and the spirit of this document is
|
||||||
|
compatible with it.
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributing to HellionChat
|
||||||
|
|
||||||
|
Thanks for taking a look. HellionChat is a small, opinionated fork of
|
||||||
|
[Chat 2](https://github.com/Infiziert90/ChatTwo) maintained by one
|
||||||
|
person in spare time. This document explains what I am looking for,
|
||||||
|
what I am not, and how to make a contribution land smoothly.
|
||||||
|
|
||||||
|
## Before you open anything
|
||||||
|
|
||||||
|
- Read the [README](README.md) so you understand the scope: this is a
|
||||||
|
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
|
||||||
|
removes the upstream webinterface and ships smaller defaults.
|
||||||
|
- Read [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md). Cherry-picks from upstream
|
||||||
|
Chat 2 are selective and conscious; not everything that lands there
|
||||||
|
belongs here.
|
||||||
|
- Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes
|
||||||
|
through a private advisory, never a public issue or PR.
|
||||||
|
- Read the [code of conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
## What I will accept
|
||||||
|
|
||||||
|
- Bug fixes for behaviour documented in the README, the in-plugin
|
||||||
|
settings or the changelog.
|
||||||
|
- Translation contributions for Hellion-specific strings via direct
|
||||||
|
pull requests against `ChatTwo/Resources/HellionStrings.*.resx`.
|
||||||
|
Translations for the upstream Chat 2 strings (`Language.*.resx`) are
|
||||||
|
not handled here; they go through the upstream Chat 2 project.
|
||||||
|
- Documentation improvements (README, comments, this file).
|
||||||
|
- Performance fixes with a measurable before/after.
|
||||||
|
- New features that fit the privacy-first scope and do not duplicate
|
||||||
|
what an existing Dalamud plugin already does well.
|
||||||
|
|
||||||
|
## What I will probably decline
|
||||||
|
|
||||||
|
- Re-introducing the webinterface or any remote-access feature. It was
|
||||||
|
removed in v0.2.0 on purpose. See README "Was gegenüber Chat 2 fehlt".
|
||||||
|
- Features that bypass the privacy filter or weaken the default
|
||||||
|
retention behaviour without an explicit, documented opt-in.
|
||||||
|
- Sweeping refactors that touch large parts of the upstream codebase.
|
||||||
|
They make selective upstream cherry-picks much harder and the
|
||||||
|
maintenance cost outweighs the benefit for a one-person project.
|
||||||
|
- AI-generated code dropped in without disclosure or human review. See
|
||||||
|
[AI_DISCLOSURE.md](AI_DISCLOSURE.md) for how I handle AI assistance
|
||||||
|
on my side; I expect comparable transparency from contributors.
|
||||||
|
|
||||||
|
If you are unsure whether an idea fits, open a feature-request issue
|
||||||
|
first and ask before writing code. I would rather say "no" to a
|
||||||
|
proposal than to a finished pull request.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Open an issue (bug or feature request) using the templates under
|
||||||
|
`.github/ISSUE_TEMPLATE/`. Skip this step only for trivial typos.
|
||||||
|
2. Fork the repository and branch off `main`. Branch naming is
|
||||||
|
informal; something like `fix/auto-tell-history-empty` or
|
||||||
|
`feat/adblock-light-mode` is plenty.
|
||||||
|
3. Match the existing code style. The repository ships an
|
||||||
|
`.editorconfig` that VS Code and Rider pick up automatically.
|
||||||
|
4. Keep commits focused. Several small commits with clear messages are
|
||||||
|
easier to review than one big one. Squash-on-merge happens at the
|
||||||
|
PR level if needed.
|
||||||
|
5. If your change touches user-visible behaviour, update the README
|
||||||
|
and/or the changelog block in `ChatTwo/HellionChat.yaml` and
|
||||||
|
`repo.json` for the next version. I bump the version number myself
|
||||||
|
at release time, so you do not need to.
|
||||||
|
6. Open the pull request against `main`. The PR template will ask
|
||||||
|
you to summarise the change, the testing you did and any
|
||||||
|
compatibility notes.
|
||||||
|
|
||||||
|
## Build and test
|
||||||
|
|
||||||
|
The project targets `net10.0-windows` against Dalamud SDK 15. To build
|
||||||
|
locally you need:
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- A working Dalamud development environment with `DALAMUD_HOME` set
|
||||||
|
(XIVLauncher installed and launched once is the simplest path)
|
||||||
|
- VS Code with the C# Dev Kit, Rider, or Visual Studio
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet restore
|
||||||
|
dotnet build ChatTwo.sln -c Release
|
||||||
|
dotnet test ChatTwo.sln -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
The test project is `ChatTwo.Tests`. New behaviour should come with a
|
||||||
|
test where the existing test infrastructure makes that practical
|
||||||
|
(privacy filter, configuration migration, message store).
|
||||||
|
|
||||||
|
For a smoke test in-game: build, copy the output into your Dalamud
|
||||||
|
`devPlugins/HellionChat/` directory and load it through `/xlplugins`.
|
||||||
|
|
||||||
|
## Continuous integration
|
||||||
|
|
||||||
|
Every push and every pull request runs:
|
||||||
|
|
||||||
|
- `build.yml` — `dotnet build` and `dotnet test`
|
||||||
|
- `codeql.yml` — CodeQL security analysis
|
||||||
|
|
||||||
|
A pull request will not be merged while either of these is failing.
|
||||||
|
CodeQL findings on changed code need to be addressed; pre-existing
|
||||||
|
findings on untouched code are tracked separately.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
By submitting a pull request you confirm that:
|
||||||
|
|
||||||
|
- Your contribution is your own work, or you have the right to
|
||||||
|
contribute it under the project licence.
|
||||||
|
- You agree that your contribution will be released under the
|
||||||
|
[EUPL-1.2](LICENSE), the same licence as the rest of the project.
|
||||||
|
|
||||||
|
There is no separate CLA.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
Hellion-specific strings live in `ChatTwo/Resources/HellionStrings.resx`
|
||||||
|
(English source) and `HellionStrings.<lang>.resx` (per-language).
|
||||||
|
Translations are accepted as direct pull requests against those files.
|
||||||
|
|
||||||
|
The upstream Chat 2 strings in `ChatTwo/Resources/Language.*.resx` are
|
||||||
|
**not** translated in this repository. They are owned by the upstream
|
||||||
|
Chat 2 project and synced in via cherry-pick. Please contribute
|
||||||
|
upstream-string translations to
|
||||||
|
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead.
|
||||||
|
|
||||||
|
## A note on response times
|
||||||
|
|
||||||
|
I respond on weekdays during European business hours and I take
|
||||||
|
weekends and FFXIV patch days off. A pull request that sits for a few
|
||||||
|
days has not been ignored; I just have not gotten to it yet. Pinging
|
||||||
|
once after a week is fine; please do not ping daily.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
||||||
|
Original ChatTwo authors and copyright holders of the upstream
|
||||||
|
plugin this fork is built on. Their work covers the message store,
|
||||||
|
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||||
|
hooks, the localisation infrastructure and most of the
|
||||||
|
architecture HellionChat still relies on.
|
||||||
|
|
||||||
|
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
|
||||||
|
HellionChat-specific modifications, including the privacy filter,
|
||||||
|
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
|
||||||
|
Hellion theme and font integration, German localisation and the
|
||||||
|
EUPL-1.2 fork maintenance.
|
||||||
|
|
||||||
|
Licensed under the European Union Public Licence (EUPL), Version 1.2
|
||||||
|
only. The full Licence text lives in the LICENSE file at the root of
|
||||||
|
this repository. The official Licence website is at:
|
||||||
|
|
||||||
|
https://eupl.eu/1.2/en/
|
||||||
|
|
||||||
|
This Work is provided "AS IS" without warranties of any kind. See
|
||||||
|
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
|
||||||
|
Liability) of the Licence for the legally binding wording.
|
||||||
|
|
||||||
|
Acknowledgements directed at the upstream ChatTwo authors live in
|
||||||
|
NOTICE.md. The manual upstream-sync workflow lives in UPSTREAM_SYNC.md.
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net10.0-windows</TargetFrameworks>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.6.3" />
|
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\ChatTwo\ChatTwo.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
|
||||||
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsCI)' == 'true'">
|
|
||||||
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Dalamud">
|
|
||||||
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="FFXIVClientStructs">
|
|
||||||
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Lumina">
|
|
||||||
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Lumina.Excel">
|
|
||||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
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;
|
|
||||||
using Chat2PartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
|
||||||
|
|
||||||
namespace ChatTwo.Tests;
|
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
[TestSubject(typeof(MessageStore))]
|
|
||||||
public class MessageStoreTest {
|
|
||||||
// From Message.cs
|
|
||||||
private static readonly byte[] ExtraChatChannelPayloadBytes = [0, 0x27, 18, 0x20];
|
|
||||||
|
|
||||||
public TestContext TestContext { get; set; }
|
|
||||||
|
|
||||||
public static string GetImportPath() {
|
|
||||||
string[] importPaths = [
|
|
||||||
@".\TestData",
|
|
||||||
@"..\TestData",
|
|
||||||
@"..\..\TestData",
|
|
||||||
@"..\..\..\TestData",
|
|
||||||
];
|
|
||||||
var importPath = importPaths.FirstOrDefault(Directory.Exists);
|
|
||||||
if (string.IsNullOrEmpty(importPath)) {
|
|
||||||
throw new DirectoryNotFoundException("Could not find the import path");
|
|
||||||
}
|
|
||||||
return importPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void StoreAndRetrieve() {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Write the message.
|
|
||||||
var input = BigMessage();
|
|
||||||
store.UpsertMessage(input);
|
|
||||||
|
|
||||||
// Read the message back.
|
|
||||||
using var messageEnumerator = store.GetMostRecentMessages();
|
|
||||||
var messages = messageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(1, messages.Count);
|
|
||||||
AssertMessagesEqual(input, messages.First());
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void RetrieveMultiple() {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Insert 10 messages in the wrong order of date.
|
|
||||||
var messages = new List<Message>();
|
|
||||||
const uint receiver = 12345;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
for (var i = 0; i < 10; i++) {
|
|
||||||
var message = BigMessage(true, receiver, now.AddSeconds(-i));
|
|
||||||
TestContext.WriteLine($"Inserting message {i}: {message.Id}");
|
|
||||||
store.UpsertMessage(message);
|
|
||||||
messages.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert a message for a different receiver. This shouldn't be returned
|
|
||||||
// because of the receiver filtering.
|
|
||||||
var otherReceiverMsg = BigMessage(receiver: receiver + 1, dateTime: now.AddSeconds(1));
|
|
||||||
TestContext.WriteLine($"Inserting other receiver message: {otherReceiverMsg.Id}");
|
|
||||||
store.UpsertMessage(otherReceiverMsg);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages. Should return the 4 newest messages
|
|
||||||
// from the list, as well as the different receiver message because we
|
|
||||||
// aren't filtering.
|
|
||||||
using var unfilteredMessageEnumerator = store.GetMostRecentMessages(count: 5);
|
|
||||||
var outputMessages = unfilteredMessageEnumerator.ToList();
|
|
||||||
var gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 1 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[3].Id,
|
|
||||||
messages[2].Id,
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
otherReceiverMsg.Id
|
|
||||||
}, gotIds);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages but filter by receiver ID.
|
|
||||||
using var filteredByReceiverMessageEnumerator = store.GetMostRecentMessages(receiver: receiver, count: 5);
|
|
||||||
outputMessages = filteredByReceiverMessageEnumerator.ToList();
|
|
||||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 2 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[4].Id,
|
|
||||||
messages[3].Id,
|
|
||||||
messages[2].Id,
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
}, gotIds);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages but only since a specific date.
|
|
||||||
using var filteredByReceiverAndDateMessageEnumerator = store.GetMostRecentMessages(receiver, since: messages[1].Date, count: 5);
|
|
||||||
outputMessages = filteredByReceiverAndDateMessageEnumerator.ToList();
|
|
||||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 3 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
}, gotIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
// This test guards against the data format changing in an incompatible way.
|
|
||||||
public void RetrieveExisting() {
|
|
||||||
var input = BigMessage(uniqId: false);
|
|
||||||
|
|
||||||
var dbPath = Path.Join(GetImportPath(), "existing.db");
|
|
||||||
TestContext.WriteLine($"Using existing database: {dbPath}");
|
|
||||||
Assert.IsTrue(File.Exists(dbPath));
|
|
||||||
|
|
||||||
// Uncomment this section to regenerate the existing database.
|
|
||||||
/*
|
|
||||||
File.Delete(dbPath);
|
|
||||||
using (var newStore = new MessageStore(dbPath)) {
|
|
||||||
newStore.UpsertMessage(input);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
using var existingMessageEnumerator = store.GetMostRecentMessages();
|
|
||||||
var output = existingMessageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(1, output.Count);
|
|
||||||
AssertMessagesEqual(input, output[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(30_000)]
|
|
||||||
public void ProfileMany() {
|
|
||||||
const int count = 20_000;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
var message = BigMessage(uniqId: true);
|
|
||||||
store.UpsertMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var messageEnumerator = store.GetMostRecentMessages(count: count);
|
|
||||||
var messages = messageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(count, messages.Count);
|
|
||||||
foreach (var message in messages) {
|
|
||||||
// Load the message because they are lazily parsed.
|
|
||||||
Assert.IsTrue(message.Id != Guid.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Message BigMessage(bool uniqId = true, uint receiver = 12345, DateTimeOffset? dateTime = null) {
|
|
||||||
// NOTE: These values aren't valid in the game.
|
|
||||||
// NOTE: we can't test UiForeground, UiGlow, or AutoTranslatePayload
|
|
||||||
// because they load data from the game.
|
|
||||||
var senderSeString = new SeStringBuilder()
|
|
||||||
.AddText("<")
|
|
||||||
.Add(new PlayerPayload("Player Name", 12345))
|
|
||||||
.AddItalics("Player Name")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddText(">: ")
|
|
||||||
.Build();
|
|
||||||
var extraChatId = Guid.Parse("03d9e6d4-dc1a-4005-bbe7-66b8c3529277");
|
|
||||||
var contentSeString = new SeStringBuilder()
|
|
||||||
.Add(new RawPayload(ExtraChatChannelPayloadBytes.Concat(extraChatId.ToByteArray()).ToArray()))
|
|
||||||
.AddIcon(BitmapFontIcon.IslandSanctuary)
|
|
||||||
.AddMapLink(1, 2, 3, 4)
|
|
||||||
.AddText("map")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddQuestLink(12345)
|
|
||||||
.AddText("quest")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.Add(new DalamudLinkPayload())
|
|
||||||
.AddText("dalamud")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddStatusLink(12345)
|
|
||||||
.AddText("status")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddPartyFinderLink(12345)
|
|
||||||
.AddText("party finder")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Add Chat 2 specific payloads (that can't be serialized into the
|
|
||||||
// SeString).
|
|
||||||
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, ChatType.Say).ToList();
|
|
||||||
contentChunks = contentChunks.Concat([
|
|
||||||
new TextChunk(ChunkSource.Content, new Chat2PartyFinderPayload(12345), "chat 2 party finder"),
|
|
||||||
new TextChunk(ChunkSource.Content, new AchievementPayload(12345), "chat 2 achievement"),
|
|
||||||
new TextChunk(ChunkSource.Content, new UriPayload(new Uri("https://dalamud.dev")), "chat 2 uri"),
|
|
||||||
]).ToList();
|
|
||||||
|
|
||||||
var chatCode = new ChatCode((XivChatType)46, XivChatRelationKind.LocalPlayer, XivChatRelationKind.EngagedEnemy);
|
|
||||||
return new Message(
|
|
||||||
uniqId ? Guid.NewGuid() : Guid.Parse("f011343e-6a21-49e5-a6f9-238f0f1f8c2c"),
|
|
||||||
receiver,
|
|
||||||
54321,
|
|
||||||
dateTime ?? DateTimeOffset.FromUnixTimeMilliseconds(1713520182440),
|
|
||||||
chatCode,
|
|
||||||
ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, ChatType.Debug).ToList(),
|
|
||||||
contentChunks,
|
|
||||||
senderSeString,
|
|
||||||
contentSeString,
|
|
||||||
extraChatId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void AssertMessagesEqual(Message input, Message output) {
|
|
||||||
// Check basic fields.
|
|
||||||
Assert.AreEqual(input.Id, output.Id);
|
|
||||||
Assert.AreEqual(input.Receiver, output.Receiver);
|
|
||||||
Assert.AreEqual(input.ContentId, output.ContentId);
|
|
||||||
// Assert time is within 1 second
|
|
||||||
var timeDifference = Math.Abs(input.Date.ToUniversalTime().Subtract(output.Date.ToUniversalTime()).TotalSeconds);
|
|
||||||
Assert.IsTrue(timeDifference < 1);
|
|
||||||
Assert.AreEqual(input.Code, output.Code);
|
|
||||||
Assert.AreEqual($"{input.SenderSource.Encode():X}", $"{output.SenderSource.Encode():X}");
|
|
||||||
Assert.AreEqual($"{input.ContentSource.Encode():X}", $"{output.ContentSource.Encode():X}");
|
|
||||||
Assert.AreEqual(input.SortCodeV2, output.SortCodeV2);
|
|
||||||
Assert.AreEqual(input.ExtraChatChannel, output.ExtraChatChannel);
|
|
||||||
|
|
||||||
// Check chunks.
|
|
||||||
AssertChunksEqual(input.Sender, output.Sender);
|
|
||||||
AssertChunksEqual(input.Content, output.Content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertChunksEqual(IReadOnlyList<Chunk> inputChunks, IReadOnlyList<Chunk> outputChunks) {
|
|
||||||
Assert.AreEqual(inputChunks.Count, outputChunks.Count);
|
|
||||||
for (var i = 0; i < inputChunks.Count; i++) {
|
|
||||||
var inputChunk = inputChunks[i];
|
|
||||||
var outputChunk = outputChunks[i];
|
|
||||||
Assert.AreEqual(inputChunk.Source, outputChunk.Source);
|
|
||||||
switch (inputChunk.Link) {
|
|
||||||
case AchievementPayload inputAchievementPayload:
|
|
||||||
Assert.AreEqual(inputAchievementPayload.Id, ((AchievementPayload) outputChunk.Link)!.Id);
|
|
||||||
break;
|
|
||||||
case Chat2PartyFinderPayload inputPartyFinderPayload:
|
|
||||||
Assert.AreEqual(inputPartyFinderPayload.Id, ((Chat2PartyFinderPayload) outputChunk.Link)!.Id);
|
|
||||||
break;
|
|
||||||
case UriPayload inputUriPayload:
|
|
||||||
Assert.AreEqual(inputUriPayload.Uri, ((UriPayload) outputChunk.Link)!.Uri);
|
|
||||||
break;
|
|
||||||
case null:
|
|
||||||
Assert.IsTrue(outputChunk.Link == null);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Assert.AreEqual($"{inputChunk.Link.Encode():X}", $"{outputChunk.Link!.Encode():X}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (inputChunk) {
|
|
||||||
case TextChunk inputTextChunk:
|
|
||||||
var outputTextChunk = (TextChunk)outputChunk;
|
|
||||||
Assert.AreEqual(inputTextChunk.FallbackColour, outputTextChunk.FallbackColour);
|
|
||||||
Assert.AreEqual(inputTextChunk.Foreground, outputTextChunk.Foreground);
|
|
||||||
Assert.AreEqual(inputTextChunk.Glow, outputTextChunk.Glow);
|
|
||||||
Assert.AreEqual(inputTextChunk.Italic, outputTextChunk.Italic);
|
|
||||||
Assert.AreEqual(inputTextChunk.Content, outputTextChunk.Content);
|
|
||||||
break;
|
|
||||||
case IconChunk inputIconChunk:
|
|
||||||
Assert.AreEqual(inputIconChunk.Icon, ((IconChunk) outputChunk).Icon);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception("Unknown chunk type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertGuidsEqual(IReadOnlyList<Guid> expected, IReadOnlyList<Guid> got) {
|
|
||||||
Assert.AreEqual(expected.Count, got.Count);
|
|
||||||
for (var i = 0; i < expected.Count; i++) {
|
|
||||||
Assert.AreEqual(expected[i].ToString(), got[i].ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,418 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// v0.6.1 — if the victim is currently popped out, tear down the
|
||||||
|
// matching Popout window first. Otherwise the window stays in
|
||||||
|
// PopOutWindows + WindowSystem and renders empty / re-spawns on the
|
||||||
|
// next AddPopOutsToDraw tick. Latent since pop-outs were introduced;
|
||||||
|
// becomes visible with AutoTellTabsOpenAsPopout where dropping a
|
||||||
|
// popped tab is now a routine code path.
|
||||||
|
if (victim.Tab.PopOut)
|
||||||
|
{
|
||||||
|
var popout = _plugin.ChatLogWindow.ActivePopouts
|
||||||
|
.FirstOrDefault(p => p.TabIdentifier == victim.Tab.Identifier);
|
||||||
|
if (popout != null)
|
||||||
|
{
|
||||||
|
popout.IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// The current message is already persisted in the store by the
|
||||||
|
// time MessageProcessed fires (see MessageManager.cs: UpsertMessage
|
||||||
|
// runs before the event), so we have to exclude it explicitly to
|
||||||
|
// avoid the separator landing below the live tell.
|
||||||
|
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
|
||||||
|
|
||||||
|
tab.AddMessage(currentMessage, unread: true);
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — opt-in: open new /tell tabs directly as a
|
||||||
|
// pop-out window. Set BEFORE Tabs.Add so the next render-tick's
|
||||||
|
// AddPopOutsToDraw() sees PopOut=true and spawns the Popout window
|
||||||
|
// alongside the tab going into the list. No SaveConfig() because
|
||||||
|
// auto-tell tabs are IsTempTab (session-only, never persisted).
|
||||||
|
if (Plugin.Config.AutoTellTabsOpenAsPopout)
|
||||||
|
{
|
||||||
|
tab.PopOut = 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, Guid currentMessageId)
|
||||||
|
{
|
||||||
|
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
|
if (preloadCount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Pull one extra row because the live tell that triggered this
|
||||||
|
// spawn is already in the store and would otherwise eat one of
|
||||||
|
// the user's preload-budget slots.
|
||||||
|
var history = _store.GetTellHistoryWithSender(
|
||||||
|
_messageManager.CurrentContentId,
|
||||||
|
senderName,
|
||||||
|
senderWorld,
|
||||||
|
preloadCount + 1);
|
||||||
|
|
||||||
|
var historicMessages = history
|
||||||
|
.Where(m => m.Id != currentMessageId)
|
||||||
|
.Take(preloadCount)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (historicMessages.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 historicMessages)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
// v0.6.1 — symmetric to DropOldestTempTab cleanup: tear down any
|
||||||
|
// popped-out temp tab windows before removing the tabs themselves,
|
||||||
|
// otherwise PopOutWindows + WindowSystem keep ghost entries until
|
||||||
|
// the next plugin reload. Especially relevant once Auto-Pop-Out is
|
||||||
|
// enabled — every logout would otherwise leak as many ghosts as
|
||||||
|
// there were active /tell pop-outs.
|
||||||
|
var poppedTempTabIds = Plugin.Config.Tabs
|
||||||
|
.Where(t => t.IsTempTab && t.PopOut)
|
||||||
|
.Select(t => t.Identifier)
|
||||||
|
.ToList();
|
||||||
|
if (poppedTempTabIds.Count > 0)
|
||||||
|
{
|
||||||
|
var poppedSet = poppedTempTabIds.ToHashSet();
|
||||||
|
foreach (var popout in _plugin.ChatLogWindow.ActivePopouts
|
||||||
|
.Where(p => poppedSet.Contains(p.TabIdentifier))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
popout.IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable → Regular
+11
-12
@@ -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.3.1</Version>
|
<Version>0.6.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,
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
<PackageReference Include="Pidgin" Version="3.3.0" />
|
<PackageReference Include="Pidgin" Version="3.3.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
@@ -57,17 +57,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Plugin icon. Copy images/* into the build output so Dalamud
|
||||||
|
finds the icon next to the DLL, and let the SDK default
|
||||||
|
DalamudPackager pipeline include the same path in the
|
||||||
|
release ZIP. Earlier we shipped a custom DalamudPackager
|
||||||
|
targets override that explicitly set HandleImages and
|
||||||
|
ImagesPath; that override conflicted with the SDK 15
|
||||||
|
default and the resulting manifest carried no IconUrl.
|
||||||
|
Removed in v0.5.2. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="images\" />
|
<None Include="images\**">
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Copy images/icon.png next to the built DLL so Dalamud's local
|
|
||||||
plugin loader finds it at <plugindir>/images/icon.png. The
|
|
||||||
DalamudPackager.targets file in this directory then includes
|
|
||||||
the same path inside the release ZIP — see that file for the
|
|
||||||
full packaging override. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="images\icon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+107
-9
@@ -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 = 12;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -72,8 +73,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool HellionThemeEnabled = true;
|
public bool HellionThemeEnabled = true;
|
||||||
|
|
||||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||||||
// panes more glass-like so the game shines through. Default ~92%.
|
// panes more glass-like so the game shines through. Default 0.5
|
||||||
public float HellionThemeWindowOpacity = 0.92f;
|
// matches the maintainer's daily-driver preference; users who want
|
||||||
|
// a less translucent look bump it up in Aussehen → Theme.
|
||||||
|
public float HellionThemeWindowOpacity = 0.5f;
|
||||||
|
|
||||||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
||||||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
||||||
@@ -81,6 +84,52 @@ 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 (1–50). 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 0–100; 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;
|
||||||
|
|
||||||
|
// Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
|
||||||
|
// input feature. Set to true once the user dismisses the banner from a
|
||||||
|
// pop-out window; never reset after that.
|
||||||
|
public bool SeenPopOutInputHint;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 master switch for the pop-out input bar.
|
||||||
|
// Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
|
||||||
|
// are session-only and would force the user to re-enable it for every
|
||||||
|
// new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
|
||||||
|
// because tester feedback called the manual toggle "umständlich, wirkt
|
||||||
|
// unfertig". v11 → v12 migration applies the same flip to existing users.
|
||||||
|
public bool PopOutInputEnabled = true;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
|
||||||
|
// chat-header pop-out toolbar button and reminds about the pop-out
|
||||||
|
// input default flip. Set to true once the user dismisses the banner
|
||||||
|
// from the main chat window; never reset after that.
|
||||||
|
public bool SeenPopOutHeaderHint;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
|
||||||
|
// sets tab.PopOut = true on every new auto-tell tab so the conversation
|
||||||
|
// pops out as its own window directly. Closing the pop-out returns the
|
||||||
|
// tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
|
||||||
|
// because the existing sidebar workflow is what most users (especially
|
||||||
|
// club greeters tracking many parallel tells) expect by default.
|
||||||
|
public bool AutoTellTabsOpenAsPopout;
|
||||||
|
|
||||||
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 +161,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 5–15 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 +176,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,8 +186,12 @@ 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;
|
// Default ON to match a German / European 24h locale. The
|
||||||
|
// ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
|
||||||
|
// CultureInfo.InvariantCulture so the result is consistent across
|
||||||
|
// host locales.
|
||||||
|
public bool Use24HourClock = true;
|
||||||
|
|
||||||
public bool ShowEmotes = true;
|
public bool ShowEmotes = true;
|
||||||
public HashSet<string> BlockedEmotes = [];
|
public HashSet<string> BlockedEmotes = [];
|
||||||
@@ -230,7 +288,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 +317,17 @@ 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;
|
||||||
|
|
||||||
|
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||||
|
PopOutInputEnabled = other.PopOutInputEnabled;
|
||||||
|
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
|
||||||
|
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,9 +403,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 +472,7 @@ public class Tab
|
|||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.From(TellTarget),
|
TellTarget = TellTarget.From(TellTarget),
|
||||||
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
HellionChat — DalamudPackager override.
|
|
||||||
|
|
||||||
The default DalamudPackager.targets shipped by the SDK does not set
|
|
||||||
HandleImages / ImagesPath, so the images/ directory is silently
|
|
||||||
excluded from the release ZIP. The presence of this file at
|
|
||||||
$(ProjectDir)DalamudPackager.targets disables the SDK's default
|
|
||||||
target (it guards on `!Exists('$(PackagerTargetFile)')`) and lets
|
|
||||||
us call the packager task ourselves with the image fields wired in.
|
|
||||||
|
|
||||||
Apart from HandleImages + ImagesPath the property list mirrors the
|
|
||||||
SDK default verbatim so we don't lose any other manifest field as
|
|
||||||
the upstream SDK evolves.
|
|
||||||
-->
|
|
||||||
<Project>
|
|
||||||
<Target Name="HellionDalamudPackagerDebug"
|
|
||||||
AfterTargets="Build"
|
|
||||||
Condition="'$(Configuration)' == 'Debug'">
|
|
||||||
<DalamudPackager ProjectDir="$(ProjectDir)"
|
|
||||||
OutputPath="$(OutputPath)"
|
|
||||||
AssemblyName="$(AssemblyName)"
|
|
||||||
MakeZip="false"
|
|
||||||
Author="$(Author)"
|
|
||||||
Name="$(Name)"
|
|
||||||
MinimumDalamudVersion="$(MinimumDalamudVersion)"
|
|
||||||
Punchline="$(Punchline)"
|
|
||||||
Description="$(Description)"
|
|
||||||
ApplicableVersion="$(ApplicableVersion)"
|
|
||||||
RepoUrl="$(RepoUrl)"
|
|
||||||
Tags="$(Tags)"
|
|
||||||
CategoryTags="$(CategoryTags)"
|
|
||||||
DalamudApiLevel="$(DalamudApiLevel)"
|
|
||||||
LoadRequiredState="$(LoadRequiredState)"
|
|
||||||
LoadSync="$(LoadSync)"
|
|
||||||
CanUnloadAsync="$(CanUnloadAsync)"
|
|
||||||
LoadPriority="$(LoadPriority)"
|
|
||||||
ImageUrls="$(ImageUrls)"
|
|
||||||
IconUrl="$(IconUrl)"
|
|
||||||
Changelog="$(Changelog)"
|
|
||||||
AcceptsFeedback="$(AcceptsFeedback)"
|
|
||||||
FeedbackMessage="$(FeedbackMessage)"
|
|
||||||
HandleImages="true"
|
|
||||||
ImagesPath="$(ProjectDir)images" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<Target Name="HellionDalamudPackagerRelease"
|
|
||||||
AfterTargets="Build"
|
|
||||||
Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<DalamudPackager ProjectDir="$(ProjectDir)"
|
|
||||||
OutputPath="$(OutputPath)"
|
|
||||||
AssemblyName="$(AssemblyName)"
|
|
||||||
MakeZip="true"
|
|
||||||
Author="$(Author)"
|
|
||||||
Name="$(Name)"
|
|
||||||
MinimumDalamudVersion="$(MinimumDalamudVersion)"
|
|
||||||
Punchline="$(Punchline)"
|
|
||||||
Description="$(Description)"
|
|
||||||
ApplicableVersion="$(ApplicableVersion)"
|
|
||||||
RepoUrl="$(RepoUrl)"
|
|
||||||
Tags="$(Tags)"
|
|
||||||
CategoryTags="$(CategoryTags)"
|
|
||||||
DalamudApiLevel="$(DalamudApiLevel)"
|
|
||||||
LoadRequiredState="$(LoadRequiredState)"
|
|
||||||
LoadSync="$(LoadSync)"
|
|
||||||
CanUnloadAsync="$(CanUnloadAsync)"
|
|
||||||
LoadPriority="$(LoadPriority)"
|
|
||||||
ImageUrls="$(ImageUrls)"
|
|
||||||
IconUrl="$(IconUrl)"
|
|
||||||
Changelog="$(Changelog)"
|
|
||||||
AcceptsFeedback="$(AcceptsFeedback)"
|
|
||||||
FeedbackMessage="$(FeedbackMessage)"
|
|
||||||
HandleImages="true"
|
|
||||||
ImagesPath="$(ProjectDir)images" />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
||||||
+10
-5
@@ -35,20 +35,20 @@ public static class EmoteCache
|
|||||||
public Emote Emote { get; set; }
|
public Emote Emote { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public string Id { get; set; }
|
public required string Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public struct Emote()
|
public struct Emote()
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public string Id { get; set; }
|
public required string Id { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")]
|
||||||
public string Code { get; set; }
|
public required string Code { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("imageType")]
|
[JsonPropertyName("imageType")]
|
||||||
public string ImageType { get; set; }
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,13 +414,13 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward))
|
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward))
|
||||||
{
|
{
|
||||||
Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false;
|
Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false;
|
||||||
Plugin.ChatLogWindow.ChangeTabDelta(1);
|
DispatchTabDelta(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward))
|
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward))
|
||||||
{
|
{
|
||||||
Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false;
|
Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false;
|
||||||
Plugin.ChatLogWindow.ChangeTabDelta(-1);
|
DispatchTabDelta(-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +465,24 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v0.6.0 — central dispatch for ChatTabForward/Backward. If a pop-out
|
||||||
|
// window currently has its compact input focused, the keybind is
|
||||||
|
// forwarded into that pop-out's ChatInputBar so the user navigates
|
||||||
|
// tabs in the window they are typing in. Otherwise the main window
|
||||||
|
// handles it (= v0.5.x behavior).
|
||||||
|
private void DispatchTabDelta(int delta)
|
||||||
|
{
|
||||||
|
foreach (var popout in Plugin.ChatLogWindow.ActivePopouts)
|
||||||
|
{
|
||||||
|
if (popout.HasFocusedInputBar && popout.InputBar != null)
|
||||||
|
{
|
||||||
|
popout.InputBar.HandleKeybindForward(delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Plugin.ChatLogWindow.ChangeTabDelta(delta);
|
||||||
|
}
|
||||||
|
|
||||||
private static Keybind GetKeybind(string id)
|
private static Keybind GetKeybind(string id)
|
||||||
{
|
{
|
||||||
var outData = new FFXIVClientStructs.FFXIV.Client.System.Input.Keybind();
|
var outData = new FFXIVClientStructs.FFXIV.Client.System.Input.Keybind();
|
||||||
|
|||||||
+83
-149
@@ -31,8 +31,16 @@ description: |-
|
|||||||
so Hellion Chat does not share state with the upstream plugin
|
so Hellion Chat does not share state with the upstream plugin
|
||||||
|
|
||||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||||
|
|
||||||
|
Modding & support: join the Hellion Forge Discord at
|
||||||
|
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
|
||||||
|
other Hellion Online Media plugins/tools.
|
||||||
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
||||||
accepts_feedback: true
|
accepts_feedback: true
|
||||||
|
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/icon.png
|
||||||
|
image_urls:
|
||||||
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/chatWindow.png
|
||||||
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/withSimpleTweaks.png
|
||||||
tags:
|
tags:
|
||||||
- Social
|
- Social
|
||||||
- UI
|
- UI
|
||||||
@@ -40,170 +48,96 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**Hellion Chat 0.3.1 — Upstream emote regression fix**
|
**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**
|
||||||
|
|
||||||
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
|
- Pop-out button now visible in the chat header (no more hunting
|
||||||
from API 15 updates" which changes the BetterTTV emote DTOs
|
through the right-click menu)
|
||||||
(Emote and Top100) from public fields to public properties.
|
- One-time hint banner explains pop-out tabs and the right-click
|
||||||
System.Text.Json under the API 15 toolchain only honours the
|
shortcut
|
||||||
[JsonPropertyName] attribute on properties, so the previous
|
- New setting: open new /tell tabs directly as pop-out windows
|
||||||
field-based version deserialised every fetched emote into empty
|
(Settings → Chat → Auto-Tell-Tabs)
|
||||||
default values. Result: BetterTTV emotes were silently broken
|
- Pop-out input is now enabled by default — closing a pop-out still
|
||||||
on fresh installs. The fix is six lines and applies cleanly on
|
returns the tab to the sidebar
|
||||||
top of our defensive null-check from earlier; the EmoteCache
|
- Bugfix: dropping or logging out with an LRU/popped auto-tell tab
|
||||||
path-traversal hardening from 0.3.0 stays as it is.
|
now also closes its pop-out window (no more ghost windows)
|
||||||
|
- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out
|
||||||
|
hint banner was visible (also fixed retroactively for the v0.6.0
|
||||||
|
banner inside pop-outs)
|
||||||
|
|
||||||
Authorship of the fix is preserved with git cherry-pick -x, so
|
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||||
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).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.2.0 — Webinterface removed**
|
**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**
|
||||||
|
|
||||||
The upstream webinterface has been removed in its entirety. It
|
Two opt-in UX features land in the same release. Existing users see
|
||||||
serves a different use case from the smaller default footprint
|
no change unless they enable the new toggles.
|
||||||
this fork is built around, namely remote access to chat from a
|
|
||||||
second device. Aligning it with the data minimisation defaults
|
|
||||||
Hellion Chat ships with would have meant a substantial rebuild.
|
|
||||||
Removing it was the cleaner path for this particular fork.
|
|
||||||
|
|
||||||
What changed in this release:
|
Pop-out input bar:
|
||||||
|
|
||||||
- Settings tab "Webinterface" is gone, the corresponding
|
- New global master switch in Settings → Window → Frame: "Enable input
|
||||||
Configuration fields (WebinterfaceEnabled / AutoStart / Password /
|
in pop-outs". Default OFF so existing behaviour is preserved
|
||||||
Port / AuthStore / MaxLinesToSend) are dropped and stale entries
|
- When enabled, every pop-out window grows a compact input bar at the
|
||||||
fall out of the JSON on the next save automatically
|
bottom (channel-coloured icon button left, text input right). The
|
||||||
- The whole ChatTwo/Http tree, the bundled Svelte frontend in
|
auto-translate picker is intentionally not part of the compact bar
|
||||||
websiteBuild.zip and the WebinterfaceUtil helper are deleted
|
in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)
|
||||||
- Watson.Lite (the HTTP server) and Newtonsoft.Json (only used by
|
rarely need it there
|
||||||
the webinterface JSON wire format) are removed from the
|
- Each pop-out keeps an independent text buffer and history cursor;
|
||||||
package references
|
channel changes still apply globally because that is how the FFXIV
|
||||||
- DbViewer's "Chat2 JSON Export" button is dropped because it
|
channel API works
|
||||||
serialised the database into the webinterface message protocol;
|
- Up/Down navigates a shared input history singleton across the main
|
||||||
the Privacy tab's MessageExporter (Markdown, JSON, CSV with
|
window and every open pop-out
|
||||||
channel and date filters) covers the same ground without the
|
- First pop-out opening after the upgrade shows a one-time hint
|
||||||
proprietary shape
|
banner pointing users to the new toggle
|
||||||
- About tab notes the absence so users coming from Chat 2 do not
|
|
||||||
look for it
|
|
||||||
- Configuration version bumps from 7 to 8 with a one-shot
|
|
||||||
notification (EN + DE)
|
|
||||||
|
|
||||||
No changes to the privacy filter, retention sweep, first-run wizard
|
Chat colour presets:
|
||||||
or export pipeline. Existing chat history is preserved.
|
|
||||||
|
- Seven built-in presets above the per-channel colour list in
|
||||||
|
Settings → Appearance → Colours: ChatTwo Default, High-Contrast,
|
||||||
|
Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange
|
||||||
|
Arctic Cyan + Ember Glow palette from the Hellion Online Media
|
||||||
|
branding spec), plus two bonus mood presets — Night Blue (royal
|
||||||
|
blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)
|
||||||
|
- Apply is immediate and overwrites the channels covered by the
|
||||||
|
preset; battle-channel colours are left alone so combat tuning
|
||||||
|
stays intact
|
||||||
|
|
||||||
|
Configuration migrates from v10 to v11 with a diagnostic log entry;
|
||||||
|
no data is reset. Bilingual (English/German) for both new sections.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.1.2 — About tab rebrand, DBViewer polish**
|
**Hellion Chat 0.5.4 — WrapText hardening**
|
||||||
|
|
||||||
- About tab now shows Hellion-specific maintainer, license, EU/US/JP
|
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
||||||
disclaimer and SQUARE ENIX disclaimer instead of the inherited
|
Span- and index-based control flow. Closes the persistent CodeQL
|
||||||
Chat 2 contact info; original ChatTwo translator credits stay
|
Critical alert "unvalidated local pointer arithmetic" that kept
|
||||||
visible under a clearly labelled upstream tree node
|
re-firing on every shape of the previous fix.
|
||||||
- Localization clarified: Hellion-specific German strings are
|
|
||||||
maintained by the fork maintainer, the Crowdin contributor list
|
|
||||||
only covers the inherited upstream strings
|
|
||||||
- Cherry-picked DBViewer UI improvements from upstream Chat 2
|
|
||||||
(auto-scroll-reset on page change, tooltips on date reset,
|
|
||||||
folder export, page arrows, localized export-running messages)
|
|
||||||
- README rewritten in the Hellion project style with a tech-stack
|
|
||||||
table, architecture tree, database column list, install guide,
|
|
||||||
upstream-sync workflow notes and project-status checklist
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.1 — Packaging and migration fixes**
|
Hardening:
|
||||||
|
|
||||||
- Plugin icon now ships inside the bundle, so the Hellion logo
|
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
|
||||||
renders locally in the Dalamud plugin list once installed (the
|
via ArrayPool, validates the actual encoded length against that
|
||||||
previous release relied only on the remote IconUrl)
|
ceiling, and threads the rest of the algorithm through int offsets
|
||||||
- Plugin icon downsampled from 1024×1024 to 256×256 to match the
|
instead of raw byte pointers
|
||||||
rendered size; loads faster and caches better
|
- Pointer arithmetic only happens inside two small private helpers
|
||||||
- Migration from upstream Chat 2 is more robust: each file move is
|
(CalcWordWrap and DrawText) that take the pinned base pointer plus
|
||||||
wrapped individually, a locked SQLite database no longer aborts
|
int offsets sourced from the plugin's own logic, not from any
|
||||||
the rest of the migration, and a warning notification fires when
|
virtual-method return
|
||||||
any file is held open (with a hint to disable Chat 2 and restart
|
- Added a 16 KiB upper bound on the buffer rent to prevent a
|
||||||
the game)
|
pathological input from triggering an unbounded ArrayPool allocation
|
||||||
- README ships a step-by-step migration guide (fresh install versus
|
|
||||||
coming from Chat 2) and a troubleshooting section with manual
|
|
||||||
recovery commands for Linux and Windows
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.0 — Initial fork release**
|
No user-visible behaviour change. Word-wrap output is byte-identical
|
||||||
|
to v0.5.3.
|
||||||
Privacy
|
|
||||||
- Channel whitelist filter in MessageStore.UpsertMessage with a
|
|
||||||
Privacy-First default (own conversations only)
|
|
||||||
- Per-channel retention with a 24-hour idempotent background sweep
|
|
||||||
- Retroactive cleanup with a Ctrl+Shift confirm and VACUUM
|
|
||||||
- Export to Markdown / JSON / CSV via Dalamud's file dialog
|
|
||||||
|
|
||||||
Onboarding
|
|
||||||
- First-run wizard with three profiles: Privacy-First / Casual /
|
|
||||||
Full History
|
|
||||||
- Configuration migration that seeds defaults on update
|
|
||||||
- One-shot migration from upstream Chat 2's pluginConfigs layout
|
|
||||||
- Migrate3 idempotency recovery for half-migrated databases
|
|
||||||
|
|
||||||
Look & feel
|
|
||||||
- Localized UI (English and German) with live language switching
|
|
||||||
- Industrial HUD theme with cyan-teal action accents, slate-violet
|
|
||||||
tabs, amber active highlights and a window-opacity slider
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
||||||
|
|
||||||
|
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
||||||
|
encoded byte buffer length via GetByteCount before pointer
|
||||||
|
arithmetic. Single-fix patch on top of v0.5.2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ChatTwo;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
|
||||||
|
// ChatLogWindow.InputBacklog so that pop-out windows with their own
|
||||||
|
// ChatInputBar can navigate the same Up/Down history as the main window.
|
||||||
|
// Index semantics are kept identical to the v0.5.x InputBacklog:
|
||||||
|
// index 0 = oldest entry
|
||||||
|
// index Count - 1 = newest entry
|
||||||
|
// Push performs move-to-newest deduplication: existing entries are
|
||||||
|
// removed before the new one is appended at the end.
|
||||||
|
public static class InputHistoryService
|
||||||
|
{
|
||||||
|
private const int MaxSize = 30;
|
||||||
|
private static readonly List<string> _entries = new();
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> Entries => _entries;
|
||||||
|
|
||||||
|
public static int Count => _entries.Count;
|
||||||
|
|
||||||
|
public static void Push(string entry)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var trimmed = entry.Trim();
|
||||||
|
|
||||||
|
// Move-to-newest: existing entries are removed before the append
|
||||||
|
// so the same line typed twice does not occupy two history slots.
|
||||||
|
for (var i = 0; i < _entries.Count; i++)
|
||||||
|
{
|
||||||
|
if (_entries[i] == trimmed)
|
||||||
|
{
|
||||||
|
_entries.RemoveAt(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_entries.Add(trimmed);
|
||||||
|
if (_entries.Count > MaxSize)
|
||||||
|
_entries.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetByCursor(int cursor)
|
||||||
|
{
|
||||||
|
if (cursor < 0 || cursor >= _entries.Count)
|
||||||
|
return null;
|
||||||
|
return _entries[cursor];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
+149
-31
@@ -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 = @"
|
||||||
@@ -728,6 +828,24 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
+105
-51
@@ -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; }
|
||||||
@@ -70,8 +72,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
// plugin start would launch two sweeps in parallel and the second one
|
// 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
|
// 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.
|
// 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 readonly object RetentionSweepLock = new();
|
||||||
internal bool RetentionSweepRunning;
|
internal volatile bool RetentionSweepRunning;
|
||||||
|
|
||||||
internal DateTime GameStarted { get; }
|
internal DateTime GameStarted { get; }
|
||||||
|
|
||||||
@@ -100,75 +105,102 @@ 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");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
tab.SelectedChannels = tab.ChatCodes.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
|
if (File.Exists(liveConfigPath))
|
||||||
tab.ChatCodes.Clear();
|
{
|
||||||
|
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.InactivityHideChannels.Count > 0)
|
Config = new Configuration
|
||||||
{
|
{
|
||||||
Config.InactivityHideChannelsV2 = Config.InactivityHideChannels.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
|
Version = 10,
|
||||||
Config.InactivityHideChannels.Clear();
|
FirstRunCompleted = true,
|
||||||
}
|
};
|
||||||
|
|
||||||
Config.Version = 6;
|
|
||||||
SaveConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
// Hellion Chat v6→v7: seed Privacy-First defaults.
|
|
||||||
if (Config.Version <= 6)
|
|
||||||
{
|
|
||||||
Config.PrivacyFilterEnabled = 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
|
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
|
||||||
// entries (WebinterfacePassword, AuthStore, etc.) get dropped on
|
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
|
||||||
// the next save because their properties no longer exist on the
|
// input feature. Lightweight migration: defaults both fields,
|
||||||
// Configuration class. The bump is recorded so the notification
|
// no user-facing notification because the change is opt-in only.
|
||||||
// only fires once.
|
if (Config.Version < 11)
|
||||||
if (Config.Version <= 7)
|
|
||||||
{
|
{
|
||||||
Config.Version = 8;
|
Config.PopOutInputEnabled = false;
|
||||||
|
Config.SeenPopOutInputHint = false;
|
||||||
|
Config.Version = 11;
|
||||||
SaveConfig();
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
|
||||||
{
|
"SeenPopOutInputHint added (default false)");
|
||||||
Title = HellionStrings.Migration_Webinterface_Removed_Title,
|
|
||||||
Content = HellionStrings.Migration_Webinterface_Removed_Content,
|
|
||||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
|
||||||
InitialDuration = TimeSpan.FromSeconds(20),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from
|
||||||
|
// the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX
|
||||||
|
// polish. Hard-flip is a deliberate design call (see Spec section 5.7);
|
||||||
|
// users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint
|
||||||
|
// reset). Re-toggle after migration is preserved because this block
|
||||||
|
// only fires for Version < 12.
|
||||||
|
if (Config.Version < 12)
|
||||||
|
{
|
||||||
|
Config.PopOutInputEnabled = true;
|
||||||
|
Config.SeenPopOutHeaderHint = false;
|
||||||
|
Config.Version = 12;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " +
|
||||||
|
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
@@ -184,6 +216,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.
|
||||||
@@ -233,7 +273,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
|
||||||
@@ -274,6 +314,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();
|
||||||
@@ -491,7 +535,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)
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ChatTwo.Code;
|
||||||
|
using ChatTwo.Util;
|
||||||
|
|
||||||
|
namespace ChatTwo.Resources;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 built-in colour presets for the ChatColours
|
||||||
|
// settings section. Read-only static data; users apply a preset via the
|
||||||
|
// settings UI which overwrites Configuration.ChatColours immediately.
|
||||||
|
// Battle-channel types are intentionally NOT covered by the stylistic
|
||||||
|
// presets so that combat-log tuning the user has done stays intact.
|
||||||
|
public sealed record ChatColourPreset(
|
||||||
|
string DisplayName,
|
||||||
|
string LocalizationKey,
|
||||||
|
bool IsBrandPreset,
|
||||||
|
IReadOnlyDictionary<ChatType, uint> Colours);
|
||||||
|
|
||||||
|
public static class ChatColourPresets
|
||||||
|
{
|
||||||
|
public static IReadOnlyDictionary<string, ChatColourPreset> All { get; } = BuildAll();
|
||||||
|
|
||||||
|
private static Dictionary<string, ChatColourPreset> BuildAll()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, ChatColourPreset>
|
||||||
|
{
|
||||||
|
["Default"] = new(
|
||||||
|
DisplayName: "ChatTwo Default",
|
||||||
|
LocalizationKey: "ChatColourPresets_Default",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildDefault()),
|
||||||
|
["HighContrast"] = new(
|
||||||
|
DisplayName: "High-Contrast",
|
||||||
|
LocalizationKey: "ChatColourPresets_HighContrast",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildHighContrast()),
|
||||||
|
["Pastell"] = new(
|
||||||
|
DisplayName: "Pastell",
|
||||||
|
LocalizationKey: "ChatColourPresets_Pastell",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildPastell()),
|
||||||
|
["DarkModeTuned"] = new(
|
||||||
|
DisplayName: "Dark-Mode-Tuned",
|
||||||
|
LocalizationKey: "ChatColourPresets_DarkModeTuned",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildDarkModeTuned()),
|
||||||
|
["Hellion"] = new(
|
||||||
|
DisplayName: "Hellion",
|
||||||
|
LocalizationKey: "ChatColourPresets_Hellion",
|
||||||
|
IsBrandPreset: true,
|
||||||
|
Colours: BuildHellion()),
|
||||||
|
["NightBlue"] = new(
|
||||||
|
DisplayName: "Night Blue",
|
||||||
|
LocalizationKey: "ChatColourPresets_NightBlue",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildNightBlue()),
|
||||||
|
["IndigoViolet"] = new(
|
||||||
|
DisplayName: "Indigo Violet",
|
||||||
|
LocalizationKey: "ChatColourPresets_IndigoViolet",
|
||||||
|
IsBrandPreset: false,
|
||||||
|
Colours: BuildIndigoViolet()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Default preset spiegelt 1:1 die Werte aus ChatTypeExt.DefaultColor.
|
||||||
|
// Channels ohne Default-Wert (return null) werden ausgelassen — wer sie
|
||||||
|
// anwenden will, behält seine aktuelle Farbe.
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildDefault()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<ChatType, uint>();
|
||||||
|
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
||||||
|
{
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
var def = type.DefaultColor();
|
||||||
|
if (def.HasValue)
|
||||||
|
dict[type] = def.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildHighContrast()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(255, 255, 255),
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 192, 0),
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 96, 0),
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(255, 128, 255),
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(255, 128, 255),
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(128, 192, 255),
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 128, 64),
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(96, 192, 255),
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(192, 255, 64),
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 128, 128),
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 192, 128),
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 255, 128),
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(192, 255, 128),
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(128, 255, 192),
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(128, 192, 255),
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(192, 128, 255),
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(255, 128, 192),
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(255, 96, 96),
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(255, 160, 96),
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(255, 255, 96),
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(160, 255, 96),
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(96, 255, 160),
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(96, 160, 255),
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(160, 96, 255),
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(255, 96, 160),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildPastell()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(232, 232, 232),
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(245, 216, 155),
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(245, 176, 155),
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(224, 176, 224),
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(224, 176, 224),
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(176, 204, 224),
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(224, 192, 160),
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(168, 200, 224),
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(200, 224, 176),
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(224, 176, 176),
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(224, 200, 176),
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(224, 224, 176),
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(200, 224, 176),
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(176, 224, 200),
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(176, 200, 224),
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(200, 176, 224),
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(224, 176, 200),
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(224, 160, 160),
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(224, 192, 160),
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(224, 224, 160),
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(192, 224, 160),
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(160, 224, 192),
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(160, 192, 224),
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(192, 160, 224),
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(224, 160, 192),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildDarkModeTuned()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 240, 240),
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 208, 64),
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 128, 64),
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(255, 160, 255),
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(255, 160, 255),
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(160, 208, 255),
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 160, 96),
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(128, 200, 255),
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(192, 255, 96),
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 160, 160),
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 192, 160),
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 255, 160),
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(192, 255, 160),
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(160, 255, 192),
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(160, 192, 255),
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(192, 160, 255),
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(255, 160, 192),
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(255, 128, 128),
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(255, 160, 128),
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(255, 255, 128),
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(160, 255, 128),
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(128, 255, 160),
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(128, 160, 255),
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(160, 128, 255),
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(255, 128, 160),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion brand preset — Arctic Cyan + Ember Orange palette aus
|
||||||
|
// /mnt/ssd-fast/Projekte/hellion-media/hellion-media-website/BRANDING.md
|
||||||
|
// (Schema-Stand 2026-04-16). Channels sind über das ganze Brand-Spektrum
|
||||||
|
// verteilt damit jede Zeile auf einen Glance unterscheidbar ist:
|
||||||
|
// Cyan-Familie für Standard/Tell, Ember + Warning für laute Channels,
|
||||||
|
// Status-Farben (Success, Danger) für Linkshells. CrossLinkshells
|
||||||
|
// nutzen die dunkleren/sattersten Varianten derselben Hue-Familien.
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildHellion()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
// Standard / Tell — Cyan-Familie (Brand-Primary)
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
|
||||||
|
|
||||||
|
// Laute Channels — Ember/Warning
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
|
||||||
|
|
||||||
|
// Gruppen-Channels — Success/Ember-dark/Cyan
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232),// Cyan-light
|
||||||
|
|
||||||
|
// Linkshells 1-8 — über das ganze Brand-Spektrum verteilt
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
|
||||||
|
|
||||||
|
// CrossWorld-Linkshells 1-8 — dunklere/sattersere Varianten
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus preset — Night Blue, KAZAMA-Stimmungs-Theme aus
|
||||||
|
// /mnt/HDD-Data1/Obsidian/Vault/Systeme/KAZAMA/Theming/Night Blue + Indigo Violet Themes.md
|
||||||
|
// Klassisch, kühl, technisch — Marineblau-Tiefe ohne Lila-Anteil.
|
||||||
|
// Bewusst NICHT als Brand-Preset markiert (Vault-Boundary): die KAZAMA-Themes
|
||||||
|
// sind persönliche Stimmungs-Themes, nicht Teil des Hellion-Brand-Systems.
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildNightBlue()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
// Standard / Tell — Royal Blue Akzent-Familie
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255),// akzent-hot
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||||
|
|
||||||
|
// Laute Channels — Warning/Danger Status-Töne
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
|
||||||
|
|
||||||
|
// Gruppen — Success/Akzent-Variations
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191),// text-dim
|
||||||
|
|
||||||
|
// Linkshells 1-8 — über Spektrum verteilt
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(130, 220, 100),
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(61, 220, 151),
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
|
||||||
|
|
||||||
|
// CrossWorld-Linkshells — gedämpfte Variants
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(90, 180, 80),
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(30, 170, 110),
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(50, 130, 170),
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(50, 110, 180),
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(90, 100, 130),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus preset — Indigo Violet, KAZAMA-Stimmungs-Theme aus demselben
|
||||||
|
// Vault-Doc. Warm-mystisch, "Galaxy/Glitter/Nordlicht" — tiefes Indigo
|
||||||
|
// mit kräftigem Violet-Akzent. Persönlicher Favorit (siehe Vault).
|
||||||
|
// Auch nicht als Brand-Preset (siehe NightBlue-Note oben).
|
||||||
|
private static IReadOnlyDictionary<ChatType, uint> BuildIndigoViolet()
|
||||||
|
{
|
||||||
|
return new Dictionary<ChatType, uint>
|
||||||
|
{
|
||||||
|
// Standard / Tell — Royal Violet Akzent-Familie
|
||||||
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary (light lavender)
|
||||||
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255),// akzent-hot
|
||||||
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||||
|
|
||||||
|
// Laute Channels — geteilt mit Night Blue (Status-Farben)
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
|
||||||
|
|
||||||
|
// Gruppen
|
||||||
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
|
||||||
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||||
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208),// text-dim
|
||||||
|
|
||||||
|
// Linkshells 1-8
|
||||||
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||||
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(200, 124, 255),
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(176, 124, 255),
|
||||||
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
|
||||||
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
|
||||||
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
|
||||||
|
|
||||||
|
// CrossWorld-Linkshells
|
||||||
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||||
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||||
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||||
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(130, 80, 180),
|
||||||
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(100, 60, 160),
|
||||||
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(91, 42, 154),
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(80, 50, 130),
|
||||||
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(117, 96, 160),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-6
@@ -45,6 +45,7 @@ internal class HellionStrings
|
|||||||
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_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));
|
||||||
@@ -63,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));
|
||||||
@@ -98,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));
|
||||||
@@ -134,7 +132,6 @@ 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));
|
||||||
@@ -164,4 +161,113 @@ internal class HellionStrings
|
|||||||
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
|
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_Localization_P2 => Get(nameof(About_Localization_P2));
|
||||||
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
|
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_OpenAsPopout_Name => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_Name));
|
||||||
|
internal static string ChatLog_AutoTellTabs_OpenAsPopout_Description => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_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));
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 chat colour presets (display labels)
|
||||||
|
internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default));
|
||||||
|
internal static string ChatColourPresets_HighContrast => Get(nameof(ChatColourPresets_HighContrast));
|
||||||
|
internal static string ChatColourPresets_Pastell => Get(nameof(ChatColourPresets_Pastell));
|
||||||
|
internal static string ChatColourPresets_DarkModeTuned => Get(nameof(ChatColourPresets_DarkModeTuned));
|
||||||
|
internal static string ChatColourPresets_Hellion => Get(nameof(ChatColourPresets_Hellion));
|
||||||
|
internal static string ChatColourPresets_NightBlue => Get(nameof(ChatColourPresets_NightBlue));
|
||||||
|
internal static string ChatColourPresets_IndigoViolet => Get(nameof(ChatColourPresets_IndigoViolet));
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 chat colour presets section copy
|
||||||
|
internal static string Settings_Appearance_Colours_PresetsHint => Get(nameof(Settings_Appearance_Colours_PresetsHint));
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 pop-out input master switch
|
||||||
|
internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name));
|
||||||
|
internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs
|
||||||
|
internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText));
|
||||||
|
internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck));
|
||||||
|
internal static string Popout_v060_HintOpenSettings => Get(nameof(Popout_v060_HintOpenSettings));
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.1 pop-out header hint banner (discoverability)
|
||||||
|
internal static string Hint_v061_PopOutHeader_Body => Get(nameof(Hint_v061_PopOutHeader_Body));
|
||||||
|
internal static string Hint_v061_PopOutHeader_Ack => Get(nameof(Hint_v061_PopOutHeader_Ack));
|
||||||
|
internal static string Hint_v061_PopOutHeader_OpenSettings => Get(nameof(Hint_v061_PopOutHeader_OpenSettings));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
@@ -75,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>
|
||||||
@@ -174,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>
|
||||||
@@ -273,9 +270,6 @@
|
|||||||
<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>
|
||||||
@@ -366,4 +360,253 @@
|
|||||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||||
<value>Chat-2-Community-Übersetzer (Upstream)</value>
|
<value>Chat-2-Community-Übersetzer (Upstream)</value>
|
||||||
</data>
|
</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_OpenAsPopout_Name" xml:space="preserve">
|
||||||
|
<value>Neue /tell-Tabs direkt als Pop-Out öffnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Description" xml:space="preserve">
|
||||||
|
<value>Wenn aktiv, wird jeder neu angelegte /tell-Tab sofort als eigenes Fenster geöffnet. Beim Schließen des Fensters kehrt der Tab in die Seitenleiste zurück.</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 & 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 & 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>
|
||||||
|
<data name="ChatColourPresets_Default" xml:space="preserve">
|
||||||
|
<value>ChatTwo Standard</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
|
||||||
|
<value>Hoher Kontrast</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_Pastell" xml:space="preserve">
|
||||||
|
<value>Pastell</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_DarkModeTuned" xml:space="preserve">
|
||||||
|
<value>Dunkelmodus-optimiert</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_Hellion" xml:space="preserve">
|
||||||
|
<value>Hellion</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_NightBlue" xml:space="preserve">
|
||||||
|
<value>Night Blue</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_IndigoViolet" xml:space="preserve">
|
||||||
|
<value>Indigo Violet</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Appearance_Colours_PresetsHint" xml:space="preserve">
|
||||||
|
<value>Tipp: Presets überschreiben deine aktuellen Channel-Farben sofort.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_PopOutInputEnabled_Name" xml:space="preserve">
|
||||||
|
<value>Eingabe in Pop-Outs aktivieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
||||||
|
<value>Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintText" xml:space="preserve">
|
||||||
|
<value>Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintAck" xml:space="preserve">
|
||||||
|
<value>Verstanden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintOpenSettings" xml:space="preserve">
|
||||||
|
<value>Fenster-Settings öffnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_Body" xml:space="preserve">
|
||||||
|
<value>Du kannst jeden Chat-Tab als eigenes Fenster öffnen. Klicke auf das Fenster-Symbol oben rechts oder rechtsklicke den Tab. Neu in v0.6.1: die Pop-Out-Eingabe ist standardmäßig aktiv (abschaltbar unter Einstellungen → Fenster).</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_Ack" xml:space="preserve">
|
||||||
|
<value>Verstanden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_OpenSettings" xml:space="preserve">
|
||||||
|
<value>Einstellungen öffnen</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
@@ -75,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>
|
||||||
@@ -174,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>
|
||||||
@@ -273,9 +270,6 @@
|
|||||||
<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>
|
||||||
@@ -366,4 +360,253 @@
|
|||||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||||
<value>Chat 2 community translators (upstream)</value>
|
<value>Chat 2 community translators (upstream)</value>
|
||||||
</data>
|
</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_OpenAsPopout_Name" xml:space="preserve">
|
||||||
|
<value>Open new /tell tabs directly as pop-out</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Description" xml:space="preserve">
|
||||||
|
<value>When enabled, each newly created /tell tab opens directly as its own window. Closing the window returns the tab to the sidebar.</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 & 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 & 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>
|
||||||
|
<data name="ChatColourPresets_Default" xml:space="preserve">
|
||||||
|
<value>ChatTwo Default</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
|
||||||
|
<value>High-Contrast</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_Pastell" xml:space="preserve">
|
||||||
|
<value>Pastell</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_DarkModeTuned" xml:space="preserve">
|
||||||
|
<value>Dark-Mode-Tuned</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_Hellion" xml:space="preserve">
|
||||||
|
<value>Hellion</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_NightBlue" xml:space="preserve">
|
||||||
|
<value>Night Blue</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatColourPresets_IndigoViolet" xml:space="preserve">
|
||||||
|
<value>Indigo Violet</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Appearance_Colours_PresetsHint" xml:space="preserve">
|
||||||
|
<value>Tip: presets overwrite your current channel colours immediately.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_PopOutInputEnabled_Name" xml:space="preserve">
|
||||||
|
<value>Enable input in pop-outs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
||||||
|
<value>Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintText" xml:space="preserve">
|
||||||
|
<value>New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintAck" xml:space="preserve">
|
||||||
|
<value>Got it</value>
|
||||||
|
</data>
|
||||||
|
<data name="Popout_v060_HintOpenSettings" xml:space="preserve">
|
||||||
|
<value>Open window settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_Body" xml:space="preserve">
|
||||||
|
<value>You can open any chat tab as its own window. Click the window icon in the top right or right-click the tab. New in v0.6.1: pop-out input is enabled by default (can be turned off under Settings → Window).</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_Ack" xml:space="preserve">
|
||||||
|
<value>Got it</value>
|
||||||
|
</data>
|
||||||
|
<data name="Hint_v061_PopOutHeader_OpenSettings" xml:space="preserve">
|
||||||
|
<value>Open Settings</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using ChatTwo.Code;
|
||||||
|
using ChatTwo.Util;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
|
||||||
|
namespace ChatTwo.Ui;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 input bar component for pop-out windows.
|
||||||
|
//
|
||||||
|
// Pragmatischer Refactor-Scope: Render() ist ein leerer Marker-Stub für
|
||||||
|
// das Hauptfenster — der bestehende Input-Layer in ChatLogWindow bleibt
|
||||||
|
// unangetastet, weil ein 400-Zeilen-Extract aus einem 1926-Zeilen-File
|
||||||
|
// das v0.6.0-Risiko unverhältnismäßig erhöhen würde. Pop-Outs nutzen
|
||||||
|
// ausschließlich RenderCompact(), das ist der ganze v0.6.0-Mehrwert.
|
||||||
|
// Sollte das Hauptfenster selber später eine Compact-Variante brauchen
|
||||||
|
// (oder das große Extract sich aus anderem Grund lohnen), kann Render()
|
||||||
|
// in einem späteren Cycle gefüllt werden.
|
||||||
|
public sealed class ChatInputBar
|
||||||
|
{
|
||||||
|
private readonly Plugin _plugin;
|
||||||
|
private readonly ChatLogWindow _host;
|
||||||
|
private readonly Func<Tab?> _activeTabAccessor;
|
||||||
|
private readonly InputState _state = new();
|
||||||
|
|
||||||
|
public ChatInputBar(Plugin plugin, ChatLogWindow host, Func<Tab?> activeTabAccessor)
|
||||||
|
{
|
||||||
|
_plugin = plugin;
|
||||||
|
_host = host;
|
||||||
|
_activeTabAccessor = activeTabAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputState State => _state;
|
||||||
|
public bool IsFocused { get; private set; }
|
||||||
|
|
||||||
|
// Stub. v0.6.0 belässt den Hauptfenster-Input wie er ist.
|
||||||
|
public void Render()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact rendering for pop-out windows.
|
||||||
|
//
|
||||||
|
// v0.6.0 Compact-Layout: Channel-Icon-Button links (Background-Farbe
|
||||||
|
// aus ChatColours), Text-Input rechts daneben. Auto-Translate-Picker
|
||||||
|
// ist bewusst NICHT im Compact-Mode (Spec-Abweichung Layout D → A).
|
||||||
|
// Rechtfertigung: das Hauptfenster-Auto-Complete-Popup ist nicht ohne
|
||||||
|
// grossen Refactor pro Window instanzierbar; typische Pop-Out-Use-Cases
|
||||||
|
// (FC-Greeter, Club-Hostess) brauchen Auto-Translate selten dort.
|
||||||
|
// Eigene Compact-Auto-Complete-Implementation kann ein späterer
|
||||||
|
// Cycle nachreichen wenn Tester-Feedback das verlangt.
|
||||||
|
//
|
||||||
|
// Channel-Switch wirkt via Plugin.Functions.Chat global (FFXIV-API).
|
||||||
|
// Pro Pop-Out unabhängig bleiben Text-Buffer und History-Cursor.
|
||||||
|
public void RenderCompact()
|
||||||
|
{
|
||||||
|
var tab = _activeTabAccessor();
|
||||||
|
if (tab == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawChannelIconButton(tab);
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawCompactInput(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCompactInput(Tab tab)
|
||||||
|
{
|
||||||
|
// Input takes the whole remaining width — no auto-translate button
|
||||||
|
// reserved on the right side in v0.6.0 (see RenderCompact comment).
|
||||||
|
var inputWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
if (inputWidth < 60f)
|
||||||
|
inputWidth = 60f;
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(inputWidth);
|
||||||
|
|
||||||
|
// CallbackHistory wires up Up/Down navigation against the shared
|
||||||
|
// InputHistoryService. Submit is detected the same way the main
|
||||||
|
// window does it: via IsItemDeactivated + Enter, NOT EnterReturnsTrue
|
||||||
|
// (matching v0.5.x ChatLogWindow.cs behavior).
|
||||||
|
const ImGuiInputTextFlags flags = ImGuiInputTextFlags.CallbackHistory;
|
||||||
|
ImGui.InputText($"##chat-compact-input-{tab.Identifier}", ref _state.Buffer, 500, flags, CompactCallback);
|
||||||
|
|
||||||
|
IsFocused = ImGui.IsItemActive();
|
||||||
|
|
||||||
|
if (ImGui.IsItemDeactivated()
|
||||||
|
&& (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter)))
|
||||||
|
{
|
||||||
|
SubmitCompact(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubmitCompact(Tab tab)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_state.Buffer))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = _state.Buffer;
|
||||||
|
_state.Buffer = string.Empty;
|
||||||
|
_state.HistoryCursor = -1;
|
||||||
|
_host.SendChatBoxFromExternal(tab, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// History-navigation callback for the compact input. Mirrors the main
|
||||||
|
// window's logic but operates on _state.HistoryCursor and the shared
|
||||||
|
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
||||||
|
// 0 = oldest, Count-1 = newest.
|
||||||
|
private unsafe int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||||
|
{
|
||||||
|
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var prev = _state.HistoryCursor;
|
||||||
|
switch (data.EventKey)
|
||||||
|
{
|
||||||
|
case ImGuiKey.UpArrow:
|
||||||
|
switch (_state.HistoryCursor)
|
||||||
|
{
|
||||||
|
case -1:
|
||||||
|
var offset = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(_state.Buffer))
|
||||||
|
{
|
||||||
|
InputHistoryService.Push(_state.Buffer);
|
||||||
|
offset = 1;
|
||||||
|
}
|
||||||
|
_state.HistoryCursor = InputHistoryService.Count - 1 - offset;
|
||||||
|
break;
|
||||||
|
case > 0:
|
||||||
|
_state.HistoryCursor--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ImGuiKey.DownArrow:
|
||||||
|
if (_state.HistoryCursor != -1)
|
||||||
|
if (++_state.HistoryCursor >= InputHistoryService.Count)
|
||||||
|
_state.HistoryCursor = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev == _state.HistoryCursor)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var historyStr = InputHistoryService.GetByCursor(_state.HistoryCursor) ?? string.Empty;
|
||||||
|
data.DeleteChars(0, data.BufTextLen);
|
||||||
|
data.InsertChars(0, historyStr);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawChannelIconButton(Tab tab)
|
||||||
|
{
|
||||||
|
var inputType = tab.CurrentChannel.UseTempChannel
|
||||||
|
? tab.CurrentChannel.TempChannel.ToChatType()
|
||||||
|
: tab.CurrentChannel.Channel.ToChatType();
|
||||||
|
|
||||||
|
var rgba = Plugin.Config.ChatColours.TryGetValue(inputType, out var c)
|
||||||
|
? c
|
||||||
|
: (inputType.DefaultColor() ?? 0xFFFFFFFFu);
|
||||||
|
var v3 = ColourUtil.RgbaToVector3(rgba);
|
||||||
|
var bg = new Vector4(v3.X, v3.Y, v3.Z, 1f);
|
||||||
|
|
||||||
|
// Compute readable foreground — black on bright, white on dark
|
||||||
|
var luminance = 0.2126f * v3.X + 0.7152f * v3.Y + 0.0722f * v3.Z;
|
||||||
|
var fg = luminance > 0.55f ? new Vector4(0f, 0f, 0f, 1f) : new Vector4(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
|
const string popupId = "chat-channel-picker-compact";
|
||||||
|
const float buttonSize = 22f;
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, bg))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, bg))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, bg))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, fg))
|
||||||
|
{
|
||||||
|
// Single-letter glyph derived from the channel — quick visual cue
|
||||||
|
// until we have a proper icon font available in the compact bar.
|
||||||
|
var label = ChannelGlyph(inputType);
|
||||||
|
if (ImGui.Button($"{label}##chan-compact", new Vector2(buttonSize, buttonSize)) && tab.Channel is null)
|
||||||
|
ImGui.OpenPopup(popupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.Channel is not null && ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip(Resources.Language.ChatLog_SwitcherDisabled);
|
||||||
|
}
|
||||||
|
else if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip(inputType.Name());
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var popup = ImRaii.Popup(popupId))
|
||||||
|
{
|
||||||
|
if (popup)
|
||||||
|
{
|
||||||
|
var channels = _host.GetValidChannels();
|
||||||
|
foreach (var (name, channel) in channels)
|
||||||
|
if (ImGui.Selectable(name))
|
||||||
|
_host.SetChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ChannelGlyph(ChatType type) => type switch
|
||||||
|
{
|
||||||
|
ChatType.Say => "S",
|
||||||
|
ChatType.Yell => "Y",
|
||||||
|
ChatType.Shout => "!",
|
||||||
|
ChatType.TellIncoming or ChatType.TellOutgoing => "T",
|
||||||
|
ChatType.Party or ChatType.CrossParty => "P",
|
||||||
|
ChatType.Alliance => "A",
|
||||||
|
ChatType.FreeCompany => "F",
|
||||||
|
ChatType.NoviceNetwork => "N",
|
||||||
|
ChatType.Linkshell1 => "1",
|
||||||
|
ChatType.Linkshell2 => "2",
|
||||||
|
ChatType.Linkshell3 => "3",
|
||||||
|
ChatType.Linkshell4 => "4",
|
||||||
|
ChatType.Linkshell5 => "5",
|
||||||
|
ChatType.Linkshell6 => "6",
|
||||||
|
ChatType.Linkshell7 => "7",
|
||||||
|
ChatType.Linkshell8 => "8",
|
||||||
|
ChatType.CrossLinkshell1 => "①",
|
||||||
|
ChatType.CrossLinkshell2 => "②",
|
||||||
|
ChatType.CrossLinkshell3 => "③",
|
||||||
|
ChatType.CrossLinkshell4 => "④",
|
||||||
|
ChatType.CrossLinkshell5 => "⑤",
|
||||||
|
ChatType.CrossLinkshell6 => "⑥",
|
||||||
|
ChatType.CrossLinkshell7 => "⑦",
|
||||||
|
ChatType.CrossLinkshell8 => "⑧",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forwards a tab-cycle keybind delta to the host so all windows
|
||||||
|
// navigate the same active-tab pointer (single source of truth).
|
||||||
|
public void HandleKeybindForward(int delta)
|
||||||
|
{
|
||||||
|
_host.ChangeTabDelta(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-window input state. Each ChatInputBar instance owns one of these
|
||||||
|
// so pop-outs and the main window keep independent buffers and channels
|
||||||
|
// (State-Sync-Entscheidung A in the v0.6.0 spec).
|
||||||
|
public sealed class InputState
|
||||||
|
{
|
||||||
|
public string Buffer = string.Empty;
|
||||||
|
public InputChannel? Channel;
|
||||||
|
public int HistoryCursor = -1;
|
||||||
|
}
|
||||||
+213
-17
@@ -44,7 +44,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
internal bool InputFocused { get; private set; }
|
internal bool InputFocused { get; private set; }
|
||||||
private int ActivatePos = -1;
|
private int ActivatePos = -1;
|
||||||
internal string Chat = string.Empty;
|
internal string Chat = string.Empty;
|
||||||
private readonly List<string> InputBacklog = [];
|
// Hellion Chat — v0.6.0 input history was extracted into
|
||||||
|
// InputHistoryService so pop-out windows with their own ChatInputBar
|
||||||
|
// share the same Up/Down history with the main window. The cursor
|
||||||
|
// stays window-local because each window navigates independently.
|
||||||
private int InputBacklogIdx = -1;
|
private int InputBacklogIdx = -1;
|
||||||
public bool TellSpecial;
|
public bool TellSpecial;
|
||||||
private readonly Stopwatch LastResize = new();
|
private readonly Stopwatch LastResize = new();
|
||||||
@@ -330,16 +333,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private void AddBacklog(string message)
|
private void AddBacklog(string message)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < InputBacklog.Count; i++)
|
// v0.6.0 — delegates to the shared InputHistoryService so pop-out
|
||||||
{
|
// ChatInputBar instances see the same history. Move-to-newest
|
||||||
if (InputBacklog[i] != message)
|
// deduplication lives inside the service.
|
||||||
continue;
|
InputHistoryService.Push(message);
|
||||||
|
|
||||||
InputBacklog.RemoveAt(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputBacklog.Add(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float GetRemainingHeightForMessageLog()
|
private float GetRemainingHeightForMessageLog()
|
||||||
@@ -350,6 +347,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Config.PreviewPosition is PreviewPosition.Inside)
|
if (Plugin.Config.PreviewPosition is PreviewPosition.Inside)
|
||||||
height -= Plugin.InputPreview.PreviewHeight;
|
height -= Plugin.InputPreview.PreviewHeight;
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — Header-Toolbar rendert auf Window-Ebene über
|
||||||
|
// einem horizontalen Layout-Pfad und wird von GetContentRegionAvail
|
||||||
|
// hier drin NICHT automatisch berücksichtigt, daher expliziter Abzug.
|
||||||
|
// Banner dagegen rendert in DrawChatLog VOR diesem ganzen Block und
|
||||||
|
// ImGui zieht seine Höhe automatisch von GetContentRegionAvail ab,
|
||||||
|
// weil der Cursor schon weiter unten steht — kein eigener Abzug.
|
||||||
|
height -= ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +456,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();
|
||||||
@@ -544,6 +551,12 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (IsChatMode && Plugin.InputPreview.IsDrawable)
|
if (IsChatMode && Plugin.InputPreview.IsDrawable)
|
||||||
Plugin.InputPreview.CalculatePreview();
|
Plugin.InputPreview.CalculatePreview();
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
|
||||||
|
// sits above the tab area / sidebar in full window width. Stash the
|
||||||
|
// height for GetRemainingHeightForMessageLog so the message log
|
||||||
|
// shrinks accordingly while the banner is visible.
|
||||||
|
_v061HintBannerHeight = DrawV061HintBannerIfNeeded();
|
||||||
|
|
||||||
if (Plugin.Config.SidebarTabView)
|
if (Plugin.Config.SidebarTabView)
|
||||||
DrawTabSidebar();
|
DrawTabSidebar();
|
||||||
else
|
else
|
||||||
@@ -923,6 +936,18 @@ public sealed class ChatLogWindow : Window
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v0.6.0 — pop-out windows route submission through this wrapper.
|
||||||
|
// The main-window Chat buffer is briefly used as a vehicle for
|
||||||
|
// SendChatBox (which reads it directly) and restored afterwards so
|
||||||
|
// the main window does not visibly lose any half-typed input.
|
||||||
|
internal void SendChatBoxFromExternal(Tab tab, string text)
|
||||||
|
{
|
||||||
|
var saved = Chat;
|
||||||
|
Chat = text;
|
||||||
|
SendChatBox(tab);
|
||||||
|
Chat = saved;
|
||||||
|
}
|
||||||
|
|
||||||
internal void SendChatBox(Tab activeTab)
|
internal void SendChatBox(Tab activeTab)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(Chat))
|
if (!string.IsNullOrWhiteSpace(Chat))
|
||||||
@@ -1188,7 +1213,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)
|
||||||
@@ -1278,6 +1309,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
TabSwitched(tab, previousTab);
|
TabSwitched(tab, previousTab);
|
||||||
|
|
||||||
tab.Unread = 0;
|
tab.Unread = 0;
|
||||||
|
DrawChatHeaderToolbar(tab);
|
||||||
DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), hasTabSwitched);
|
DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), hasTabSwitched);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,14 +1335,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)
|
||||||
@@ -1334,11 +1442,85 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab > -1)
|
if (currentTab > -1)
|
||||||
|
{
|
||||||
|
DrawChatHeaderToolbar(Plugin.Config.Tabs[currentTab]);
|
||||||
DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, hasTabSwitched);
|
DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, hasTabSwitched);
|
||||||
|
}
|
||||||
|
|
||||||
Plugin.WantedTab = null;
|
Plugin.WantedTab = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — visible pop-out trigger right above the message
|
||||||
|
// log so users discover the feature without having to right-click the tab.
|
||||||
|
// Renders only for the active tab in the main ChatLogWindow; pop-out
|
||||||
|
// windows have their own render path and skip this toolbar.
|
||||||
|
private void DrawChatHeaderToolbar(Tab tab)
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail().X;
|
||||||
|
var iconWidth = ImGui.GetFrameHeight();
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + avail - iconWidth);
|
||||||
|
|
||||||
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.WindowRestore, tooltip: Language.ChatLog_Tabs_PopOut))
|
||||||
|
{
|
||||||
|
tab.PopOut = true;
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — One-Time-Hint-Banner introducing the chat header
|
||||||
|
// pop-out toolbar button and the right-click pathway. Reuses the visual
|
||||||
|
// pattern from Popout.cs DrawHintBannerIfNeeded so users see a familiar
|
||||||
|
// dismiss-affordance. Returns the vertical space the banner consumed
|
||||||
|
// (0 when not shown) so the message log can shrink accordingly.
|
||||||
|
private float DrawV061HintBannerIfNeeded()
|
||||||
|
{
|
||||||
|
if (Plugin.Config.SeenPopOutHeaderHint)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
var hintText = Resources.HellionStrings.Hint_v061_PopOutHeader_Body;
|
||||||
|
var ackLabel = Resources.HellionStrings.Hint_v061_PopOutHeader_Ack;
|
||||||
|
var openLabel = Resources.HellionStrings.Hint_v061_PopOutHeader_OpenSettings;
|
||||||
|
|
||||||
|
var startY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
|
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
||||||
|
|
||||||
|
var dismiss = false;
|
||||||
|
var openSettings = false;
|
||||||
|
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
|
||||||
|
{
|
||||||
|
if (child)
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped(hintText);
|
||||||
|
if (ImGui.Button(ackLabel))
|
||||||
|
dismiss = true;
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button(openLabel))
|
||||||
|
{
|
||||||
|
dismiss = true;
|
||||||
|
openSettings = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
if (dismiss)
|
||||||
|
{
|
||||||
|
Plugin.Config.SeenPopOutHeaderHint = true;
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
|
||||||
|
if (openSettings)
|
||||||
|
Plugin.SettingsWindow.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImGui.GetCursorPosY() - startY;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawTabContextMenu(Tab tab, int i)
|
private void DrawTabContextMenu(Tab tab, int i)
|
||||||
{
|
{
|
||||||
using var contextMenu = ImRaii.ContextPopupItem($"tab-context-menu-{i}");
|
using var contextMenu = ImRaii.ContextPopupItem($"tab-context-menu-{i}");
|
||||||
@@ -1397,6 +1579,20 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
internal readonly List<bool> PopOutDocked = [];
|
internal readonly List<bool> PopOutDocked = [];
|
||||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||||
|
|
||||||
|
// Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the
|
||||||
|
// current frame, read by GetRemainingHeightForMessageLog so the message
|
||||||
|
// log can shrink. Unconditionally reassigned at the top of DrawChatLog
|
||||||
|
// (before any tab-area render) so the value is always in sync with the
|
||||||
|
// current frame. Returns 0 once the banner is dismissed.
|
||||||
|
private float _v061HintBannerHeight;
|
||||||
|
|
||||||
|
// v0.6.0 — live enumeration of all active Popout windows so the
|
||||||
|
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
|
||||||
|
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
|
||||||
|
// registered popouts.
|
||||||
|
internal IEnumerable<Popout> ActivePopouts =>
|
||||||
|
Plugin.WindowSystem.Windows.OfType<Popout>().Where(p => p.IsOpen);
|
||||||
private void AddPopOutsToDraw()
|
private void AddPopOutsToDraw()
|
||||||
{
|
{
|
||||||
HandlerLender.ResetCounter();
|
HandlerLender.ResetCounter();
|
||||||
@@ -1673,7 +1869,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
offset = 1;
|
offset = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputBacklogIdx = InputBacklog.Count - 1 - offset;
|
InputBacklogIdx = InputHistoryService.Count - 1 - offset;
|
||||||
break;
|
break;
|
||||||
case > 0:
|
case > 0:
|
||||||
InputBacklogIdx--;
|
InputBacklogIdx--;
|
||||||
@@ -1682,7 +1878,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
break;
|
break;
|
||||||
case ImGuiKey.DownArrow:
|
case ImGuiKey.DownArrow:
|
||||||
if (InputBacklogIdx != -1)
|
if (InputBacklogIdx != -1)
|
||||||
if (++InputBacklogIdx >= InputBacklog.Count)
|
if (++InputBacklogIdx >= InputHistoryService.Count)
|
||||||
InputBacklogIdx = -1;
|
InputBacklogIdx = -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1690,7 +1886,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (prevPos == InputBacklogIdx)
|
if (prevPos == InputBacklogIdx)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var historyStr = InputBacklogIdx >= 0 ? InputBacklog[InputBacklogIdx] : string.Empty;
|
var historyStr = InputHistoryService.GetByCursor(InputBacklogIdx) ?? string.Empty;
|
||||||
data.DeleteChars(0, data.BufTextLen);
|
data.DeleteChars(0, data.BufTextLen);
|
||||||
data.InsertChars(0, historyStr);
|
data.InsertChars(0, historyStr);
|
||||||
|
|
||||||
|
|||||||
+104
-3
@@ -15,6 +15,18 @@ internal class Popout : Window
|
|||||||
private long FrameTime; // set every frame
|
private long FrameTime; // set every frame
|
||||||
private long LastActivityTime = Environment.TickCount64;
|
private long LastActivityTime = Environment.TickCount64;
|
||||||
|
|
||||||
|
// v0.6.0 — optional input bar inside the pop-out window. Lazy-allocated
|
||||||
|
// when the user enables Tab.PopOutInputEnabled and torn down when the
|
||||||
|
// toggle is turned off (independent text buffer is intentionally
|
||||||
|
// discarded — see v0.6.0 spec edge-case P1).
|
||||||
|
public ChatInputBar? InputBar { get; private set; }
|
||||||
|
public bool HasFocusedInputBar => InputBar?.IsFocused ?? false;
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.1 expose just the tab identifier (not the whole Tab
|
||||||
|
// reference) so AutoTellTabsService.DropOldestTempTab can locate the
|
||||||
|
// matching pop-out window when an LRU temp tab gets evicted.
|
||||||
|
internal Guid TabIdentifier => Tab.Identifier;
|
||||||
|
|
||||||
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout")
|
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout")
|
||||||
{
|
{
|
||||||
ChatLogWindow = chatLogWindow;
|
ChatLogWindow = chatLogWindow;
|
||||||
@@ -70,8 +82,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,13 +105,94 @@ internal class Popout : Window
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v0.6.0 — one-time hint banner explaining the new pop-out input
|
||||||
|
// feature. Shown once per user; "Got it" or "Open settings"
|
||||||
|
// dismisses it and persists the flag.
|
||||||
|
var hintBannerHeight = DrawHintBannerIfNeeded();
|
||||||
|
|
||||||
|
// v0.6.0 — pop-out optional input bar. Reserve height first so the
|
||||||
|
// message log draws into the right region; only shown when the
|
||||||
|
// global master switch is on. Toggle-OFF resets InputBar so the
|
||||||
|
// next toggle-ON gives a fresh buffer (no stale text persists).
|
||||||
|
var inputEnabled = Plugin.Config.PopOutInputEnabled;
|
||||||
|
if (!inputEnabled && InputBar != null)
|
||||||
|
{
|
||||||
|
InputBar = null;
|
||||||
|
}
|
||||||
|
if (inputEnabled)
|
||||||
|
{
|
||||||
|
InputBar ??= new ChatInputBar(ChatLogWindow.Plugin, ChatLogWindow, () => Tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputBarHeight = inputEnabled
|
||||||
|
? ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y
|
||||||
|
: 0f;
|
||||||
|
|
||||||
var handler = ChatLogWindow.HandlerLender.Borrow();
|
var handler = ChatLogWindow.HandlerLender.Borrow();
|
||||||
ChatLogWindow.DrawMessageLog(Tab, handler, ImGui.GetContentRegionAvail().Y, false);
|
var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight;
|
||||||
|
ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false);
|
||||||
|
|
||||||
|
if (inputEnabled && InputBar != null)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
InputBar.RenderCompact();
|
||||||
|
}
|
||||||
|
|
||||||
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
|
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
|
||||||
LastActivityTime = FrameTime;
|
LastActivityTime = FrameTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the vertical space the banner consumed (0 when not shown)
|
||||||
|
// so the message log can shrink accordingly.
|
||||||
|
private float DrawHintBannerIfNeeded()
|
||||||
|
{
|
||||||
|
if (Plugin.Config.SeenPopOutInputHint)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
var hintText = Resources.HellionStrings.Popout_v060_HintText;
|
||||||
|
var ackLabel = Resources.HellionStrings.Popout_v060_HintAck;
|
||||||
|
var openLabel = Resources.HellionStrings.Popout_v060_HintOpenSettings;
|
||||||
|
|
||||||
|
var startY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
|
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
||||||
|
|
||||||
|
var dismiss = false;
|
||||||
|
var openSettings = false;
|
||||||
|
using (var child = ImRaii.Child("##v060-pop-out-hint", new System.Numerics.Vector2(0f, 64f), true))
|
||||||
|
{
|
||||||
|
if (child)
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped(hintText);
|
||||||
|
if (ImGui.Button(ackLabel))
|
||||||
|
dismiss = true;
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button(openLabel))
|
||||||
|
{
|
||||||
|
dismiss = true;
|
||||||
|
openSettings = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
if (dismiss)
|
||||||
|
{
|
||||||
|
Plugin.Config.SeenPopOutInputHint = true;
|
||||||
|
ChatLogWindow.Plugin.SaveConfig();
|
||||||
|
Plugin.Log.Debug("Pop-Out input hint dismissed");
|
||||||
|
if (openSettings)
|
||||||
|
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImGui.GetCursorPosY() - startY;
|
||||||
|
}
|
||||||
|
|
||||||
public override void PostDraw()
|
public override void PostDraw()
|
||||||
{
|
{
|
||||||
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
||||||
|
|||||||
+13
-15
@@ -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;
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,128 +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);
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// The translator list lives at the bottom of the About tab. Render
|
|
||||||
// it directly inside the parent scroll container instead of a
|
|
||||||
// fixed-height child — the previous "remaining space" calculation
|
|
||||||
// shrank to zero (or below) once the About copy grew, which made
|
|
||||||
// the section unreachable on smaller settings windows.
|
|
||||||
using (var treeNode = ImRaii.TreeNode(HellionStrings.About_Translators_TreeNode))
|
|
||||||
{
|
|
||||||
if (treeNode)
|
|
||||||
{
|
|
||||||
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
|
||||||
foreach (var translator in Translators)
|
|
||||||
ImGui.TextUnformatted(translator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.Spacing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
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.5–1.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))
|
||||||
|
{
|
||||||
|
DrawColourPresetButtons();
|
||||||
|
ImGui.TextDisabled(HellionStrings.Settings_Appearance_Colours_PresetsHint);
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat — v0.6.0 preset-buttons row above the per-channel colour
|
||||||
|
// editors. Apply is immediate and overwrites every channel covered by
|
||||||
|
// the preset; channels not in the preset keep their current colour.
|
||||||
|
private void DrawColourPresetButtons()
|
||||||
|
{
|
||||||
|
var first = true;
|
||||||
|
foreach (var (_, preset) in ChatColourPresets.All)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
if (preset.IsBrandPreset)
|
||||||
|
{
|
||||||
|
// Hellion-Brand visuell hervorheben — blau-violetter Frame-Akzent
|
||||||
|
var border = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(255, 128, 200));
|
||||||
|
var btn = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(74, 42, 106));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Border, new System.Numerics.Vector4(border.X, border.Y, border.Z, 1f));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Button, new System.Numerics.Vector4(btn.X, btn.Y, btn.Z, 1f));
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button(GetPresetLabel(preset)))
|
||||||
|
{
|
||||||
|
ApplyPreset(preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.IsBrandPreset)
|
||||||
|
{
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
ImGui.PopStyleColor(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localized label for a preset; falls back to DisplayName if the i18n
|
||||||
|
// key is missing (defensive — the resource manager returns the key
|
||||||
|
// string itself when a lookup fails).
|
||||||
|
private static string GetPresetLabel(ChatColourPreset preset)
|
||||||
|
{
|
||||||
|
var localized = HellionStrings.ResourceManager.GetString(preset.LocalizationKey, HellionStrings.Culture);
|
||||||
|
return string.IsNullOrEmpty(localized) ? preset.DisplayName : localized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyPreset(ChatColourPreset preset)
|
||||||
|
{
|
||||||
|
foreach (var (channel, colour) in preset.Colours)
|
||||||
|
{
|
||||||
|
Mutable.ChatColours[channel] = colour;
|
||||||
|
}
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
GlobalParametersCache.Refresh();
|
||||||
|
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
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_OpenAsPopout_Name, ref Mutable.AutoTellTabsOpenAsPopout);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,30 +33,47 @@ 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();
|
||||||
|
ImGui.Spacing();
|
||||||
|
DrawStatsSection();
|
||||||
|
}
|
||||||
|
|
||||||
if (ImGuiUtil.OptionCheckbox(ref Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description))
|
private void DrawStorageSection()
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Storage_Heading);
|
||||||
|
if (!tree.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
|
{
|
||||||
|
ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages);
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description);
|
||||||
|
|
||||||
|
if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession))
|
||||||
if (Mutable.LoadPreviousSession)
|
if (Mutable.LoadPreviousSession)
|
||||||
Mutable.FilterIncludePreviousSessions = true;
|
Mutable.FilterIncludePreviousSessions = true;
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description);
|
||||||
|
|
||||||
ImGui.Spacing();
|
if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions))
|
||||||
|
|
||||||
if (ImGuiUtil.OptionCheckbox(ref Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description))
|
|
||||||
if (!Mutable.FilterIncludePreviousSessions)
|
if (!Mutable.FilterIncludePreviousSessions)
|
||||||
Mutable.LoadPreviousSession = false;
|
Mutable.LoadPreviousSession = false;
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description);
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
|
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"));
|
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
|
||||||
if (old.Exists || migratedOld.Exists)
|
if (old.Exists || migratedOld.Exists)
|
||||||
{
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
|
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
@@ -76,13 +93,16 @@ internal sealed class Database : ISettingsTab
|
|||||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ImGui.Spacing();
|
}
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TextUnformatted(Language.Options_Database_Metadata_Heading);
|
private void DrawViewerSection()
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Viewer_Heading);
|
||||||
|
if (!tree.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
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,13 +152,19 @@ 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);
|
||||||
|
if (!tree.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
|
{
|
||||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||||
|
|
||||||
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
|
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
|
||||||
@@ -153,7 +179,7 @@ internal sealed class Database : ISettingsTab
|
|||||||
|
|
||||||
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
|
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
|
||||||
new Thread(() => InsertMessages(10_000)).Start();
|
new Thread(() => InsertMessages(10_000)).Start();
|
||||||
ImGui.Spacing();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertMessages(int count)
|
private void InsertMessages(int count)
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,6 +56,8 @@ 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;
|
||||||
|
|
||||||
// The retention-running state lives on Plugin so the auto-sweep and
|
// The retention-running state lives on Plugin so the auto-sweep and
|
||||||
// this manual button see the same flag. UI reads stay lock-free
|
// this manual button see the same flag. UI reads stay lock-free
|
||||||
@@ -73,41 +77,67 @@ 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.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
DrawRetentionSection();
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
DrawCleanupSection();
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
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(
|
ImGuiUtil.OptionCheckbox(
|
||||||
ref Mutable.PrivacyFilterEnabled,
|
ref Mutable.PrivacyFilterEnabled,
|
||||||
HellionStrings.Privacy_FilterEnabled_Name,
|
HellionStrings.Privacy_FilterEnabled_Name,
|
||||||
HellionStrings.Privacy_FilterEnabled_Description);
|
HellionStrings.Privacy_FilterEnabled_Description);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Privacy_FilterEnabled_StorageOnly_Help);
|
||||||
ImGuiUtil.HelpText(HellionStrings.Privacy_FilterEnabled_StorageOnly_Help);
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
@@ -138,8 +168,8 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
|
|
||||||
foreach (var (heading, types) in Groups)
|
foreach (var (heading, types) in Groups)
|
||||||
{
|
{
|
||||||
using var tree = ImRaii.TreeNode(heading());
|
using var groupTree = ImRaii.TreeNode(heading());
|
||||||
if (!tree.Success)
|
if (!groupTree.Success)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
@@ -168,24 +198,7 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
HellionStrings.Privacy_PersistUnknown_Name,
|
HellionStrings.Privacy_PersistUnknown_Name,
|
||||||
HellionStrings.Privacy_PersistUnknown_Description);
|
HellionStrings.Privacy_PersistUnknown_Description);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
DrawRetentionSection();
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
DrawCleanupSection();
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
DrawExportSection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawExportSection()
|
private void DrawExportSection()
|
||||||
@@ -335,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();
|
||||||
|
|
||||||
@@ -393,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))
|
||||||
@@ -471,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))
|
||||||
@@ -483,10 +514,22 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CleanupPreviewStale)
|
||||||
|
{
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
using (var staleColor = CleanupPreviewStale
|
||||||
|
? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
|
||||||
|
: null)
|
||||||
|
{
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
|
||||||
|
}
|
||||||
|
|
||||||
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
|
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
|
||||||
{
|
{
|
||||||
@@ -542,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
// v0.6.0 — global master switch for the pop-out input bar.
|
||||||
|
ImGui.Checkbox(HellionStrings.Settings_Window_PopOutInputEnabled_Name, ref Mutable.PopOutInputEnabled);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_PopOutInputEnabled_Description);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
+164
-89
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Buffers;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ChatTwo.Code;
|
using ChatTwo.Code;
|
||||||
@@ -58,13 +59,145 @@ internal static class ImGuiUtil
|
|||||||
handler.Click(chunk, payload, button);
|
handler.Click(chunk, payload, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static unsafe void WrapText(string csText, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
// Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
|
||||||
|
// most 4 bytes per char; ImGui's internal ImString limit is well below
|
||||||
|
// this and FFXIV's chat lines top out around a few hundred chars in
|
||||||
|
// practice. The cap prevents an unbounded ArrayPool rent if a caller
|
||||||
|
// ever feeds in a degenerate input.
|
||||||
|
private const int MaxLineByteCount = 16 * 1024;
|
||||||
|
|
||||||
|
internal static void WrapText(string csText, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
||||||
{
|
{
|
||||||
void Text(byte* text, byte* textEnd)
|
if (csText.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
||||||
|
{
|
||||||
|
if (part.Length == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate against the encoder's own MaxByteCount so the buffer
|
||||||
|
// we hand to ImGui is sized by us. The actual byte count
|
||||||
|
// returned by GetBytes is then validated against that ceiling
|
||||||
|
// before any pointer arithmetic touches it; CodeQL recognises
|
||||||
|
// that comparison as a sanitiser for the
|
||||||
|
// cs/unvalidated-local-pointer-arithmetic taint flow.
|
||||||
|
var maxBytes = Encoding.UTF8.GetMaxByteCount(part.Length);
|
||||||
|
if (maxBytes <= 0 || maxBytes > MaxLineByteCount)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(maxBytes);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var written = Encoding.UTF8.GetBytes(part, 0, part.Length, buffer, 0);
|
||||||
|
if (written <= 0 || written > maxBytes)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void WrapEncodedLine(ReadOnlySpan<byte> bytes, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
||||||
|
{
|
||||||
|
var byteCount = bytes.Length;
|
||||||
|
if (byteCount == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (byte* basePtr = bytes)
|
||||||
|
{
|
||||||
|
var widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
var endPrev = CalcWordWrap(basePtr, 0, byteCount, widthLeft);
|
||||||
|
if (endPrev < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var firstSpace = FindFirstSpace(bytes, 0, byteCount);
|
||||||
|
var properBreak = firstSpace <= endPrev;
|
||||||
|
if (properBreak)
|
||||||
|
{
|
||||||
|
DrawText(basePtr, 0, endPrev, chunk, handler, defaultText);
|
||||||
|
}
|
||||||
|
else if (lineWidth == 0f)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Check whether the next chunk would wrap at or past the
|
||||||
|
// first space. If yes, force a line break.
|
||||||
|
var wrapPos = CalcWordWrap(basePtr, 0, firstSpace, lineWidth);
|
||||||
|
if (wrapPos >= firstSpace)
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
}
|
||||||
|
|
||||||
|
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
var lineStart = 0;
|
||||||
|
while (endPrev < byteCount)
|
||||||
|
{
|
||||||
|
if (properBreak)
|
||||||
|
lineStart = endPrev;
|
||||||
|
|
||||||
|
// Skip a leading space at the start of a wrapped line.
|
||||||
|
if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
|
||||||
|
lineStart++;
|
||||||
|
|
||||||
|
var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
|
||||||
|
if (properBreak && newEnd == endPrev)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (newEnd < 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
endPrev = newEnd;
|
||||||
|
DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);
|
||||||
|
|
||||||
|
if (!properBreak)
|
||||||
|
{
|
||||||
|
properBreak = true;
|
||||||
|
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
|
||||||
|
{
|
||||||
|
var result = ImGuiNative.CalcWordWrapPositionA(
|
||||||
|
ImGui.GetFont().Handle,
|
||||||
|
ImGuiHelpers.GlobalScale,
|
||||||
|
basePtr + start,
|
||||||
|
basePtr + end,
|
||||||
|
width);
|
||||||
|
if (result == null)
|
||||||
|
return -1;
|
||||||
|
return (int)(result - basePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void DrawText(byte* basePtr, int start, int end, Chunk chunk, PayloadHandler? handler, Vector4 defaultText)
|
||||||
{
|
{
|
||||||
var oldPos = ImGui.GetCursorScreenPos();
|
var oldPos = ImGui.GetCursorScreenPos();
|
||||||
|
|
||||||
ImGuiNative.TextUnformatted(text, textEnd);
|
ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
|
||||||
PostPayload(chunk, handler);
|
PostPayload(chunk, handler);
|
||||||
|
|
||||||
if (!ReferenceEquals(LastLink, chunk.Link))
|
if (!ReferenceEquals(LastLink, chunk.Link))
|
||||||
@@ -78,8 +211,8 @@ internal static class ImGuiUtil
|
|||||||
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
||||||
|
|
||||||
foreach (var (start, size) in PayloadBounds)
|
foreach (var (boundsStart, boundsSize) in PayloadBounds)
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(start, start + size, actualCol);
|
ImGui.GetWindowDrawList().AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);
|
||||||
|
|
||||||
PayloadBounds.Clear();
|
PayloadBounds.Clear();
|
||||||
}
|
}
|
||||||
@@ -88,93 +221,13 @@ internal static class ImGuiUtil
|
|||||||
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (csText.Length == 0)
|
private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
|
||||||
{
|
{
|
||||||
var bytes = Encoding.UTF8.GetBytes(part);
|
for (var i = start; i < end; i++)
|
||||||
fixed (byte* rawText = bytes)
|
if (char.IsWhiteSpace((char)bytes[i]))
|
||||||
{
|
|
||||||
var text = rawText;
|
|
||||||
var textEnd = text + bytes.Length;
|
|
||||||
|
|
||||||
// empty string
|
|
||||||
if (text == null)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var widthLeft = ImGui.GetContentRegionAvail().X;
|
|
||||||
var endPrevLine = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft);
|
|
||||||
if (endPrevLine == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var firstSpace = FindFirstSpace(text, textEnd);
|
|
||||||
var properBreak = firstSpace <= endPrevLine;
|
|
||||||
if (properBreak)
|
|
||||||
{
|
|
||||||
Text(text, endPrevLine);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (lineWidth == 0f)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// check if the next bit is longer than the entire line width
|
|
||||||
var wrapPos = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, firstSpace, lineWidth);
|
|
||||||
|
|
||||||
// only go to next line is it's going to wrap at the space
|
|
||||||
if (wrapPos >= firstSpace)
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
|
||||||
while (endPrevLine < textEnd)
|
|
||||||
{
|
|
||||||
if (properBreak)
|
|
||||||
text = endPrevLine;
|
|
||||||
|
|
||||||
// skip a space at start of line
|
|
||||||
if (*text == ' ')
|
|
||||||
++text;
|
|
||||||
|
|
||||||
var newEnd = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft);
|
|
||||||
if (properBreak && newEnd == endPrevLine)
|
|
||||||
break;
|
|
||||||
|
|
||||||
endPrevLine = newEnd;
|
|
||||||
if (endPrevLine == null)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(text, endPrevLine);
|
|
||||||
|
|
||||||
if (!properBreak)
|
|
||||||
{
|
|
||||||
properBreak = true;
|
|
||||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe byte* FindFirstSpace(byte* text, byte* textEnd)
|
|
||||||
{
|
|
||||||
for (var i = text; i < textEnd; i++)
|
|
||||||
if (char.IsWhiteSpace((char) *i))
|
|
||||||
return i;
|
return i;
|
||||||
|
|
||||||
return textEnd;
|
return end;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IconButton(FontAwesomeIcon icon, string? id = null, string? tooltip = null, int width = 0)
|
internal static bool IconButton(FontAwesomeIcon icon, string? id = null, string? tooltip = null, int width = 0)
|
||||||
@@ -215,6 +268,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();
|
||||||
|
|||||||
+107
-29
@@ -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,105 @@ 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),
|
||||||
|
},
|
||||||
|
// No automatic input-channel switch; the Gruppe tab is a read
|
||||||
|
// surface that pulls in Party, CrossParty, Alliance and PvpTeam
|
||||||
|
// together. Auto-routing /party into this tab would surprise the
|
||||||
|
// user when they actually wanted /alliance or /pvpteam.
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
+20
-20
@@ -27,13 +27,13 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Data.Sqlite": {
|
"Microsoft.Data.Sqlite": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.0, )",
|
"requested": "[10.0.7, )",
|
||||||
"resolved": "9.0.0",
|
"resolved": "10.0.7",
|
||||||
"contentHash": "lw6wthgXGx3r/U775k1UkUAWIn0kAT0wj4ZRq0WlhPx4WAOiBsIjgDKgWkXcNTGT0KfHiClkM+tyPVFDvxeObw==",
|
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Data.Sqlite.Core": "9.0.0",
|
"Microsoft.Data.Sqlite.Core": "10.0.7",
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.10",
|
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||||
"SQLitePCLRaw.core": "2.1.10"
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"morelinq": {
|
"morelinq": {
|
||||||
@@ -66,10 +66,10 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Data.Sqlite.Core": {
|
"Microsoft.Data.Sqlite.Core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "9.0.0",
|
"resolved": "10.0.7",
|
||||||
"contentHash": "cFfZjFL+tqzGYw9lB31EkV1IWF5xRQNk2k+MQd+Cf86Gl6zTeAoiZIFw5sRB1Z8OxpEC7nu+nTDsLSjieBAPTw==",
|
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.core": "2.1.10"
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.NET.StringTools": {
|
"Microsoft.NET.StringTools": {
|
||||||
@@ -79,29 +79,29 @@
|
|||||||
},
|
},
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.10",
|
"resolved": "2.1.11",
|
||||||
"contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==",
|
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.10",
|
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.10"
|
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SQLitePCLRaw.core": {
|
"SQLitePCLRaw.core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.10",
|
"resolved": "2.1.11",
|
||||||
"contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw=="
|
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
||||||
},
|
},
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.10",
|
"resolved": "2.1.11",
|
||||||
"contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA=="
|
"contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ=="
|
||||||
},
|
},
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.10",
|
"resolved": "2.1.11",
|
||||||
"contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==",
|
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.core": "2.1.10"
|
"SQLitePCLRaw.core": "2.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
|
||||||
EUPL © the European Union 2007, 2016
|
|
||||||
|
|
||||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
|
||||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
|
||||||
other than as authorised under this Licence is prohibited (to the extent such
|
|
||||||
use is covered by a right of the copyright holder of the Work).
|
|
||||||
|
|
||||||
The Work is provided under the terms of this Licence when the Licensor (as
|
|
||||||
defined below) has placed the following notice immediately following the
|
|
||||||
copyright notice for the Work:
|
|
||||||
|
|
||||||
Licensed under the EUPL
|
|
||||||
|
|
||||||
or has expressed by any other means his willingness to license under the EUPL.
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
|
|
||||||
In this Licence, the following terms have the following meaning:
|
|
||||||
|
|
||||||
- ‘The Licence’: this Licence.
|
|
||||||
|
|
||||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
|
||||||
Licensor under this Licence, available as Source Code and also as Executable
|
|
||||||
Code as the case may be.
|
|
||||||
|
|
||||||
- ‘Derivative Works’: the works or software that could be created by the
|
|
||||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
|
||||||
does not define the extent of modification or dependence on the Original Work
|
|
||||||
required in order to classify a work as a Derivative Work; this extent is
|
|
||||||
determined by copyright law applicable in the country mentioned in Article 15.
|
|
||||||
|
|
||||||
- ‘The Work’: the Original Work or its Derivative Works.
|
|
||||||
|
|
||||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
|
||||||
convenient for people to study and modify.
|
|
||||||
|
|
||||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
|
||||||
meant to be interpreted by a computer as a program.
|
|
||||||
|
|
||||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
|
||||||
the Work under the Licence.
|
|
||||||
|
|
||||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
|
||||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
|
||||||
|
|
||||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
|
||||||
the Work under the terms of the Licence.
|
|
||||||
|
|
||||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
|
||||||
renting, distributing, communicating, transmitting, or otherwise making
|
|
||||||
available, online or offline, copies of the Work or providing access to its
|
|
||||||
essential functionalities at the disposal of any other natural or legal
|
|
||||||
person.
|
|
||||||
|
|
||||||
2. Scope of the rights granted by the Licence
|
|
||||||
|
|
||||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
|
||||||
sublicensable licence to do the following, for the duration of copyright vested
|
|
||||||
in the Original Work:
|
|
||||||
|
|
||||||
- use the Work in any circumstance and for all usage,
|
|
||||||
- reproduce the Work,
|
|
||||||
- modify the Work, and make Derivative Works based upon the Work,
|
|
||||||
- communicate to the public, including the right to make available or display
|
|
||||||
the Work or copies thereof to the public and perform publicly, as the case may
|
|
||||||
be, the Work,
|
|
||||||
- distribute the Work or copies thereof,
|
|
||||||
- lend and rent the Work or copies thereof,
|
|
||||||
- sublicense rights in the Work or copies thereof.
|
|
||||||
|
|
||||||
Those rights can be exercised on any media, supports and formats, whether now
|
|
||||||
known or later invented, as far as the applicable law permits so.
|
|
||||||
|
|
||||||
In the countries where moral rights apply, the Licensor waives his right to
|
|
||||||
exercise his moral right to the extent allowed by law in order to make effective
|
|
||||||
the licence of the economic rights here above listed.
|
|
||||||
|
|
||||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
|
||||||
any patents held by the Licensor, to the extent necessary to make use of the
|
|
||||||
rights granted on the Work under this Licence.
|
|
||||||
|
|
||||||
3. Communication of the Source Code
|
|
||||||
|
|
||||||
The Licensor may provide the Work either in its Source Code form, or as
|
|
||||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
|
||||||
provides in addition a machine-readable copy of the Source Code of the Work
|
|
||||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
|
||||||
a notice following the copyright notice attached to the Work, a repository where
|
|
||||||
the Source Code is easily and freely accessible for as long as the Licensor
|
|
||||||
continues to distribute or communicate the Work.
|
|
||||||
|
|
||||||
4. Limitations on copyright
|
|
||||||
|
|
||||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
|
||||||
any exception or limitation to the exclusive rights of the rights owners in the
|
|
||||||
Work, of the exhaustion of those rights or of other applicable limitations
|
|
||||||
thereto.
|
|
||||||
|
|
||||||
5. Obligations of the Licensee
|
|
||||||
|
|
||||||
The grant of the rights mentioned above is subject to some restrictions and
|
|
||||||
obligations imposed on the Licensee. Those obligations are the following:
|
|
||||||
|
|
||||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
|
||||||
trademarks notices and all notices that refer to the Licence and to the
|
|
||||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
|
||||||
copy of the Licence with every copy of the Work he/she distributes or
|
|
||||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
|
||||||
notices stating that the Work has been modified and the date of modification.
|
|
||||||
|
|
||||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
|
||||||
Original Works or Derivative Works, this Distribution or Communication will be
|
|
||||||
done under the terms of this Licence or of a later version of this Licence
|
|
||||||
unless the Original Work is expressly distributed only under this version of the
|
|
||||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
|
||||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
|
||||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
|
||||||
|
|
||||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
|
||||||
Works or copies thereof based upon both the Work and another work licensed under
|
|
||||||
a Compatible Licence, this Distribution or Communication can be done under the
|
|
||||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
|
||||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
|
||||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
|
||||||
his/her obligations under this Licence, the obligations of the Compatible
|
|
||||||
Licence shall prevail.
|
|
||||||
|
|
||||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
|
||||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
|
||||||
a repository where this Source will be easily and freely available for as long
|
|
||||||
as the Licensee continues to distribute or communicate the Work.
|
|
||||||
|
|
||||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
|
||||||
trademarks, service marks, or names of the Licensor, except as required for
|
|
||||||
reasonable and customary use in describing the origin of the Work and
|
|
||||||
reproducing the content of the copyright notice.
|
|
||||||
|
|
||||||
6. Chain of Authorship
|
|
||||||
|
|
||||||
The original Licensor warrants that the copyright in the Original Work granted
|
|
||||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
|
||||||
power and authority to grant the Licence.
|
|
||||||
|
|
||||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
|
||||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
|
||||||
power and authority to grant the Licence.
|
|
||||||
|
|
||||||
Each time You accept the Licence, the original Licensor and subsequent
|
|
||||||
Contributors grant You a licence to their contributions to the Work, under the
|
|
||||||
terms of this Licence.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty
|
|
||||||
|
|
||||||
The Work is a work in progress, which is continuously improved by numerous
|
|
||||||
Contributors. It is not a finished work and may therefore contain defects or
|
|
||||||
‘bugs’ inherent to this type of development.
|
|
||||||
|
|
||||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
|
||||||
and without warranties of any kind concerning the Work, including without
|
|
||||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
|
||||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
|
||||||
copyright as stated in Article 6 of this Licence.
|
|
||||||
|
|
||||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
|
||||||
for the grant of any rights to the Work.
|
|
||||||
|
|
||||||
8. Disclaimer of Liability
|
|
||||||
|
|
||||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
|
||||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
|
||||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
|
||||||
of the Work, including without limitation, damages for loss of goodwill, work
|
|
||||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
|
||||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
|
||||||
However, the Licensor will be liable under statutory product liability laws as
|
|
||||||
far such laws apply to the Work.
|
|
||||||
|
|
||||||
9. Additional agreements
|
|
||||||
|
|
||||||
While distributing the Work, You may choose to conclude an additional agreement,
|
|
||||||
defining obligations or services consistent with this Licence. However, if
|
|
||||||
accepting obligations, You may act only on your own behalf and on your sole
|
|
||||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
|
||||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
|
||||||
for any liability incurred by, or claims asserted against such Contributor by
|
|
||||||
the fact You have accepted any warranty or additional liability.
|
|
||||||
|
|
||||||
10. Acceptance of the Licence
|
|
||||||
|
|
||||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
|
||||||
placed under the bottom of a window displaying the text of this Licence or by
|
|
||||||
affirming consent in any other similar way, in accordance with the rules of
|
|
||||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
|
||||||
acceptance of this Licence and all of its terms and conditions.
|
|
||||||
|
|
||||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
|
||||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
|
||||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
|
||||||
Distribution or Communication by You of the Work or copies thereof.
|
|
||||||
|
|
||||||
11. Information to the public
|
|
||||||
|
|
||||||
In case of any Distribution or Communication of the Work by means of electronic
|
|
||||||
communication by You (for example, by offering to download the Work from a
|
|
||||||
remote location) the distribution channel or media (for example, a website) must
|
|
||||||
at least provide to the public the information requested by the applicable law
|
|
||||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
|
||||||
stored and reproduced by the Licensee.
|
|
||||||
|
|
||||||
12. Termination of the Licence
|
|
||||||
|
|
||||||
The Licence and the rights granted hereunder will terminate automatically upon
|
|
||||||
any breach by the Licensee of the terms of the Licence.
|
|
||||||
|
|
||||||
Such a termination will not terminate the licences of any person who has
|
|
||||||
received the Work from the Licensee under the Licence, provided such persons
|
|
||||||
remain in full compliance with the Licence.
|
|
||||||
|
|
||||||
13. Miscellaneous
|
|
||||||
|
|
||||||
Without prejudice of Article 9 above, the Licence represents the complete
|
|
||||||
agreement between the Parties as to the Work.
|
|
||||||
|
|
||||||
If any provision of the Licence is invalid or unenforceable under applicable
|
|
||||||
law, this will not affect the validity or enforceability of the Licence as a
|
|
||||||
whole. Such provision will be construed or reformed so as necessary to make it
|
|
||||||
valid and enforceable.
|
|
||||||
|
|
||||||
The European Commission may publish other linguistic versions or new versions of
|
|
||||||
this Licence or updated versions of the Appendix, so far this is required and
|
|
||||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
|
||||||
versions of the Licence will be published with a unique version number.
|
|
||||||
|
|
||||||
All linguistic versions of this Licence, approved by the European Commission,
|
|
||||||
have identical value. Parties can take advantage of the linguistic version of
|
|
||||||
their choice.
|
|
||||||
|
|
||||||
14. Jurisdiction
|
|
||||||
|
|
||||||
Without prejudice to specific agreement between parties,
|
|
||||||
|
|
||||||
- any litigation resulting from the interpretation of this License, arising
|
|
||||||
between the European Union institutions, bodies, offices or agencies, as a
|
|
||||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
|
||||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
|
||||||
the Functioning of the European Union,
|
|
||||||
|
|
||||||
- any litigation arising between other parties and resulting from the
|
|
||||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
|
||||||
of the competent court where the Licensor resides or conducts its primary
|
|
||||||
business.
|
|
||||||
|
|
||||||
15. Applicable Law
|
|
||||||
|
|
||||||
Without prejudice to specific agreement between parties,
|
|
||||||
|
|
||||||
- this Licence shall be governed by the law of the European Union Member State
|
|
||||||
where the Licensor has his seat, resides or has his registered office,
|
|
||||||
|
|
||||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
|
||||||
residence or registered office inside a European Union Member State.
|
|
||||||
|
|
||||||
Appendix
|
|
||||||
|
|
||||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
|
||||||
|
|
||||||
- GNU General Public License (GPL) v. 2, v. 3
|
|
||||||
- GNU Affero General Public License (AGPL) v. 3
|
|
||||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
|
||||||
- Eclipse Public License (EPL) v. 1.0
|
|
||||||
- CeCILL v. 2.0, v. 2.1
|
|
||||||
- Mozilla Public Licence (MPL) v. 2
|
|
||||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
|
||||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
|
||||||
works other than software
|
|
||||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
|
||||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
|
||||||
Reciprocity (LiLiQ-R+).
|
|
||||||
|
|
||||||
The European Commission may update this Appendix to later versions of the above
|
|
||||||
licences without producing a new version of the EUPL, as long as they provide
|
|
||||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
|
||||||
Code from exclusive appropriation.
|
|
||||||
|
|
||||||
All other changes or additions to this Appendix require the production of a new
|
|
||||||
EUPL version.
|
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||||
|
EUPL (c) the European Union 2007, 2016
|
||||||
|
|
||||||
|
This European Union Public Licence (the 'EUPL') applies to the Work (as
|
||||||
|
defined below) which is provided under the terms of this Licence. Any use
|
||||||
|
of the Work, other than as authorised under this Licence is prohibited (to
|
||||||
|
the extent such use is covered by a right of the copyright holder of the
|
||||||
|
Work).
|
||||||
|
|
||||||
|
The Work is provided under the terms of this Licence when the Licensor (as
|
||||||
|
defined below) has placed the following notice immediately following the
|
||||||
|
copyright notice for the Work:
|
||||||
|
|
||||||
|
Licensed under the EUPL
|
||||||
|
|
||||||
|
or has expressed by any other means his willingness to license under the
|
||||||
|
EUPL.
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
In this Licence, the following terms have the following meaning:
|
||||||
|
|
||||||
|
- 'The Licence': this Licence.
|
||||||
|
|
||||||
|
- 'The Original Work': the work or software distributed or communicated by
|
||||||
|
the Licensor under this Licence, available as Source Code and also as
|
||||||
|
Executable Code as the case may be.
|
||||||
|
|
||||||
|
- 'Derivative Works': the works or software that could be created by the
|
||||||
|
Licensee, based upon the Original Work or modifications thereof. This
|
||||||
|
Licence does not define the extent of modification or dependence on the
|
||||||
|
Original Work required in order to classify a work as a Derivative Work;
|
||||||
|
this extent is determined by copyright law applicable in the country
|
||||||
|
mentioned in Article 15.
|
||||||
|
|
||||||
|
- 'The Work': the Original Work or its Derivative Works.
|
||||||
|
|
||||||
|
- 'The Source Code': the human-readable form of the Work which is the most
|
||||||
|
convenient for people to study and modify.
|
||||||
|
|
||||||
|
- 'The Executable Code': any code which has generally been compiled and
|
||||||
|
which is meant to be interpreted by a computer as a program.
|
||||||
|
|
||||||
|
- 'The Licensor': the natural or legal person that distributes or
|
||||||
|
communicates the Work under the Licence.
|
||||||
|
|
||||||
|
- 'Contributor(s)': any natural or legal person who modifies the Work under
|
||||||
|
the Licence, or otherwise contributes to the creation of a Derivative
|
||||||
|
Work.
|
||||||
|
|
||||||
|
- 'The Licensee' or 'You': any natural or legal person who makes any usage
|
||||||
|
of the Work under the terms of the Licence.
|
||||||
|
|
||||||
|
- 'Distribution' or 'Communication': any act of selling, giving, lending,
|
||||||
|
renting, distributing, communicating, transmitting, or otherwise making
|
||||||
|
available, online or offline, copies of the Work or providing access to
|
||||||
|
its essential functionalities at the disposal of any other natural or
|
||||||
|
legal person.
|
||||||
|
|
||||||
|
2. Scope of the rights granted by the Licence
|
||||||
|
|
||||||
|
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||||
|
sublicensable licence to do the following, for the duration of copyright
|
||||||
|
vested in the Original Work:
|
||||||
|
|
||||||
|
- use the Work in any circumstance and for all usage,
|
||||||
|
- reproduce the Work,
|
||||||
|
- modify the Work, and make Derivative Works based upon the Work,
|
||||||
|
- communicate to the public, including the right to make available or
|
||||||
|
display the Work or copies thereof to the public and perform publicly,
|
||||||
|
as the case may be, the Work,
|
||||||
|
- distribute the Work or copies thereof,
|
||||||
|
- lend and rent the Work or copies thereof,
|
||||||
|
- sublicense rights in the Work or copies thereof.
|
||||||
|
|
||||||
|
Those rights can be exercised on any media, supports and formats, whether
|
||||||
|
now known or later invented, as far as the applicable law permits so.
|
||||||
|
|
||||||
|
In the countries where moral rights apply, the Licensor waives his right
|
||||||
|
to exercise his moral right to the extent allowed by law in order to make
|
||||||
|
effective the licence of the economic rights here above listed.
|
||||||
|
|
||||||
|
The Licensor grants to the Licensee royalty-free, non-exclusive usage
|
||||||
|
rights to any patents held by the Licensor, to the extent necessary to
|
||||||
|
make use of the rights granted on the Work under this Licence.
|
||||||
|
|
||||||
|
3. Communication of the Source Code
|
||||||
|
|
||||||
|
The Licensor may provide the Work either in its Source Code form, or as
|
||||||
|
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||||
|
provides in addition a machine-readable copy of the Source Code of the
|
||||||
|
Work along with each copy of the Work that the Licensor distributes or
|
||||||
|
indicates, in a notice following the copyright notice attached to the
|
||||||
|
Work, a repository where the Source Code is easily and freely accessible
|
||||||
|
for as long as the Licensor continues to distribute or communicate the
|
||||||
|
Work.
|
||||||
|
|
||||||
|
4. Limitations on copyright
|
||||||
|
|
||||||
|
Nothing in this Licence is intended to deprive the Licensee of the
|
||||||
|
benefits from any exception or limitation to the exclusive rights of the
|
||||||
|
rights owners in the Work, of the exhaustion of those rights or of other
|
||||||
|
applicable limitations thereto.
|
||||||
|
|
||||||
|
5. Obligations of the Licensee
|
||||||
|
|
||||||
|
The grant of the rights mentioned above is subject to some restrictions
|
||||||
|
and obligations imposed on the Licensee. Those obligations are the
|
||||||
|
following:
|
||||||
|
|
||||||
|
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||||
|
trademarks notices and all notices that refer to the Licence and to the
|
||||||
|
disclaimer of warranties. The Licensee must include a copy of such notices
|
||||||
|
and a copy of the Licence with every copy of the Work he/she distributes
|
||||||
|
or communicates. The Licensee must cause any Derivative Work to carry
|
||||||
|
prominent notices stating that the Work has been modified and the date of
|
||||||
|
modification.
|
||||||
|
|
||||||
|
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||||
|
Original Works or Derivative Works, this Distribution or Communication
|
||||||
|
will be done under the terms of this Licence or of a later version of
|
||||||
|
this Licence unless the Original Work is expressly distributed only under
|
||||||
|
this version of the Licence. The Licensee (becoming Licensor) cannot
|
||||||
|
offer or impose any additional terms or conditions on the Work or
|
||||||
|
Derivative Work that alter or restrict the terms of the Licence.
|
||||||
|
|
||||||
|
Compatibility clause: If the Licensee Distributes or Communicates
|
||||||
|
Derivative Works or copies thereof based upon both the Work and another
|
||||||
|
work licensed under a Compatible Licence, this Distribution or
|
||||||
|
Communication can be done under the terms of this Compatible Licence. For
|
||||||
|
the sake of this clause, 'Compatible Licence' refers to the licences
|
||||||
|
listed in the appendix attached to this Licence. Should the Licensee's
|
||||||
|
obligations under the Compatible Licence conflict with his/her obligations
|
||||||
|
under this Licence, the obligations of the Compatible Licence shall
|
||||||
|
prevail.
|
||||||
|
|
||||||
|
Provision of Source Code: When distributing or communicating copies of
|
||||||
|
the Work, the Licensee will provide a machine-readable copy of the Source
|
||||||
|
Code or indicate a repository where this Source will be easily and freely
|
||||||
|
available for as long as the Licensee continues to distribute or
|
||||||
|
communicate the Work.
|
||||||
|
|
||||||
|
Legal Protection: This Licence does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or names of the Licensor, except as
|
||||||
|
required for reasonable and customary use in describing the origin of the
|
||||||
|
Work and reproducing the content of the copyright notice.
|
||||||
|
|
||||||
|
6. Chain of Authorship
|
||||||
|
|
||||||
|
The original Licensor warrants that the copyright in the Original Work
|
||||||
|
granted hereunder is owned by him/her or licensed to him/her and that
|
||||||
|
he/she has the power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each Contributor warrants that the copyright in the modifications he/she
|
||||||
|
brings to the Work are owned by him/her or licensed to him/her and that
|
||||||
|
he/she has the power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each time You accept the Licence, the original Licensor and subsequent
|
||||||
|
Contributors grant You a licence to their contributions to the Work,
|
||||||
|
under the terms of this Licence.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty
|
||||||
|
|
||||||
|
The Work is a work in progress, which is continuously improved by
|
||||||
|
numerous Contributors. It is not a finished work and may therefore contain
|
||||||
|
defects or 'bugs' inherent to this type of development.
|
||||||
|
|
||||||
|
For the above reason, the Work is provided under the Licence on an 'as
|
||||||
|
is' basis and without warranties of any kind concerning the Work,
|
||||||
|
including without limitation merchantability, fitness for a particular
|
||||||
|
purpose, absence of defects or errors, accuracy, non-infringement of
|
||||||
|
intellectual property rights other than copyright as stated in Article 6
|
||||||
|
of this Licence.
|
||||||
|
|
||||||
|
This disclaimer of warranty is an essential part of the Licence and a
|
||||||
|
condition for the grant of any rights to the Work.
|
||||||
|
|
||||||
|
8. Disclaimer of Liability
|
||||||
|
|
||||||
|
Except in the cases of wilful misconduct or damages directly caused to
|
||||||
|
natural persons, the Licensor will in no event be liable for any direct
|
||||||
|
or indirect, material or moral, damages of any kind, arising out of the
|
||||||
|
Licence or of the use of the Work, including without limitation, damages
|
||||||
|
for loss of goodwill, work stoppage, computer failure or malfunction,
|
||||||
|
loss of data or any commercial damage, even if the Licensor has been
|
||||||
|
advised of the possibility of such damage. However, the Licensor will be
|
||||||
|
liable under statutory product liability laws as far such laws apply to
|
||||||
|
the Work.
|
||||||
|
|
||||||
|
9. Additional agreements
|
||||||
|
|
||||||
|
While distributing the Work, You may choose to conclude an additional
|
||||||
|
agreement, defining obligations or services consistent with this Licence.
|
||||||
|
However, if accepting obligations, You may act only on your own behalf
|
||||||
|
and on your sole responsibility, not on behalf of the original Licensor
|
||||||
|
or any other Contributor, and only if You agree to indemnify, defend, and
|
||||||
|
hold each Contributor harmless for any liability incurred by, or claims
|
||||||
|
asserted against such Contributor by the fact You have accepted any
|
||||||
|
warranty or additional liability.
|
||||||
|
|
||||||
|
10. Acceptance of the Licence
|
||||||
|
|
||||||
|
The provisions of this Licence can be accepted by clicking on an icon 'I
|
||||||
|
agree' placed under the bottom of a window displaying the text of this
|
||||||
|
Licence or by affirming consent in any other similar way, in accordance
|
||||||
|
with the rules of applicable law. Clicking on that icon indicates your
|
||||||
|
clear and irrevocable acceptance of this Licence and all of its terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||||
|
conditions by exercising any rights granted to You by Article 2 of this
|
||||||
|
Licence, such as the use of the Work, the creation by You of a Derivative
|
||||||
|
Work or the Distribution or Communication by You of the Work or copies
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
11. Information to the public
|
||||||
|
|
||||||
|
In case of any Distribution or Communication of the Work by means of
|
||||||
|
electronic communication by You (for example, by offering to download the
|
||||||
|
Work from a remote location) the distribution channel or media (for
|
||||||
|
example, a website) must at least provide to the public the information
|
||||||
|
requested by the applicable law regarding the Licensor, the Licence and
|
||||||
|
the way it may be accessible, concluded, stored and reproduced by the
|
||||||
|
Licensee.
|
||||||
|
|
||||||
|
12. Termination of the Licence
|
||||||
|
|
||||||
|
The Licence and the rights granted hereunder will terminate automatically
|
||||||
|
upon any breach by the Licensee of the terms of the Licence.
|
||||||
|
|
||||||
|
Such a termination will not terminate the licences of any person who has
|
||||||
|
received the Work from the Licensee under the Licence, provided such
|
||||||
|
persons remain in full compliance with the Licence.
|
||||||
|
|
||||||
|
13. Miscellaneous
|
||||||
|
|
||||||
|
Without prejudice of Article 9 above, the Licence represents the complete
|
||||||
|
agreement between the Parties as to the Work.
|
||||||
|
|
||||||
|
If any provision of the Licence is invalid or unenforceable under
|
||||||
|
applicable law, this will not affect the validity or enforceability of
|
||||||
|
the Licence as a whole. Such provision will be construed or reformed so
|
||||||
|
as necessary to make it valid and enforceable.
|
||||||
|
|
||||||
|
The European Commission may publish other linguistic versions or new
|
||||||
|
versions of this Licence or updated versions of the Appendix, so far this
|
||||||
|
is required and reasonable, without reducing the scope of the rights
|
||||||
|
granted by the Licence. New versions of the Licence will be published
|
||||||
|
with a unique version number.
|
||||||
|
|
||||||
|
All linguistic versions of this Licence, approved by the European
|
||||||
|
Commission, have identical value. Parties can take advantage of the
|
||||||
|
linguistic version of their choice.
|
||||||
|
|
||||||
|
14. Jurisdiction
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- any litigation resulting from the interpretation of this License,
|
||||||
|
arising between the European Union institutions, bodies, offices or
|
||||||
|
agencies, as a Licensor, and any Licensee, will be subject to the
|
||||||
|
jurisdiction of the Court of Justice of the European Union, as laid
|
||||||
|
down in article 272 of the Treaty on the Functioning of the European
|
||||||
|
Union,
|
||||||
|
|
||||||
|
- any litigation arising between other parties and resulting from the
|
||||||
|
interpretation of this License, will be subject to the exclusive
|
||||||
|
jurisdiction of the competent court where the Licensor resides or
|
||||||
|
conducts its primary business.
|
||||||
|
|
||||||
|
15. Applicable Law
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- this Licence shall be governed by the law of the European Union Member
|
||||||
|
State where the Licensor has his seat, resides or has his registered
|
||||||
|
office,
|
||||||
|
|
||||||
|
- this licence shall be governed by Belgian law if the Licensor has no
|
||||||
|
seat, residence or registered office inside a European Union Member
|
||||||
|
State.
|
||||||
|
|
||||||
|
Appendix
|
||||||
|
|
||||||
|
'Compatible Licences' according to Article 5 EUPL are:
|
||||||
|
|
||||||
|
- GNU General Public License (GPL) v. 2, v. 3
|
||||||
|
- GNU Affero General Public License (AGPL) v. 3
|
||||||
|
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||||
|
- Eclipse Public License (EPL) v. 1.0
|
||||||
|
- CeCILL v. 2.0, v. 2.1
|
||||||
|
- Mozilla Public Licence (MPL) v. 2
|
||||||
|
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||||
|
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0)
|
||||||
|
for works other than software
|
||||||
|
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||||
|
- Quebec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||||
|
Reciprocity (LiLiQ-R+)
|
||||||
|
|
||||||
|
The European Commission may update this Appendix to later versions of the
|
||||||
|
above licences without producing a new version of the EUPL, as long as
|
||||||
|
they provide the rights granted in Article 2 of this Licence and protect
|
||||||
|
the covered Source Code from exclusive appropriation.
|
||||||
|
|
||||||
|
All other changes or additions to this Appendix require the production of
|
||||||
|
a new EUPL version.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Notice
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) by
|
||||||
|
**Infiziert90 (Infi)** and **Anna Clemens**, both of whom kept that plugin
|
||||||
|
running and maintained for years before I ever opened the source. Without
|
||||||
|
their work this fork would not exist, full stop. I owe them the architecture,
|
||||||
|
the message store, the channel filtering, the sidebar tab system, the
|
||||||
|
hooks into FFXIV's chat, the localisation infrastructure, and countless
|
||||||
|
small decisions that I only noticed because they had already been made
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
If you find HellionChat useful, please remember that the foundation came
|
||||||
|
from Chat 2. The code Anna and Infi wrote is doing most of the heavy
|
||||||
|
lifting in this fork too.
|
||||||
|
|
||||||
|
## A direct word to Infi and Anna
|
||||||
|
|
||||||
|
Hi. I am Florian (Flo, also Jon Kazama in-game on Phoenix). I forked Chat 2
|
||||||
|
because I wanted a privacy-by-default version for my own use case and a
|
||||||
|
small group of friends I play with, not because I thought I could do
|
||||||
|
anything better than what you built. The opposite is true. ChatTwo's
|
||||||
|
default of full history and cross-character logging is the right call for
|
||||||
|
most users. I just have a different threat model and a different
|
||||||
|
data-handling philosophy that fits a smaller, locally-stored, retention-
|
||||||
|
limited approach.
|
||||||
|
|
||||||
|
What HellionChat adds is mostly Hellion-specific surface area: a privacy
|
||||||
|
filter, per-channel retention windows, an export pipeline, an Auto-Tell-
|
||||||
|
Tabs feature for FFXIV club greeters, the Hellion theme and font, German
|
||||||
|
localisation, and a settings UX rebuild. None of it touches the bones of
|
||||||
|
what you built. Where I had to modify your code I tried to keep the
|
||||||
|
edits minimal, isolated to clearly-marked Hellion files, and reversible.
|
||||||
|
|
||||||
|
Concrete example: when API 15 hit, I cherry-picked your fix for the
|
||||||
|
BetterTTV emote regression with `git cherry-pick -x` so authorship and
|
||||||
|
co-author trail stay intact. That is the standard I want to keep using as
|
||||||
|
long as both projects are alive. You should never have to look at this
|
||||||
|
fork and wonder if I quietly ate your work.
|
||||||
|
|
||||||
|
If anything in this fork ever steps on something you would not be okay
|
||||||
|
with, please reach out and I will fix it. Genuinely. The list of contacts
|
||||||
|
is below.
|
||||||
|
|
||||||
|
## Maintainer contact
|
||||||
|
|
||||||
|
If something in HellionChat causes problems, especially if it relates back
|
||||||
|
to Chat 2 or to anything Infi or Anna would want flagged:
|
||||||
|
|
||||||
|
- **GitHub Issues:** [JonKazama-Hellion/HellionChat/issues](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||||
|
- **Discord:** `@j.j_kazama`
|
||||||
|
- **Email (business):** kontakt@hellion-media.de
|
||||||
|
|
||||||
|
I respond on weekdays during European business hours. For anything
|
||||||
|
urgent (security, attribution, takedown), email is the fastest path.
|
||||||
|
|
||||||
|
## Why this fork is not upstreamed
|
||||||
|
|
||||||
|
The privacy-by-default position fits a small audience. ChatTwo's
|
||||||
|
full-history-by-default position fits a much larger one, including the
|
||||||
|
roleplaying community where chat archive is part of the play experience.
|
||||||
|
Trying to upstream HellionChat's defaults would have meant arguing that
|
||||||
|
Chat 2's defaults are wrong, and they are not. They are right for the
|
||||||
|
user base ChatTwo serves. So I keep the fork separate, attribute clearly,
|
||||||
|
and pull selected upstream patches when they apply.
|
||||||
|
|
||||||
|
## Why HellionChat left the GitHub fork network
|
||||||
|
|
||||||
|
The Dalamud plugin ecosystem treats the GitHub-Fork relation as a signal
|
||||||
|
that a fork is either a development branch or a dead mirror. HellionChat
|
||||||
|
is neither. It is an independently-maintained EUPL-1.2 fork with its own
|
||||||
|
release cadence, its own custom repo, its own user base. Detaching the
|
||||||
|
fork-network relation just makes the situation honest. The git history,
|
||||||
|
the cherry-pick trail, and the attribution stay exactly the same. The
|
||||||
|
only thing that changes is the GitHub UI no longer says "forked from".
|
||||||
|
|
||||||
|
## Trademarks and naming
|
||||||
|
|
||||||
|
"Chat 2" and "ChatTwo" are the names Infi and Anna chose for the upstream
|
||||||
|
plugin. HellionChat does not use either of those names in user-facing
|
||||||
|
copy except where required to describe origin (settings tab, manifest,
|
||||||
|
this file, the README). The Hellion brand is mine.
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
This file is the canonical place for "is this attribution correct, is the
|
||||||
|
maintainer reachable, is the relationship to Chat 2 documented". If
|
||||||
|
anything in here is wrong, please open an issue or contact me directly.
|
||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
# Privacy notice
|
||||||
|
|
||||||
|
HellionChat is a Dalamud plugin for FINAL FANTASY XIV that focuses on
|
||||||
|
giving the user explicit control over what their chat client stores
|
||||||
|
locally. This document describes what the plugin does with your data,
|
||||||
|
what it does not do, and how you exercise the rights the GDPR gives
|
||||||
|
you over data you generate yourself.
|
||||||
|
|
||||||
|
This document is informational. The maintainer of HellionChat is
|
||||||
|
**not** a controller or processor of your data in the GDPR sense,
|
||||||
|
because no data ever leaves your machine on the maintainer's
|
||||||
|
infrastructure. Independently of that, the plugin is built so that
|
||||||
|
you can act on your own data the way the GDPR expects.
|
||||||
|
|
||||||
|
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- All chat data the plugin stores stays on your machine, in your
|
||||||
|
Dalamud `pluginConfigs/HellionChat/` directory.
|
||||||
|
- The plugin does not phone home. There is no telemetry, no analytics,
|
||||||
|
no crash reporter, no usage counter, no remote update check beyond
|
||||||
|
what Dalamud itself does.
|
||||||
|
- Two outbound network calls exist by design: the BetterTTV emote
|
||||||
|
service (for chat emotes) and the Square Enix Lodestone font CDN
|
||||||
|
(for the in-game symbol font). Both are documented in detail below
|
||||||
|
and both can be reasoned about per request.
|
||||||
|
- You can export every message the plugin has stored, in Markdown,
|
||||||
|
JSON or CSV, and you can wipe stored history per channel, per date
|
||||||
|
range, or globally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the plugin stores locally
|
||||||
|
|
||||||
|
HellionChat keeps three kinds of state on your machine, all under
|
||||||
|
`%appdata%\XIVLauncher\pluginConfigs\HellionChat\` on Windows
|
||||||
|
(`~/.xlcore/pluginConfigs/HellionChat/` on Linux/macOS via XIVLauncher
|
||||||
|
Core):
|
||||||
|
|
||||||
|
1. **Configuration** (`HellionChat.json`)
|
||||||
|
Plugin settings, channel whitelist, retention values, layout state,
|
||||||
|
theme colours. Contains no chat content.
|
||||||
|
|
||||||
|
2. **Message database** (SQLite file in the same directory)
|
||||||
|
Chat messages from the channels on your whitelist, stored as
|
||||||
|
MessagePack-encoded blobs. Default whitelist out of the box covers
|
||||||
|
only your own conversations: tells, party, free company, linkshells,
|
||||||
|
cross-world linkshells, alliance, ExtraChat. Public chat, NPC
|
||||||
|
dialogue, system messages and battle logs are dropped on the
|
||||||
|
storage layer and never written to disk.
|
||||||
|
|
||||||
|
3. **Cached emote images** (`EmoteCacheV1/` directory)
|
||||||
|
Image files downloaded from BetterTTV when an emote appears in a
|
||||||
|
message you receive. See "Outbound network calls" below.
|
||||||
|
|
||||||
|
There is no shared state with the upstream Chat 2 plugin.
|
||||||
|
`pluginConfigs/HellionChat/` is independent from `pluginConfigs/ChatTwo/`.
|
||||||
|
|
||||||
|
### Retention defaults
|
||||||
|
|
||||||
|
- Tells: 365 days
|
||||||
|
- Your-conversation channels (party, FC, linkshells, cross-world LS,
|
||||||
|
alliance, ExtraChat): 90 days
|
||||||
|
- Global default for anything else: 30 days
|
||||||
|
|
||||||
|
**Retention is off by default.** The plugin does not delete anything
|
||||||
|
on its own until you explicitly turn the retention sweep on in the
|
||||||
|
settings. Until then, stored messages stay until you clear them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the plugin does not store
|
||||||
|
|
||||||
|
- Public chat (`/say`, `/yell`, `/shout`), NPC dialogue, system
|
||||||
|
messages and battle logs. These are filtered before they reach the
|
||||||
|
storage layer.
|
||||||
|
- Anything from channels you remove from the whitelist. The privacy
|
||||||
|
filter runs on the way in, not on the way out.
|
||||||
|
- Login credentials, character IDs, account IDs. The plugin uses
|
||||||
|
whatever Dalamud already exposes about the local character to
|
||||||
|
attribute messages; nothing of that is sent anywhere or persisted
|
||||||
|
beyond the message itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outbound network calls
|
||||||
|
|
||||||
|
HellionChat makes two kinds of automatic outbound network requests.
|
||||||
|
Both are inherited from upstream Chat 2 and both are documented here
|
||||||
|
because "DSGVO-by-design" means you should know what your client does
|
||||||
|
on your behalf.
|
||||||
|
|
||||||
|
### 1. BetterTTV emote service (`api.betterttv.net`, `cdn.betterttv.net`)
|
||||||
|
|
||||||
|
- **What it does:** When a chat message arrives that references a
|
||||||
|
BetterTTV emote, the plugin asks the BetterTTV API for the emote
|
||||||
|
metadata and downloads the image from the BetterTTV CDN to display
|
||||||
|
it inline.
|
||||||
|
- **What is sent:** A standard HTTPS GET request. Your IP address
|
||||||
|
reaches BetterTTV (unavoidable for any HTTPS request); the request
|
||||||
|
itself contains no identifying user data, no character name, no
|
||||||
|
message text. Only the emote ID being looked up is in the URL path.
|
||||||
|
- **When it triggers:** Only when an incoming message contains an
|
||||||
|
emote token that is on the BetterTTV emote list.
|
||||||
|
- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once
|
||||||
|
per machine and reused.
|
||||||
|
- **How to opt out:** Turn off the **Show emotes** option in
|
||||||
|
Settings → Chat. With it disabled, the emote cache does not load
|
||||||
|
and no requests to BetterTTV are made for the rest of the session.
|
||||||
|
- **BetterTTV's privacy policy:** <https://betterttv.com/privacy>
|
||||||
|
|
||||||
|
Source: `ChatTwo/EmoteCache.cs`.
|
||||||
|
|
||||||
|
### 2. Square Enix Lodestone font (`img.finalfantasyxiv.com`)
|
||||||
|
|
||||||
|
- **What it does:** Downloads the `FFXIV_Lodestone_SSF.ttf` font file
|
||||||
|
from the official Square Enix Lodestone CDN once during font setup,
|
||||||
|
so the plugin can render in-game special symbols (job icons, item
|
||||||
|
glyphs, etc.) inside ImGui.
|
||||||
|
- **What is sent:** A single HTTPS GET request to the public Square
|
||||||
|
Enix font URL. Your IP address reaches Square Enix (unavoidable);
|
||||||
|
no character data, no plugin identifier, no message content.
|
||||||
|
- **When it triggers:** Once per font initialisation, not per session
|
||||||
|
if the file is already cached locally.
|
||||||
|
- **Cached:** Yes, by Dalamud's font subsystem.
|
||||||
|
- **How to opt out:** This call is part of the font pipeline inherited
|
||||||
|
from upstream Chat 2 and not toggleable from the settings UI today.
|
||||||
|
If a user-facing opt-out for this would be useful for you, please
|
||||||
|
open a feature-request issue.
|
||||||
|
|
||||||
|
Source: `ChatTwo/FontManager.cs`.
|
||||||
|
|
||||||
|
### Links you click yourself (no automatic traffic)
|
||||||
|
|
||||||
|
The settings panel contains a few buttons that open external pages in
|
||||||
|
your browser when you click them: the upstream Chat 2 GitHub repo,
|
||||||
|
the upstream maintainers' Ko-fi pages, the HellionChat issue tracker
|
||||||
|
and `hellion-media.de`. Nothing happens until you click. They are
|
||||||
|
documented here for completeness, not because they generate background
|
||||||
|
traffic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the plugin does not do
|
||||||
|
|
||||||
|
- **No telemetry.** Source verified: no calls to AppInsights, Sentry,
|
||||||
|
PostHog, Plausible, Google Analytics, Microsoft Clarity or any
|
||||||
|
comparable service exist in the codebase, nor in the direct
|
||||||
|
dependencies the plugin pulls in. See `THIRD_PARTY_NOTICES.md`.
|
||||||
|
- **No crash reporting.** Crashes go to Dalamud's local `xllog`,
|
||||||
|
not to a remote endpoint controlled by HellionChat.
|
||||||
|
- **No usage counters.** The plugin does not count installs, sessions,
|
||||||
|
feature usage, channel activity or anything else for the maintainer.
|
||||||
|
- **No phone-home update check.** Updates are delivered through
|
||||||
|
Dalamud's plugin installer, which polls the custom-repo
|
||||||
|
`repo.json` on GitHub. That is GitHub's traffic and falls under
|
||||||
|
GitHub's privacy policy; the plugin code does no separate update
|
||||||
|
check.
|
||||||
|
- **No background sync.** Messages stay on your machine. There is no
|
||||||
|
cloud backup, no sharing feature, no remote viewer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your data, your rights
|
||||||
|
|
||||||
|
The GDPR gives you specific rights over data about you. Because
|
||||||
|
HellionChat stores everything locally, those rights translate
|
||||||
|
directly into plugin features:
|
||||||
|
|
||||||
|
### Right to access (Art. 15)
|
||||||
|
|
||||||
|
Use the export feature in the plugin settings. You can export to
|
||||||
|
**Markdown**, **JSON** or **CSV**, filtered by channel, date range
|
||||||
|
or sender substring. The export goes through a Dalamud file dialog
|
||||||
|
and writes wherever you point it, on your machine.
|
||||||
|
|
||||||
|
### Right to erasure (Art. 17)
|
||||||
|
|
||||||
|
Two options:
|
||||||
|
|
||||||
|
1. **Targeted deletion** — the "retroactive cleanup" feature lets you
|
||||||
|
apply your current whitelist to the existing database. It shows a
|
||||||
|
preview of what will be removed before you confirm with
|
||||||
|
Ctrl+Shift, runs in the background, and calls `VACUUM` afterwards
|
||||||
|
to actually shrink the file.
|
||||||
|
2. **Full deletion** — close the game and delete the
|
||||||
|
`pluginConfigs/HellionChat/` directory. Next plugin start will
|
||||||
|
produce a fresh, empty configuration.
|
||||||
|
|
||||||
|
### Right to portability (Art. 20)
|
||||||
|
|
||||||
|
The JSON and CSV exports are open formats. The Markdown export is
|
||||||
|
human-readable and machine-parseable. Nothing is locked into a
|
||||||
|
proprietary container.
|
||||||
|
|
||||||
|
### Right to object / restrict processing (Art. 21, 18)
|
||||||
|
|
||||||
|
Adjust the channel whitelist or set retention to a low value. Both
|
||||||
|
take effect immediately on new messages; existing data needs the
|
||||||
|
retroactive cleanup to apply retroactively, by design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Third parties involved
|
||||||
|
|
||||||
|
| Party | Why they appear | What reaches them | Their privacy policy |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | <https://betterttv.com/privacy> |
|
||||||
|
| Square Enix | Lodestone font download (once) | HTTPS request for the font file; your IP | <https://www.square-enix.com/privacy> |
|
||||||
|
| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement> |
|
||||||
|
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
||||||
|
|
||||||
|
Square Enix and GitHub are unavoidable for anyone playing FFXIV
|
||||||
|
through Dalamud at all. BetterTTV is the only third party HellionChat
|
||||||
|
introduces on top of the baseline that is not also part of using FFXIV
|
||||||
|
or Dalamud, and BetterTTV is opt-out via settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies that touch the network
|
||||||
|
|
||||||
|
For a full dependency inventory see `THIRD_PARTY_NOTICES.md`. Of the
|
||||||
|
direct dependencies the plugin pulls in:
|
||||||
|
|
||||||
|
- `MessagePack` — local serialisation, no network.
|
||||||
|
- `Microsoft.Data.Sqlite` — local SQLite access, no network.
|
||||||
|
- `morelinq` — LINQ helpers, no network.
|
||||||
|
- `Pidgin` — parser combinators, no network.
|
||||||
|
- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV
|
||||||
|
emote pipeline), no network on its own.
|
||||||
|
|
||||||
|
The two network calls listed under "Outbound network calls" are
|
||||||
|
written directly in HellionChat's own source, not delegated to a
|
||||||
|
dependency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes to this notice
|
||||||
|
|
||||||
|
If a future release changes what HellionChat stores, sends or caches,
|
||||||
|
this document will be updated and the change called out in the
|
||||||
|
changelog block of that release. The "Last reviewed" date at the top
|
||||||
|
tracks the version this document is accurate for.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
For privacy-related questions specific to HellionChat:
|
||||||
|
|
||||||
|
- Email: `kontakt@hellion-media.de`
|
||||||
|
- Discord DM: `@j.j_kazama`
|
||||||
|
|
||||||
|
Security-relevant findings (e.g. the plugin storing or sending
|
||||||
|
something this document says it does not) go through the private
|
||||||
|
advisory in `SECURITY.md`, not a public issue.
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
# Hellion Chat
|
# Hellion Chat
|
||||||
|
|
||||||
**Version 0.3.1** — DSGVO-bewusste Erweiterung von [Chat 2](https://github.com/Infiziert90/ChatTwo) für FINAL FANTASY XIV / Dalamud.
|
[](https://github.com/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||||
|
[](https://github.com/JonKazama-Hellion/HellionChat/security/code-scanning)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/JonKazama-Hellion/HellionChat/releases/latest)
|
||||||
|
[](https://github.com/goatcorp/Dalamud)
|
||||||
|
[](https://dotnet.microsoft.com/)
|
||||||
|
[](https://www.finalfantasyxiv.com/)
|
||||||
|
|
||||||
|
**Version 0.6.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.
|
||||||
|
|
||||||
Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo während der Bootstrap-Phase.
|
Eigenständiges Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Infiziert90 (Infi)** und **Anna Clemens** auf, die das Plugin über Jahre gepflegt haben bevor ich den Source-Code überhaupt gesehen habe. Die ganze Kern-Architektur, der Message-Store, die Channel-Logik, das Hook-System und vieles mehr stammt von ihnen. Wenn dir Hellion Chat hilft, dann läuft die Anerkennung dafür zu großen Teilen an Infi und Anna. Eine ausführliche Danksagung liegt in [NOTICE.md](NOTICE.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,7 +29,7 @@ Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo währen
|
|||||||
| Build | Dalamud.NET.Sdk 15.0.0, DalamudPackager 15.0.0 |
|
| Build | Dalamud.NET.Sdk 15.0.0, DalamudPackager 15.0.0 |
|
||||||
| UI | Dear ImGui (Dalamud-Bindings) |
|
| UI | Dear ImGui (Dalamud-Bindings) |
|
||||||
| Datenbank | SQLite (Microsoft.Data.Sqlite, MessagePack-Storage) |
|
| Datenbank | SQLite (Microsoft.Data.Sqlite, MessagePack-Storage) |
|
||||||
| Lokalisierung | ResX (HellionStrings.resx, .de.resx) + Crowdin-Sync |
|
| Lokalisierung | ResX (HellionStrings.resx, .de.resx; PR-basiert) |
|
||||||
| Schriftart | Exo 2 (SIL Open Font License 1.1, gebündelt) |
|
| Schriftart | Exo 2 (SIL Open Font License 1.1, gebündelt) |
|
||||||
| Toolchain | dotnet 10 SDK, VS Code mit C# Dev Kit |
|
| Toolchain | dotnet 10 SDK, VS Code mit C# Dev Kit |
|
||||||
| Deployment | GitHub Releases + Custom-Repo (`repo.json`) |
|
| Deployment | GitHub Releases + Custom-Repo (`repo.json`) |
|
||||||
@@ -32,6 +44,7 @@ Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo währen
|
|||||||
- **Aufbewahrungsdauer pro Kanal** mit täglicher Background-Bereinigung. Tells 365 Tage, eigene Konversations-Kanäle 90 Tage, globaler Default 30 Tage. Standard ist AUS, das Plugin löscht ohne ausdrückliche Zustimmung nichts.
|
- **Aufbewahrungsdauer pro Kanal** mit täglicher Background-Bereinigung. Tells 365 Tage, eigene Konversations-Kanäle 90 Tage, globaler Default 30 Tage. Standard ist AUS, das Plugin löscht ohne ausdrückliche Zustimmung nichts.
|
||||||
- **Retroaktive Säuberung** mit Vorschau und Strg+Umschalt-Bestätigung. Wendet die aktuelle Whitelist auf eine bestehende Datenbank an, läuft im Hintergrund, ruft danach VACUUM auf.
|
- **Retroaktive Säuberung** mit Vorschau und Strg+Umschalt-Bestätigung. Wendet die aktuelle Whitelist auf eine bestehende Datenbank an, läuft im Hintergrund, ruft danach VACUUM auf.
|
||||||
- **Export** nach Markdown, JSON oder CSV via Dalamud-Datei-Dialog (DSGVO Art. 15 Auskunftsrecht). Filter nach Kanal, Datums-Bereich oder Sender-Substring.
|
- **Export** nach Markdown, JSON oder CSV via Dalamud-Datei-Dialog (DSGVO Art. 15 Auskunftsrecht). Filter nach Kanal, Datums-Bereich oder Sender-Substring.
|
||||||
|
- **Vollständige Datenschutz-Übersicht** in [`PRIVACY.md`](PRIVACY.md): was gespeichert wird, welche zwei Outbound-Calls existieren (BetterTTV opt-out, Square-Enix-Lodestone-Font), explizite Telemetry-None-Zusage und das Mapping der DSGVO-Rechte (Art. 15/17/18/20/21) auf konkrete Plugin-Funktionen.
|
||||||
|
|
||||||
### Onboarding
|
### Onboarding
|
||||||
|
|
||||||
@@ -44,10 +57,17 @@ Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo währen
|
|||||||
|
|
||||||
- **Bilinguale UI** (Englisch + Deutsch) mit Live-Sprachwechsel. Hellion-spezifische Strings in `HellionStrings.<lang>.resx`.
|
- **Bilinguale UI** (Englisch + Deutsch) mit Live-Sprachwechsel. Hellion-spezifische Strings in `HellionStrings.<lang>.resx`.
|
||||||
- **Hellion-HUD-Theme** mit Cyan-Teal-Akzenten, Slate-Violet-Tabs, Bernstein-Highlights für aktive Zustände.
|
- **Hellion-HUD-Theme** mit Cyan-Teal-Akzenten, Slate-Violet-Tabs, Bernstein-Highlights für aktive Zustände.
|
||||||
|
- **Chat-Farben-Presets** (v0.6.0) mit sieben Built-in-Bundles in Settings → Aussehen → Chat-Farben: ChatTwo Default, High-Contrast, Pastell, Dark-Mode-Tuned, Hellion (Brand), plus Bonus-Stimmungen Night Blue und Indigo Violet. One-Click-Apply, Battle-Channels bleiben unangetastet.
|
||||||
- **Fenster-Deckkraft-Slider** für Kampf-freundliche Transparenz.
|
- **Fenster-Deckkraft-Slider** für Kampf-freundliche Transparenz.
|
||||||
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
||||||
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
||||||
|
|
||||||
|
### Pop-Out Convenience (v0.6.0)
|
||||||
|
|
||||||
|
- **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort.
|
||||||
|
- **Pro-Pop-Out unabhängiger Text-Buffer und History-Cursor.** Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster (FFXIV-Channel-API), aber halb-getippte Eingaben kollidieren nicht zwischen Hauptfenster und Pop-Outs.
|
||||||
|
- **Geteilte Eingabe-Historie** zwischen allen Fenstern via Singleton-Service — Up/Down-Pfeile navigieren überall durch dieselbe Liste der letzten 30 Eingaben.
|
||||||
|
|
||||||
### Stability
|
### Stability
|
||||||
|
|
||||||
- BetterTTV-Cache-Crash-Fix (Null-Key-Handling).
|
- BetterTTV-Cache-Crash-Fix (Null-Key-Handling).
|
||||||
@@ -90,7 +110,7 @@ ChatTwo/
|
|||||||
|
|
||||||
- **Code-Namespace bleibt `ChatTwo.*`** — Cherry-Picks von Upstream-Bugfixes bleiben damit konfliktarm.
|
- **Code-Namespace bleibt `ChatTwo.*`** — Cherry-Picks von Upstream-Bugfixes bleiben damit konfliktarm.
|
||||||
- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigene Datei-Manifest, kein Shared State mit Chat 2.
|
- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigene Datei-Manifest, kein Shared State mit Chat 2.
|
||||||
- **Hellion-eigene Strings nur in `HellionStrings.*.resx`** — die Upstream-`Language.*.resx` bleiben unverändert für sauberen Crowdin-Sync.
|
- **Hellion-eigene Strings nur in `HellionStrings.*.resx`** — die Upstream-`Language.*.resx` bleiben unverändert, damit Cherry-Picks aus Chat 2 (inklusive deren Übersetzungs-Updates aus dem Upstream-Crowdin) konfliktarm bleiben.
|
||||||
- **Kein Direkt-Eingriff in `Plugin.Interface.UiBuilder.FontAtlas`** außerhalb von `FontManager` — Font-Fallback und Hellion-Font laufen zentral.
|
- **Kein Direkt-Eingriff in `Plugin.Interface.UiBuilder.FontAtlas`** außerhalb von `FontManager` — Font-Fallback und Hellion-Font laufen zentral.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -169,7 +189,7 @@ Spiel starten, Hellion Chat aktivieren, Verlauf ist zurück.
|
|||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
Updates erscheinen automatisch in der Plugin-Liste, sobald ein neuer `v0.1.x`-Tag mit GitHub-Release publiziert ist. Keine Neu-Installation nötig.
|
Updates erscheinen automatisch in der Plugin-Liste, sobald ein neuer `v0.X.Y`-Tag mit GitHub-Release publiziert ist. Keine Neu-Installation nötig.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -213,7 +233,7 @@ git log --oneline HEAD..upstream/main # Welche Commits gibt es?
|
|||||||
git cherry-pick -x <commit> # Selektiv übernehmen
|
git cherry-pick -x <commit> # Selektiv übernehmen
|
||||||
```
|
```
|
||||||
|
|
||||||
Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig vor weil Crowdin sie regelmäßig anfasst. Pragmatisch mit `git checkout --theirs` auflösen, da wir sie selbst nicht editieren.
|
Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig vor, weil Upstream-Übersetzungen (über das Chat-2-Crowdin-Projekt, nicht unseres) regelmäßig nachkommen. Pragmatisch mit `git checkout --theirs` auflösen, da wir sie selbst nicht editieren.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -231,7 +251,7 @@ Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig
|
|||||||
|
|
||||||
## Projektstatus
|
## Projektstatus
|
||||||
|
|
||||||
**Version 0.3.1** | Stand: Mai 2026
|
**Version 0.6.1** | Stand: 2026-05-03
|
||||||
|
|
||||||
Alle Bootstrap-Phasen abgeschlossen:
|
Alle Bootstrap-Phasen abgeschlossen:
|
||||||
|
|
||||||
@@ -263,9 +283,17 @@ Phase 3 (offen, kein festes Datum):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Community & Support
|
||||||
|
|
||||||
|
- **Hellion Forge Discord** (Modding- und Plugin-Community von Hellion Online Media): https://discord.gg/X9V7Kcv5gR
|
||||||
|
- Bug-Reports und Feature-Requests: [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||||
|
- Weitere Kontaktwege (Security, Privacy, Quick-Questions): siehe [SUPPORT.md](SUPPORT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
EUPL-1.2 (gleiche Lizenz wie Upstream Chat 2). Siehe `LICENCE`.
|
EUPL-1.2 (gleiche Lizenz wie Upstream Chat 2). Volltext in [LICENSE](LICENSE), Copyright-Notes mit Dual-Holder-Block in [COPYRIGHT](COPYRIGHT), persönliche Danksagung an die Upstream-Autoren in [NOTICE.md](NOTICE.md).
|
||||||
|
|
||||||
© 2023–2026 die Chat-2-Autoren (Infi, Anna und die Upstream-Contributors) für die Engine, IPC und Storage-Schicht.
|
© 2023–2026 die Chat-2-Autoren (Infi, Anna und die Upstream-Contributors) für die Engine, IPC und Storage-Schicht.
|
||||||
© 2026 Hellion Online Media für die Hellion-Chat-Erweiterungen.
|
© 2026 Hellion Online Media für die Hellion-Chat-Erweiterungen.
|
||||||
@@ -286,4 +314,20 @@ Siehe [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) für die Pair-Level-Disclosure.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Projekt-Dokumente
|
||||||
|
|
||||||
|
| Dokument | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| [`PRIVACY.md`](PRIVACY.md) | Datenschutz-Übersicht: lokale Speicherung, Outbound-Calls, Telemetry-Status, DSGVO-Rechte und ihre Plugin-Entsprechungen. |
|
||||||
|
| [`SECURITY.md`](SECURITY.md) | Vulnerability-Reporting via Private Advisory, Scope und Disclosure-Fenster. |
|
||||||
|
| [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. |
|
||||||
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Was ich akzeptiere bzw. ablehne, Workflow, Build-Anleitung, EUPL-1.2-Bestätigung. |
|
||||||
|
| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Verhaltens-Erwartungen und Reporting-Pfad. |
|
||||||
|
| [`SUPPORT.md`](SUPPORT.md) | Wegweiser für Bugs, Security, Privacy, Quick-Questions. |
|
||||||
|
| [`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
||||||
|
| [`NOTICE.md`](NOTICE.md) | Attribution an Upstream-Maintainer und Komponenten-Credits. |
|
||||||
|
| [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Hellion Online Media** | Bad Harzburg | [hellion-media.de](https://hellion-media.de)
|
**Hellion Online Media** | Bad Harzburg | [hellion-media.de](https://hellion-media.de)
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
# Security policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
If you find a security issue in HellionChat, please do not open a public
|
||||||
|
GitHub issue. Use one of the private channels below instead so we can
|
||||||
|
investigate and ship a fix before the details go out.
|
||||||
|
|
||||||
|
**Preferred:**
|
||||||
|
[Privately report a vulnerability](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||||
|
through GitHub's Security Advisories. This routes the report directly to
|
||||||
|
me and keeps the conversation off the public timeline.
|
||||||
|
|
||||||
|
**Alternative:**
|
||||||
|
- Email: `kontakt@hellion-media.de`
|
||||||
|
- Discord: `@j.j_kazama`
|
||||||
|
|
||||||
|
I respond on weekdays during European business hours. For urgent
|
||||||
|
disclosures (active exploitation, user-data exposure) email is the
|
||||||
|
fastest path.
|
||||||
|
|
||||||
|
## What I treat as in scope
|
||||||
|
|
||||||
|
- Code paths in HellionChat that touch user-controlled input (chat
|
||||||
|
messages, plugin config, file paths the user can influence)
|
||||||
|
- The privacy filter in MessageStore.cs and the export pipeline
|
||||||
|
- The Configuration migration logic
|
||||||
|
- The EmoteCache HTTP client and path handling
|
||||||
|
- The Auto-Tell-Tabs spawn logic and history preload
|
||||||
|
|
||||||
|
## What is not in scope
|
||||||
|
|
||||||
|
- Issues in upstream Chat 2 that we have not modified — please report
|
||||||
|
those at <https://github.com/Infiziert90/ChatTwo/issues>
|
||||||
|
- Issues in Dalamud itself — those go to <https://github.com/goatcorp/Dalamud>
|
||||||
|
- Issues in the FFXIV game client
|
||||||
|
- Anything that needs the user to install a malicious plugin first
|
||||||
|
|
||||||
|
## Acknowledgement
|
||||||
|
|
||||||
|
I list everyone who reports a real issue in the changelog of the release
|
||||||
|
that fixes it, unless they prefer to stay anonymous. No bug bounty,
|
||||||
|
nothing financial; this is a hobby plugin.
|
||||||
|
|
||||||
|
## Disclosure window
|
||||||
|
|
||||||
|
I aim to ship a fix within 14 days for high-severity issues and within
|
||||||
|
30 days for everything else. If a fix needs more time I will say so in
|
||||||
|
the private thread.
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# Support
|
||||||
|
|
||||||
|
HellionChat is a small hobby project maintained by one person. There
|
||||||
|
are a few different paths depending on what you need; please pick the
|
||||||
|
one that matches.
|
||||||
|
|
||||||
|
## Bugs and feature requests
|
||||||
|
|
||||||
|
GitHub issues, using the templates:
|
||||||
|
|
||||||
|
- [Bug report](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml)
|
||||||
|
- [Feature request](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml)
|
||||||
|
|
||||||
|
Please search [existing issues](https://github.com/JonKazama-Hellion/HellionChat/issues?q=is%3Aissue)
|
||||||
|
first. Duplicates get closed and pointed at the original.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Do **not** open a public issue for security-relevant findings. Use
|
||||||
|
the private advisory route described in [SECURITY.md](SECURITY.md):
|
||||||
|
|
||||||
|
- [Private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||||
|
- Email `kontakt@hellion-media.de`
|
||||||
|
|
||||||
|
## Privacy questions
|
||||||
|
|
||||||
|
Specific questions about what HellionChat does or does not store and
|
||||||
|
send are covered in [PRIVACY.md](PRIVACY.md). For follow-ups beyond
|
||||||
|
that document:
|
||||||
|
|
||||||
|
- Email `kontakt@hellion-media.de`
|
||||||
|
|
||||||
|
## Quick questions and casual feedback
|
||||||
|
|
||||||
|
- **Hellion Forge Discord** — community for HellionChat and other
|
||||||
|
Hellion Online Media plugins/tools: https://discord.gg/X9V7Kcv5gR
|
||||||
|
- Discord DM `@j.j_kazama`
|
||||||
|
|
||||||
|
Bug reports still go through the issue tracker so they can be tracked,
|
||||||
|
but a quick "is this a bug or am I holding it wrong" message is fine.
|
||||||
|
|
||||||
|
## Upstream Chat 2 issues
|
||||||
|
|
||||||
|
If the issue exists in upstream Chat 2 too, please report it at
|
||||||
|
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo/issues).
|
||||||
|
That keeps the original maintainers in the loop and helps everyone
|
||||||
|
who uses Chat 2 directly.
|
||||||
|
|
||||||
|
## Response times
|
||||||
|
|
||||||
|
Weekdays during European business hours. Weekends and FFXIV patch
|
||||||
|
days, replies will be slower. A few days of silence on a non-urgent
|
||||||
|
issue is normal; pinging once after a week is fine.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
HellionChat ships and depends on a number of third-party components.
|
||||||
|
This document lists them, their licences and which of them touch the
|
||||||
|
network. It is the inventory referenced by `PRIVACY.md`.
|
||||||
|
|
||||||
|
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Direct NuGet dependencies
|
||||||
|
|
||||||
|
Pinned in `ChatTwo/ChatTwo.csproj`. Versions reflect the v0.5.4 build.
|
||||||
|
|
||||||
|
| Package | Version | Licence | Network | Purpose |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. |
|
||||||
|
| [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. |
|
||||||
|
| [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. |
|
||||||
|
| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.3.0 | MIT | no | Parser combinator library used for chat-input parsing. |
|
||||||
|
| [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. |
|
||||||
|
|
||||||
|
Six Labors note: HellionChat is an EUPL-1.2-licensed open-source
|
||||||
|
project distributed at no cost. Use of ImageSharp 3.x under the
|
||||||
|
Six Labors Split License 1.0 is permitted on that basis. Anyone
|
||||||
|
forking HellionChat for closed-source or commercial redistribution
|
||||||
|
should review the
|
||||||
|
[Six Labors licence terms](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
|
||||||
|
and obtain a commercial licence if required.
|
||||||
|
|
||||||
|
## SDK and tooling
|
||||||
|
|
||||||
|
| Component | Licence | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [Dalamud.NET.Sdk](https://github.com/goatcorp/Dalamud) 15.0.0 | AGPL-3.0 (Dalamud) / SDK terms per goatcorp | Plugin SDK; pulls in DalamudPackager 15.0.0. |
|
||||||
|
| [.NET 10 SDK](https://dotnet.microsoft.com/) | MIT | Build toolchain. |
|
||||||
|
|
||||||
|
## Bundled assets
|
||||||
|
|
||||||
|
| Asset | Licence | Source |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Exo 2 (`HellionFont.ttf`) | SIL Open Font License 1.1 | [Google Fonts / Natanael Gama](https://fonts.google.com/specimen/Exo+2). The OFL licence text travels embedded next to the font (`HellionFont-OFL.txt`) to satisfy the "licence must be distributed with the font" clause. |
|
||||||
|
| Hellion plugin icon (`images/icon.png`) | © Hellion Media, included under the project licence (EUPL-1.2). | Original artwork. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upstream code
|
||||||
|
|
||||||
|
HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo)
|
||||||
|
by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2.
|
||||||
|
The bulk of the code, including the message store architecture, the
|
||||||
|
channel logic, the hook system and the ImGui chat window, originates
|
||||||
|
from upstream. See `NOTICE.md` and `UPSTREAM_SYNC.md` for the
|
||||||
|
attribution and the cherry-pick policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components that touch the network
|
||||||
|
|
||||||
|
Of everything listed above, **none** of the bundled or NuGet
|
||||||
|
components opens network connections on their own. All outbound
|
||||||
|
traffic is initiated explicitly by HellionChat's own source files
|
||||||
|
and is documented in `PRIVACY.md` under "Outbound network calls":
|
||||||
|
|
||||||
|
- `ChatTwo/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting)
|
||||||
|
- `ChatTwo/FontManager.cs` → Square Enix Lodestone font CDN (one-time
|
||||||
|
download)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifying this list
|
||||||
|
|
||||||
|
To regenerate the dependency inventory after a version bump:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet list ChatTwo.sln package --include-transitive
|
||||||
|
```
|
||||||
|
|
||||||
|
The "direct NuGet dependencies" table above only lists direct
|
||||||
|
references. Transitive dependencies pulled in by Dalamud SDK or by
|
||||||
|
the listed packages are covered by the SDK / package licences and
|
||||||
|
documented by their respective maintainers.
|
||||||
|
|
||||||
|
To re-audit the network-call inventory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn -E "HttpClient|HttpRequest|new Uri\(|https?://" \
|
||||||
|
--include="*.cs" ChatTwo/
|
||||||
|
```
|
||||||
|
|
||||||
|
Any new hit that is not a click-through (`Util.OpenLink`) or a
|
||||||
|
payload-parsing call must be added to `PRIVACY.md` before release.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Upstream sync workflow
|
||||||
|
|
||||||
|
HellionChat is a standalone EUPL-1.2 fork of [Chat 2](https://github.com/Infiziert90/ChatTwo).
|
||||||
|
We pull selected patches from upstream manually instead of running an
|
||||||
|
automated mirror. This file documents how that works so anyone (including
|
||||||
|
future-me) can do it cleanly.
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
Add the upstream repo as a remote on a fresh clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/Infiziert90/ChatTwo.git
|
||||||
|
git fetch upstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify both remotes are wired up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote -v
|
||||||
|
# origin https://github.com/JonKazama-Hellion/HellionChat.git (fetch)
|
||||||
|
# origin https://github.com/JonKazama-Hellion/HellionChat.git (push)
|
||||||
|
# upstream https://github.com/Infiziert90/ChatTwo.git (fetch)
|
||||||
|
# upstream https://github.com/Infiziert90/ChatTwo.git (push)
|
||||||
|
```
|
||||||
|
|
||||||
|
You never push to `upstream`. It is read-only for us.
|
||||||
|
|
||||||
|
## Reviewing what is new upstream
|
||||||
|
|
||||||
|
Before any feature cycle starts I run a quick check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch upstream
|
||||||
|
git log --oneline main..upstream/main | head -30
|
||||||
|
```
|
||||||
|
|
||||||
|
That shows every commit Infi or contributors landed since the last sync.
|
||||||
|
Read the messages, decide which ones apply.
|
||||||
|
|
||||||
|
## What we cherry-pick
|
||||||
|
|
||||||
|
**Always:** security fixes, API-version compatibility patches (Dalamud
|
||||||
|
API 15 → 16 → ...), BetterTTV / emote-cache fixes, regression fixes for
|
||||||
|
the upstream behaviour we still rely on.
|
||||||
|
|
||||||
|
**Sometimes:** small bug fixes in `MessageManager.cs`, `MessageStore.cs`,
|
||||||
|
`ChatLogWindow.cs`, the Tabs system. Pull them when they touch code we
|
||||||
|
have not heavily modified.
|
||||||
|
|
||||||
|
**Never:** webinterface changes (the entire webinterface tree is gone in
|
||||||
|
HellionChat), changes that conflict with the privacy filter, changes that
|
||||||
|
re-add upstream defaults we deliberately reversed (full-history logging,
|
||||||
|
Tell Exclusive defaults, etc.).
|
||||||
|
|
||||||
|
## How we cherry-pick
|
||||||
|
|
||||||
|
Always with `-x` so authorship and the original commit hash stay
|
||||||
|
visible:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b sync/upstream-<topic> main
|
||||||
|
git cherry-pick -x <upstream-commit-sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
`-x` adds a `(cherry picked from commit <sha>)` line to the commit
|
||||||
|
message. That preserves the upstream-author credit and lets anyone
|
||||||
|
reading `git log` trace the change back to ChatTwo. Co-Author trail
|
||||||
|
intact, no AI lines, no "Hellion" prefix on commits that were not
|
||||||
|
authored by us.
|
||||||
|
|
||||||
|
## Conflict handling
|
||||||
|
|
||||||
|
When a cherry-pick conflicts:
|
||||||
|
|
||||||
|
1. Resolve the conflict by hand. Do not "fix" upstream code to match
|
||||||
|
Hellion conventions; that is what the merge marker showed us.
|
||||||
|
2. If the conflict is fundamental (touches code that no longer exists
|
||||||
|
in our fork), abort the cherry-pick and document why in
|
||||||
|
`Hellion Chat Backlog.md` instead. Some upstream patches are not
|
||||||
|
portable; that is fine.
|
||||||
|
3. After a successful resolve, the commit message stays identical to
|
||||||
|
the upstream message, with the `-x` cherry-pick footer Git appends
|
||||||
|
automatically. Do not rewrite the message to match our format.
|
||||||
|
|
||||||
|
## Pushing the sync
|
||||||
|
|
||||||
|
Cherry-picked commits go through the same review as our own work: the
|
||||||
|
sync branch lands in `main` via a no-fast-forward merge, then a release
|
||||||
|
tag if the user-visible behaviour changes (otherwise just merged).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git merge --no-ff sync/upstream-<topic> -m "merge: upstream sync — <topic>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## When upstream goes silent
|
||||||
|
|
||||||
|
If Chat 2 stops receiving updates entirely we keep this workflow alive
|
||||||
|
anyway. The remote stays configured, the documentation stays here. The
|
||||||
|
moment maintenance picks back up we are ready to pull again.
|
||||||
|
|
||||||
|
## When upstream takes a direction we cannot follow
|
||||||
|
|
||||||
|
If a future ChatTwo release breaks compatibility with our privacy
|
||||||
|
philosophy in a way we cannot resolve (e.g. mandatory cloud sync,
|
||||||
|
removal of the local message store, a license change that makes EUPL
|
||||||
|
incompatible), HellionChat continues on its own from the last
|
||||||
|
compatible cherry-pick. The history we already inherited stays under
|
||||||
|
EUPL-1.2 and stays attributed.
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
"project_id" : "663694"
|
|
||||||
"base_path" : "."
|
|
||||||
"base_url" : "https://api.crowdin.com"
|
|
||||||
"preserve_hierarchy": true
|
|
||||||
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
"source" : "/ChatTwo/Resources/Language.resx",
|
|
||||||
"translation" : "/ChatTwo/Resources/Language.%two_letters_code%.resx",
|
|
||||||
"dest" : "/Language.resx",
|
|
||||||
"skip_untranslated_strings": true,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user