Compare commits
82 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 |
@@ -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
|
||||||
@@ -374,6 +374,9 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
#Specs und Plan datein
|
#Specs und Plan datein
|
||||||
/.superpowers/
|
/.superpowers/
|
||||||
|
|
||||||
|
#Test Datein
|
||||||
|
ChatTwo.Tests
|
||||||
TestResults
|
TestResults
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
@@ -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,167 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using ChatTwo.Code;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Game.Text;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
|
|
||||||
namespace ChatTwo.Tests;
|
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs history-preload coverage.
|
|
||||||
//
|
|
||||||
// These tests exercise MessageStore.GetTellHistoryWithSender, the query the
|
|
||||||
// AutoTellTabsService uses to populate a freshly spawned temp tab with the
|
|
||||||
// last conversations with that player.
|
|
||||||
//
|
|
||||||
// NOTE: like the rest of ChatTwo.Tests today, these will fail at runtime
|
|
||||||
// until the project's Dalamud.dll runtime dependency is sorted out (see
|
|
||||||
// Phase-2 backlog item "Test-Projekt fixen"). Compile-time the suite builds
|
|
||||||
// fine via DALAMUD_HOME, so the tests guard against API drift even before
|
|
||||||
// they can be executed locally.
|
|
||||||
[TestClass]
|
|
||||||
[TestSubject(typeof(MessageStore))]
|
|
||||||
public class AutoTellTabsHistoryTest
|
|
||||||
{
|
|
||||||
public TestContext TestContext { get; set; }
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void GetTellHistoryWithSender_FiltersByNameAndWorld()
|
|
||||||
{
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
const ulong receiver = 99001;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
// Two tells with the target sender, one with a different sender on
|
|
||||||
// the same world, one with the same name on a different world. Only
|
|
||||||
// the first two should make it into the result.
|
|
||||||
var asukaLichIn = TellMessage("Asuka", 76, receiver, now.AddMinutes(-30), ChatType.TellIncoming);
|
|
||||||
var asukaLichOut = TellMessage("Asuka", 76, receiver, now.AddMinutes(-20), ChatType.TellOutgoing);
|
|
||||||
var broboLich = TellMessage("Brobo", 76, receiver, now.AddMinutes(-10), ChatType.TellIncoming);
|
|
||||||
var asukaOmega = TellMessage("Asuka", 90, receiver, now.AddMinutes(-5), ChatType.TellIncoming);
|
|
||||||
|
|
||||||
store.UpsertMessage(asukaLichIn);
|
|
||||||
store.UpsertMessage(asukaLichOut);
|
|
||||||
store.UpsertMessage(broboLich);
|
|
||||||
store.UpsertMessage(asukaOmega);
|
|
||||||
|
|
||||||
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 50);
|
|
||||||
|
|
||||||
Assert.AreEqual(2, result.Count);
|
|
||||||
// Result is oldest-first so a tab can append messages chronologically.
|
|
||||||
Assert.AreEqual(asukaLichIn.Id, result[0].Id);
|
|
||||||
Assert.AreEqual(asukaLichOut.Id, result[1].Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void GetTellHistoryWithSender_RespectsLimit()
|
|
||||||
{
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
const ulong receiver = 99002;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
for (var i = 0; i < 30; i++)
|
|
||||||
{
|
|
||||||
var msg = TellMessage("Asuka", 76, receiver, now.AddMinutes(-i - 1), ChatType.TellIncoming);
|
|
||||||
store.UpsertMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 5);
|
|
||||||
|
|
||||||
Assert.AreEqual(5, result.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void GetTellHistoryWithSender_ZeroLimitReturnsEmpty()
|
|
||||||
{
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
const ulong receiver = 99003;
|
|
||||||
|
|
||||||
var msg = TellMessage("Asuka", 76, receiver, DateTimeOffset.UtcNow, ChatType.TellIncoming);
|
|
||||||
store.UpsertMessage(msg);
|
|
||||||
|
|
||||||
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 0);
|
|
||||||
|
|
||||||
Assert.AreEqual(0, result.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void GetTellHistoryWithSender_IgnoresOtherReceivers()
|
|
||||||
{
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
const ulong ourReceiver = 99004;
|
|
||||||
const ulong otherReceiver = 99005;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
// Tell on the local player's account.
|
|
||||||
var ours = TellMessage("Asuka", 76, ourReceiver, now.AddMinutes(-1), ChatType.TellIncoming);
|
|
||||||
// Same sender, but logged against a different local character —
|
|
||||||
// common when the user has alts. Must not surface.
|
|
||||||
var foreign = TellMessage("Asuka", 76, otherReceiver, now, ChatType.TellIncoming);
|
|
||||||
|
|
||||||
store.UpsertMessage(ours);
|
|
||||||
store.UpsertMessage(foreign);
|
|
||||||
|
|
||||||
var result = store.GetTellHistoryWithSender(ourReceiver, "Asuka", 76, limit: 50);
|
|
||||||
|
|
||||||
Assert.AreEqual(1, result.Count);
|
|
||||||
Assert.AreEqual(ours.Id, result[0].Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Message TellMessage(
|
|
||||||
string senderName,
|
|
||||||
uint senderWorld,
|
|
||||||
ulong receiver,
|
|
||||||
DateTimeOffset dateTime,
|
|
||||||
ChatType chatType)
|
|
||||||
{
|
|
||||||
var senderSeString = new SeStringBuilder()
|
|
||||||
.Add(new PlayerPayload(senderName, senderWorld))
|
|
||||||
.AddText(senderName)
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var contentSeString = new SeStringBuilder()
|
|
||||||
.AddText("test message")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var senderChunks = ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, chatType).ToList();
|
|
||||||
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, chatType).ToList();
|
|
||||||
|
|
||||||
var chatCode = new ChatCode((XivChatType)chatType, XivChatRelationKind.LocalPlayer, XivChatRelationKind.LocalPlayer);
|
|
||||||
return new Message(
|
|
||||||
Guid.NewGuid(),
|
|
||||||
receiver,
|
|
||||||
0,
|
|
||||||
dateTime,
|
|
||||||
chatCode,
|
|
||||||
senderChunks,
|
|
||||||
contentChunks,
|
|
||||||
senderSeString,
|
|
||||||
contentSeString,
|
|
||||||
Guid.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
@@ -183,6 +183,22 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
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);
|
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||||
|
|
||||||
// Re-anchor the active tab so the user does not silently end up on
|
// Re-anchor the active tab so the user does not silently end up on
|
||||||
@@ -200,9 +216,24 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
// Preload first so the tab opens with chronological history above
|
// Preload first so the tab opens with chronological history above
|
||||||
// the current message — and so a slow DB query never causes a
|
// the current message — and so a slow DB query never causes a
|
||||||
// visible "empty tab, then history pops in" effect on screen.
|
// visible "empty tab, then history pops in" effect on screen.
|
||||||
PreloadHistory(tab, partner.Name, partner.World);
|
// 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);
|
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);
|
Plugin.Config.Tabs.Add(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +269,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return $"{playerName}@World{worldRowId}";
|
return $"{playerName}@World{worldRowId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PreloadHistory(Tab tab, string senderName, uint senderWorld)
|
private void PreloadHistory(Tab tab, string senderName, uint senderWorld, Guid currentMessageId)
|
||||||
{
|
{
|
||||||
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
|
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
if (preloadCount <= 0)
|
if (preloadCount <= 0)
|
||||||
@@ -248,13 +279,21 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
|
|
||||||
try
|
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(
|
var history = _store.GetTellHistoryWithSender(
|
||||||
_messageManager.CurrentContentId,
|
_messageManager.CurrentContentId,
|
||||||
senderName,
|
senderName,
|
||||||
senderWorld,
|
senderWorld,
|
||||||
preloadCount);
|
preloadCount + 1);
|
||||||
|
|
||||||
if (history.Count == 0)
|
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
|
// No prior tells with this player — leave the tab to start
|
||||||
// empty so the user does not see a "history loaded" marker
|
// empty so the user does not see a "history loaded" marker
|
||||||
@@ -265,7 +304,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
// The history list is already oldest-first, so a plain AddPrune
|
// The history list is already oldest-first, so a plain AddPrune
|
||||||
// loop produces the chronological order the user expects to see
|
// loop produces the chronological order the user expects to see
|
||||||
// when the tab opens.
|
// when the tab opens.
|
||||||
foreach (var message in history)
|
foreach (var message in historicMessages)
|
||||||
{
|
{
|
||||||
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||||||
}
|
}
|
||||||
@@ -343,6 +382,27 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
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);
|
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||||
|
|
||||||
// Force a switch to tab 0 if the active tab was a temp tab OR
|
// Force a switch to tab 0 if the active tab was a temp tab OR
|
||||||
|
|||||||
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.5.0</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>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 10;
|
private const int LatestVersion = 12;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -73,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
|
||||||
@@ -101,6 +103,33 @@ public class Configuration : IPluginConfiguration
|
|||||||
// want the auto tabs themselves without the extra UI affordance.
|
// want the auto tabs themselves without the extra UI affordance.
|
||||||
public bool AutoTellTabsShowGreetedToggle;
|
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))
|
||||||
@@ -158,7 +187,11 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool PlaySounds = true;
|
public bool PlaySounds = true;
|
||||||
public bool KeepInputFocus = true;
|
public bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 5_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 = [];
|
||||||
@@ -290,6 +323,11 @@ public class Configuration : IPluginConfiguration
|
|||||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||||
|
|
||||||
|
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||||
|
PopOutInputEnabled = other.PopOutInputEnabled;
|
||||||
|
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
|
||||||
|
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
+77
-221
@@ -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,248 +48,96 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**Hellion Chat 0.5.0 — Settings UX polish**
|
**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**
|
||||||
|
|
||||||
The settings window has been pulled apart and rebuilt around eight
|
- Pop-out button now visible in the chat header (no more hunting
|
||||||
themed tabs instead of the twelve organic ones it grew into.
|
through the right-click menu)
|
||||||
Settings now sit where they belong and the wall-of-text descriptions
|
- One-time hint banner explains pop-out tabs and the right-click
|
||||||
have been replaced with hover help markers across every section.
|
shortcut
|
||||||
|
- New setting: open new /tell tabs directly as pop-out windows
|
||||||
|
(Settings → Chat → Auto-Tell-Tabs)
|
||||||
|
- Pop-out input is now enabled by default — closing a pop-out still
|
||||||
|
returns the tab to the sidebar
|
||||||
|
- Bugfix: dropping or logging out with an LRU/popped auto-tell tab
|
||||||
|
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)
|
||||||
|
|
||||||
What changed in this release:
|
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||||
|
|
||||||
- Twelve tabs collapsed into eight: General, Appearance, Window,
|
|
||||||
Chat, Tabs, Privacy, Database and Information
|
|
||||||
- Theme and font controls moved out of the Privacy tab into
|
|
||||||
Appearance where they belong
|
|
||||||
- Auto-Tell-Tabs settings, message preview and emote controls now
|
|
||||||
live under one Chat tab with collapsible sections
|
|
||||||
- About and Changelog merged into a single Information tab
|
|
||||||
- Disabled settings keep their tooltip help marker visible so you
|
|
||||||
can still read why an option is greyed out
|
|
||||||
- Section headings start collapsed by default, the same pattern
|
|
||||||
used for the Auto-Tell-Tabs preload section in 0.4.0
|
|
||||||
|
|
||||||
Configuration version bumps from 9 to 10 as a wipe migration. The
|
|
||||||
old config file is copied to HellionChat.json.pre-v10-backup before
|
|
||||||
the new defaults are written, so you can restore your previous
|
|
||||||
setup by hand if anything looks off. A one-shot notification on
|
|
||||||
first start explains the reset.
|
|
||||||
|
|
||||||
No changes to message storage, retention sweep, the privacy filter
|
|
||||||
or the export pipeline. Tabs and chat history are untouched by the
|
|
||||||
migration.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.4.0 — Auto-Tell-Tabs**
|
**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**
|
||||||
|
|
||||||
Auto-Tell-Tabs lets you turn each /tell into a session-only tab
|
Two opt-in UX features land in the same release. Existing users see
|
||||||
dedicated to that conversation partner. The original use case is
|
no change unless they enable the new toggles.
|
||||||
the FFXIV club greeter who has to track 5–15 parallel "hi, welcome"
|
|
||||||
exchanges; everyone else can disable the feature in one click and
|
|
||||||
go back to a single Tell Exclusive tab.
|
|
||||||
|
|
||||||
What lands in this release:
|
Pop-out input bar:
|
||||||
|
|
||||||
- Auto-spawn temp tab "Name@World" on /tell (incoming and outgoing)
|
- New global master switch in Settings → Window → Frame: "Enable input
|
||||||
- Tab limit (default 15, range 1–50) with LRU drop that prefers
|
in pop-outs". Default OFF so existing behaviour is preserved
|
||||||
greeted tabs first, then sorts by last activity
|
- When enabled, every pop-out window grows a compact input bar at the
|
||||||
- History preload from the local message store (default 20 tells,
|
bottom (channel-coloured icon button left, text input right). The
|
||||||
range 0–100) with a "— Earlier conversations —" separator above
|
auto-translate picker is intentionally not part of the compact bar
|
||||||
the live tell that triggered the spawn
|
in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)
|
||||||
- Optional "mark as greeted" toggle button (off by default,
|
rarely need it there
|
||||||
greeter-specific) that dims the tab name and lets you flip the
|
- Each pop-out keeps an independent text buffer and history cursor;
|
||||||
status
|
channel changes still apply globally because that is how the FFXIV
|
||||||
- Section header "Active Tells (n)" or compact-mode separator in
|
channel API works
|
||||||
the sidebar between persistent tabs and the temp tabs
|
- Up/Down navigates a shared input history singleton across the main
|
||||||
- Settings UI under Chat (toggle / limit / compact / greeted-toggle)
|
window and every open pop-out
|
||||||
and Privacy (history preload count), with hover-tooltip help
|
- First pop-out opening after the upgrade shows a one-time hint
|
||||||
markers replacing the previous wall-of-text descriptions for the
|
banner pointing users to the new toggle
|
||||||
new sections
|
|
||||||
- Save and load filters strip temp tabs from the on-disk config so
|
|
||||||
a crash or a sidebar-mode toggle never persists or wipes them
|
|
||||||
|
|
||||||
Compatibility note: if XIV Messanger or another plugin is
|
Chat colour presets:
|
||||||
suppressing direct messages, disable its "Suppress DMs" option so
|
|
||||||
Hellion Chat can receive tells and open the auto tabs.
|
|
||||||
|
|
||||||
Configuration version bumps from 8 to 9. Existing users get a one-
|
- Seven built-in presets above the per-channel colour list in
|
||||||
shot notification on the first start, defaults are seeded by
|
Settings → Appearance → Colours: ChatTwo Default, High-Contrast,
|
||||||
property initializers, persistent tabs are untouched.
|
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
|
||||||
|
|
||||||
The vertical sidebar tab view becomes the default for fresh
|
Configuration migrates from v10 to v11 with a diagnostic log entry;
|
||||||
installs; existing users keep their saved preference.
|
no data is reset. Bilingual (English/German) for both new sections.
|
||||||
|
|
||||||
Inspired by the per-sender tab pattern in XIV InstantMessenger
|
|
||||||
(Limiana, AGPL-3.0). No code was ported across the licence
|
|
||||||
boundary; only the architectural concept influenced this design.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.3.1 — Upstream emote regression fix**
|
**Hellion Chat 0.5.4 — WrapText hardening**
|
||||||
|
|
||||||
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
|
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
||||||
from API 15 updates" which changes the BetterTTV emote DTOs
|
Span- and index-based control flow. Closes the persistent CodeQL
|
||||||
(Emote and Top100) from public fields to public properties.
|
Critical alert "unvalidated local pointer arithmetic" that kept
|
||||||
System.Text.Json under the API 15 toolchain only honours the
|
re-firing on every shape of the previous fix.
|
||||||
[JsonPropertyName] attribute on properties, so the previous
|
|
||||||
field-based version deserialised every fetched emote into empty
|
|
||||||
default values. Result: BetterTTV emotes were silently broken
|
|
||||||
on fresh installs. The fix is six lines and applies cleanly on
|
|
||||||
top of our defensive null-check from earlier; the EmoteCache
|
|
||||||
path-traversal hardening from 0.3.0 stays as it is.
|
|
||||||
|
|
||||||
Authorship of the fix is preserved with git cherry-pick -x, so
|
Hardening:
|
||||||
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**
|
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
|
||||||
|
via ArrayPool, validates the actual encoded length against that
|
||||||
|
ceiling, and threads the rest of the algorithm through int offsets
|
||||||
|
instead of raw byte pointers
|
||||||
|
- Pointer arithmetic only happens inside two small private helpers
|
||||||
|
(CalcWordWrap and DrawText) that take the pinned base pointer plus
|
||||||
|
int offsets sourced from the plugin's own logic, not from any
|
||||||
|
virtual-method return
|
||||||
|
- Added a 16 KiB upper bound on the buffer rent to prevent a
|
||||||
|
pathological input from triggering an unbounded ArrayPool allocation
|
||||||
|
|
||||||
This release closes the remaining audit follow-ups from the
|
No user-visible behaviour change. Word-wrap output is byte-identical
|
||||||
0.2.0 cleanup and finishes turning Hellion Chat into a properly
|
to v0.5.3.
|
||||||
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.5.3 — Pointer arithmetic hardening**
|
||||||
|
|
||||||
The upstream webinterface has been removed in its entirety. It
|
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
||||||
serves a different use case from the smaller default footprint
|
encoded byte buffer length via GetByteCount before pointer
|
||||||
this fork is built around, namely remote access to chat from a
|
arithmetic. Single-fix patch on top of v0.5.2.
|
||||||
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:
|
---
|
||||||
|
|
||||||
- Settings tab "Webinterface" is gone, the corresponding
|
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
||||||
Configuration fields (WebinterfaceEnabled / AutoStart / Password /
|
|
||||||
Port / AuthStore / MaxLinesToSend) are dropped and stale entries
|
|
||||||
fall out of the JSON on the next save automatically
|
|
||||||
- The whole ChatTwo/Http tree, the bundled Svelte frontend in
|
|
||||||
websiteBuild.zip and the WebinterfaceUtil helper are deleted
|
|
||||||
- Watson.Lite (the HTTP server) and Newtonsoft.Json (only used by
|
|
||||||
the webinterface JSON wire format) are removed from the
|
|
||||||
package references
|
|
||||||
- DbViewer's "Chat2 JSON Export" button is dropped because it
|
|
||||||
serialised the database into the webinterface message protocol;
|
|
||||||
the Privacy tab's MessageExporter (Markdown, JSON, CSV with
|
|
||||||
channel and date filters) covers the same ground without the
|
|
||||||
proprietary shape
|
|
||||||
- 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
|
|
||||||
or export pipeline. Existing chat history is preserved.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.2 — About tab rebrand, DBViewer polish**
|
|
||||||
|
|
||||||
- About tab now shows Hellion-specific maintainer, license, EU/US/JP
|
|
||||||
disclaimer and SQUARE ENIX disclaimer instead of the inherited
|
|
||||||
Chat 2 contact info; original ChatTwo translator credits stay
|
|
||||||
visible under a clearly labelled upstream tree node
|
|
||||||
- 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**
|
|
||||||
|
|
||||||
- Plugin icon now ships inside the bundle, so the Hellion logo
|
|
||||||
renders locally in the Dalamud plugin list once installed (the
|
|
||||||
previous release relied only on the remote IconUrl)
|
|
||||||
- Plugin icon downsampled from 1024×1024 to 256×256 to match the
|
|
||||||
rendered size; loads faster and caches better
|
|
||||||
- Migration from upstream Chat 2 is more robust: each file move is
|
|
||||||
wrapped individually, a locked SQLite database no longer aborts
|
|
||||||
the rest of the migration, and a warning notification fires when
|
|
||||||
any file is held open (with a hint to disable Chat 2 and restart
|
|
||||||
the game)
|
|
||||||
- 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**
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
-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,
|
||||||
@@ -693,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 = @"
|
||||||
@@ -722,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 = @"
|
||||||
@@ -763,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 = @"
|
||||||
@@ -806,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
|
||||||
|
|||||||
+33
-1
@@ -152,6 +152,38 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
|
||||||
|
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
|
||||||
|
// input feature. Lightweight migration: defaults both fields,
|
||||||
|
// no user-facing notification because the change is opt-in only.
|
||||||
|
if (Config.Version < 11)
|
||||||
|
{
|
||||||
|
Config.PopOutInputEnabled = false;
|
||||||
|
Config.SeenPopOutInputHint = false;
|
||||||
|
Config.Version = 11;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
|
||||||
|
"SeenPopOutInputHint added (default false)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Hellion default tab layout for first-run and v10-wipe.
|
||||||
// General catches player chat plus active gameplay events; the
|
// General catches player chat plus active gameplay events; the
|
||||||
// System tab takes the technical noise so it does not bury real
|
// System tab takes the technical noise so it does not bury real
|
||||||
@@ -241,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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -64,6 +64,7 @@ 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 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));
|
||||||
@@ -178,6 +179,8 @@ internal class HellionStrings
|
|||||||
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description));
|
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_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_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_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
|
||||||
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
|
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
|
||||||
|
|
||||||
@@ -196,6 +199,8 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Tab_Appearance => Get(nameof(Settings_Tab_Appearance));
|
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_Window => Get(nameof(Settings_Tab_Window));
|
||||||
internal static string Settings_Tab_Chat => Get(nameof(Settings_Tab_Chat));
|
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));
|
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
||||||
|
|
||||||
// Hellion Chat — General-Tab section headings
|
// Hellion Chat — General-Tab section headings
|
||||||
@@ -239,4 +244,30 @@ internal class HellionStrings
|
|||||||
internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner));
|
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 => Get(nameof(Tabs_Presets_Linkshell));
|
||||||
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
|
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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@
|
|||||||
<data name="Retention_Help_SavedNote" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
@@ -403,6 +406,12 @@
|
|||||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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">
|
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
|
||||||
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
|
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -445,6 +454,12 @@
|
|||||||
<data name="Settings_Tab_Chat" xml:space="preserve">
|
<data name="Settings_Tab_Chat" xml:space="preserve">
|
||||||
<value>Chat</value>
|
<value>Chat</value>
|
||||||
</data>
|
</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">
|
<data name="Settings_Tab_Information" xml:space="preserve">
|
||||||
<value>Über</value>
|
<value>Über</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -546,4 +561,52 @@
|
|||||||
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -81,6 +81,9 @@
|
|||||||
<data name="Retention_Help_SavedNote" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
@@ -403,6 +406,12 @@
|
|||||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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">
|
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
|
||||||
<value>The number of preloaded tells is configured in the Privacy tab.</value>
|
<value>The number of preloaded tells is configured in the Privacy tab.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -445,6 +454,12 @@
|
|||||||
<data name="Settings_Tab_Chat" xml:space="preserve">
|
<data name="Settings_Tab_Chat" xml:space="preserve">
|
||||||
<value>Chat</value>
|
<value>Chat</value>
|
||||||
</data>
|
</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">
|
<data name="Settings_Tab_Information" xml:space="preserve">
|
||||||
<value>Information</value>
|
<value>Information</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -546,4 +561,52 @@
|
|||||||
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
|
<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>
|
<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>
|
||||||
|
<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;
|
||||||
|
}
|
||||||
+137
-15
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,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
|
||||||
@@ -925,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))
|
||||||
@@ -1286,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1374,8 +1398,18 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
// Dim the tab name once the user marked the partner
|
// Dim the tab name once the user marked the partner
|
||||||
// as greeted, so a glance at the sidebar tells them
|
// as greeted, so a glance at the sidebar tells them
|
||||||
// who still needs attention.
|
// 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.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Header, dimHeader))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, dimHover))
|
||||||
{
|
{
|
||||||
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
|
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
|
||||||
}
|
}
|
||||||
@@ -1408,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}");
|
||||||
@@ -1471,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();
|
||||||
@@ -1747,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--;
|
||||||
@@ -1756,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;
|
||||||
}
|
}
|
||||||
@@ -1764,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);
|
||||||
|
|
||||||
|
|||||||
+94
-1
@@ -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;
|
||||||
@@ -93,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();
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
globalChooser?.ResultTask.ContinueWith(r =>
|
globalChooser?.ResultTask.ContinueWith(r =>
|
||||||
{
|
{
|
||||||
if (r.IsCompletedSuccessfully)
|
if (r.IsCompletedSuccessfully)
|
||||||
Mutable.GlobalFontV2 = r.Result;
|
{
|
||||||
|
Plugin.Framework.Run(() => Mutable.GlobalFontV2 = r.Result);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Reset##global"))
|
if (ImGui.Button("Reset##global"))
|
||||||
@@ -164,7 +166,9 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
japaneseChooser?.ResultTask.ContinueWith(r =>
|
japaneseChooser?.ResultTask.ContinueWith(r =>
|
||||||
{
|
{
|
||||||
if (r.IsCompletedSuccessfully)
|
if (r.IsCompletedSuccessfully)
|
||||||
Mutable.JapaneseFontV2 = r.Result;
|
{
|
||||||
|
Plugin.Framework.Run(() => Mutable.JapaneseFontV2 = r.Result);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Reset##japanese"))
|
if (ImGui.Button("Reset##japanese"))
|
||||||
@@ -179,7 +183,9 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
italicChooser?.ResultTask.ContinueWith(r =>
|
italicChooser?.ResultTask.ContinueWith(r =>
|
||||||
{
|
{
|
||||||
if (r.IsCompletedSuccessfully)
|
if (r.IsCompletedSuccessfully)
|
||||||
Mutable.ItalicFontV2 = r.Result;
|
{
|
||||||
|
Plugin.Framework.Run(() => Mutable.ItalicFontV2 = r.Result);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Reset##italic"))
|
if (ImGui.Button("Reset##italic"))
|
||||||
@@ -224,6 +230,12 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
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 (_, types) in ChatTypeExt.SortOrder)
|
||||||
{
|
{
|
||||||
foreach (var type in types)
|
foreach (var type in types)
|
||||||
@@ -257,6 +269,63 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
private void DrawTimestampsSection()
|
||||||
{
|
{
|
||||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading);
|
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading);
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
|
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
|
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);
|
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ChatTwo.Resources;
|
using ChatTwo.Resources;
|
||||||
using ChatTwo.Util;
|
using ChatTwo.Util;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
@@ -81,10 +82,12 @@ internal sealed class General : ISettingsTab
|
|||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
{
|
{
|
||||||
if (ImGuiUtil.InputIntVertical(Language.Options_MaxLinesToShow_Name, Language.Options_MaxLinesToShow_Description, ref Mutable.MaxLinesToRender))
|
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.InputInt(Language.Options_MaxLinesToShow_Name, ref Mutable.MaxLinesToRender))
|
||||||
{
|
{
|
||||||
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
|
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
|
||||||
}
|
}
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_MaxLinesToShow_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -55,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
|
||||||
@@ -484,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))
|
||||||
@@ -496,10 +514,22 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CleanupPreviewStale)
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
|
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
|
using (var staleColor = CleanupPreviewStale
|
||||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
|
? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
|
||||||
|
: null)
|
||||||
|
{
|
||||||
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
|
||||||
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
|
||||||
|
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
|
||||||
|
}
|
||||||
|
|
||||||
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
|
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
|
||||||
{
|
{
|
||||||
@@ -555,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ internal sealed class Tabs : ISettingsTab
|
|||||||
private Plugin Plugin { get; }
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ internal sealed class Window : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar);
|
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);
|
ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description);
|
ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description);
|
||||||
|
|
||||||
|
|||||||
+143
-90
@@ -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,123 +59,175 @@ 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)
|
|
||||||
{
|
|
||||||
var oldPos = ImGui.GetCursorScreenPos();
|
|
||||||
|
|
||||||
ImGuiNative.TextUnformatted(text, textEnd);
|
|
||||||
PostPayload(chunk, handler);
|
|
||||||
|
|
||||||
if (!ReferenceEquals(LastLink, chunk.Link))
|
|
||||||
PayloadBounds.Clear();
|
|
||||||
|
|
||||||
LastLink = chunk.Link;
|
|
||||||
|
|
||||||
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
|
|
||||||
{
|
|
||||||
defaultText.W = 0.25f;
|
|
||||||
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
|
||||||
|
|
||||||
foreach (var (start, size) in PayloadBounds)
|
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(start, start + size, actualCol);
|
|
||||||
|
|
||||||
PayloadBounds.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Hovered == null && chunk.Link != null)
|
|
||||||
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (csText.Length == 0)
|
if (csText.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
||||||
{
|
{
|
||||||
var bytes = Encoding.UTF8.GetBytes(part);
|
if (part.Length == 0)
|
||||||
fixed (byte* rawText = bytes)
|
|
||||||
{
|
{
|
||||||
var text = rawText;
|
ImGui.TextUnformatted("");
|
||||||
var textEnd = text + bytes.Length;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// empty string
|
// Allocate against the encoder's own MaxByteCount so the buffer
|
||||||
if (text == null)
|
// 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("");
|
ImGui.TextUnformatted("");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var widthLeft = ImGui.GetContentRegionAvail().X;
|
WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
|
||||||
var endPrevLine = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft);
|
}
|
||||||
if (endPrevLine == null)
|
finally
|
||||||
continue;
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var firstSpace = FindFirstSpace(text, textEnd);
|
private static unsafe void WrapEncodedLine(ReadOnlySpan<byte> bytes, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
||||||
var properBreak = firstSpace <= endPrevLine;
|
{
|
||||||
|
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)
|
if (properBreak)
|
||||||
{
|
lineStart = endPrev;
|
||||||
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
|
// Skip a leading space at the start of a wrapped line.
|
||||||
if (wrapPos >= firstSpace)
|
if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
|
||||||
ImGui.TextUnformatted("");
|
lineStart++;
|
||||||
}
|
|
||||||
|
var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
|
||||||
|
if (properBreak && newEnd == endPrev)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (newEnd < 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
endPrev = newEnd;
|
||||||
while (endPrevLine < textEnd)
|
DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);
|
||||||
|
|
||||||
|
if (!properBreak)
|
||||||
{
|
{
|
||||||
if (properBreak)
|
properBreak = true;
|
||||||
text = endPrevLine;
|
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
|
||||||
// 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)
|
private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
|
||||||
{
|
{
|
||||||
for (var i = text; i < textEnd; i++)
|
var result = ImGuiNative.CalcWordWrapPositionA(
|
||||||
if (char.IsWhiteSpace((char) *i))
|
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();
|
||||||
|
|
||||||
|
ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
|
||||||
|
PostPayload(chunk, handler);
|
||||||
|
|
||||||
|
if (!ReferenceEquals(LastLink, chunk.Link))
|
||||||
|
PayloadBounds.Clear();
|
||||||
|
|
||||||
|
LastLink = chunk.Link;
|
||||||
|
|
||||||
|
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
|
||||||
|
{
|
||||||
|
defaultText.W = 0.25f;
|
||||||
|
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
||||||
|
|
||||||
|
foreach (var (boundsStart, boundsSize) in PayloadBounds)
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);
|
||||||
|
|
||||||
|
PayloadBounds.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hovered == null && chunk.Link != null)
|
||||||
|
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
|
||||||
|
{
|
||||||
|
for (var i = start; i < end; i++)
|
||||||
|
if (char.IsWhiteSpace((char)bytes[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)
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ public static class TabsUtil
|
|||||||
[ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All),
|
[ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||||
[ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All),
|
[ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||||
},
|
},
|
||||||
Channel = InputChannel.Party,
|
// 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()
|
public static Tab HellionBeginner => new()
|
||||||
|
|||||||
+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