Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7f2c40e6 | |||
| 93d52ae819 | |||
| 48b3d5c6b1 | |||
| e9a9d8a01c | |||
| a155a57f33 | |||
| 90b83a0690 | |||
| f10301c3e4 | |||
| 8571a936a4 | |||
| 3f6144836c | |||
| 53c432a635 | |||
| 340cadf3b9 | |||
| 8d6868aef6 | |||
| 6e8fcc8cc3 | |||
| 57670ffc76 | |||
| 2144eedd76 | |||
| 43daef83de | |||
| 4a9ad426e7 | |||
| 13beda3a8d | |||
| 18c05af4db | |||
| df6e1e1cbd | |||
| 01b1a14511 | |||
| b6af8d559c | |||
| 22dbfc2e24 | |||
| 2f3b01732c | |||
| 88803382dd | |||
| 74c51163c7 | |||
| 877ff4ba18 | |||
| ad2feb5a27 | |||
| 46b63ffdd1 | |||
| 4ba5004322 | |||
| 3584c94523 | |||
| 303729f3d3 | |||
| 12085ff1e2 | |||
| e4593a0fda | |||
| 3fc42963ae | |||
| 7c52e890e6 | |||
| 4d977d5118 | |||
| ddd72a878e | |||
| 66450dd518 | |||
| 7de28ef9b2 | |||
| da3c1f6832 | |||
| e66ae1f5b4 | |||
| 281a1e172f | |||
| 45a5035426 | |||
| e1931fc7d2 | |||
| 2201478a54 | |||
| 50963ccf1b | |||
| fde85e6d69 | |||
| c22b169b73 | |||
| 6839ccaf34 | |||
| fa108c2271 | |||
| 395a0d7c98 | |||
| b76bfb3cfc | |||
| 0512e4729c | |||
| 654f24c609 | |||
| 0e2a14197c | |||
| 52e163a472 | |||
| e086afe2a8 | |||
| c97ce7543b | |||
| cca4571470 | |||
| 444d7f8e2e | |||
| 71ae95d79c | |||
| 9a38f7f094 | |||
| c33e519bb9 | |||
| 14e585ef63 | |||
| d4aa3971c5 | |||
| e9ec587e3b | |||
| 39cd7ab801 | |||
| bb6259e14d | |||
| 757370dd53 | |||
| 3f35b76c54 | |||
| 74bdc4f927 | |||
| eb379d84ef | |||
| 7add74dbbe | |||
| e91c7a3888 | |||
| f8b0804321 | |||
| a9d4e9bd69 | |||
| 7e3e4c8b72 | |||
| 397c84be2c | |||
| 269708150d | |||
| a2977ef75b | |||
| baa4d011e8 | |||
| 4810e8b518 | |||
| 133f5c536f | |||
| 92bb368d2b | |||
| 07f47f32e3 | |||
| 141fcbf074 | |||
| 32c410e8e2 | |||
| 824037e55f | |||
| 173cb76bea | |||
| 2736551505 | |||
| 0679a0e57a | |||
| 02cbfff748 | |||
| 9c86619c9f | |||
| 6b44310e04 | |||
| 59332ce9ea | |||
| 462530dec5 | |||
| 8e964ca498 | |||
| 1f2cb000a2 | |||
| 4f25c2756b | |||
| de0d2c80cd | |||
| 2ce30383d9 | |||
| a857714064 | |||
| 705c7d3116 | |||
| bf5d03c7ea | |||
| 960ce980d3 | |||
| c09aa26ffc | |||
| c2801c4113 | |||
| 7bacd1aaba | |||
| 23e0f37dfb | |||
| 96fa05dc9b | |||
| d891ec5e50 | |||
| e219b3e1fe | |||
| 135f7a9bf7 | |||
| 81d3c9ca6b | |||
| cb90c6ab93 | |||
| 2ad81cc3ef |
@@ -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.1"
|
||||
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,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,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,61 @@
|
||||
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.
|
||||
# Does not consume any user-controlled event payload, only the tag name
|
||||
# (validated by the on.tags filter) and the steps output of the locate
|
||||
# step (path string from Get-ChildItem on a controlled directory).
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
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: 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
|
||||
|
||||
- name: Attach to GitHub release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
fail_on_unmatched_files: true
|
||||
generate_release_notes: false
|
||||
@@ -372,6 +372,11 @@ MigrationBackup/
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
#Specs und Plan datein
|
||||
/.superpowers/
|
||||
|
||||
#Test Datein
|
||||
ChatTwo.Tests
|
||||
TestResults
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
+56
-21
@@ -1,18 +1,49 @@
|
||||
# AI assistance disclosure
|
||||
|
||||
Per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/),
|
||||
this fork uses AI assistance at the **Pair** level. Pair means the maintainer
|
||||
plans the architecture, decides what gets built, reviews each change and
|
||||
tests against the running game; Claude (Anthropic) helps explain Dalamud
|
||||
APIs, suggests patterns, drafts code on request, and reviews approaches.
|
||||
Neither side acts autonomously: nothing ships without the maintainer's
|
||||
review, and Claude can't run the game.
|
||||
This fork uses AI assistance per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/)
|
||||
at the **Pair** level.
|
||||
|
||||
The level varies by area and over time. Some commits are mostly hand-written
|
||||
with the AI used as a sounding board, others lean more on Claude for an API
|
||||
walkthrough or a code draft that the maintainer then reads, edits and
|
||||
integrates. The maintainer's commitment is to be able to explain why every
|
||||
piece of Hellion code is the way it is — not "I typed every character."
|
||||
A note up front: Hellion Chat is currently in a rebuild and adjustment
|
||||
phase, and there are no plans to submit it to the Dalamud team for review
|
||||
while it stays standalone. If the plugin stays out of the official repo I
|
||||
technically wouldn't need to disclose any of this, but I'd rather be
|
||||
upfront about how it's built.
|
||||
|
||||
Hellion Chat is my entry point into game modding and plugin development. I
|
||||
have never written a plugin for a game before. I work alone, so I get help
|
||||
where I need it. That's not something I want to hide.
|
||||
|
||||
## How I actually work
|
||||
|
||||
I plan the architecture, decide what gets built, and own every design
|
||||
decision. For each change I:
|
||||
|
||||
- Read the code Claude drafts before I integrate it
|
||||
- Test with my own tooling and in the running game
|
||||
- Read the Dalamud log output to verify behaviour
|
||||
- Run security and privacy audits on anything that touches user data
|
||||
|
||||
One of the main reasons I use AI is consistency. I want the Hellion code to
|
||||
match the style of the upstream Chat 2 codebase and stay readable for
|
||||
anyone who opens the repo, not just for me. Claude helps me catch when I'm
|
||||
drifting from upstream conventions or writing something that only makes
|
||||
sense in my own head.
|
||||
|
||||
The balance is shifting toward more hand-written work as I get more
|
||||
comfortable with Dalamud and plugin development in general.
|
||||
|
||||
## What AI is used for
|
||||
|
||||
- API explanations (Dalamud, ImGui, .NET specifics I haven't worked with before)
|
||||
- Code drafts that I read, edit, and integrate
|
||||
- Pattern suggestions and code review
|
||||
- Keeping the style aligned with the upstream Chat 2 codebase
|
||||
|
||||
## What AI isn't used for
|
||||
|
||||
- **Visual assets.** Logos, icons, banners, and screenshots are human-drawn
|
||||
or taken from the running game.
|
||||
- **German translations.** Written by me as a native speaker.
|
||||
|
||||
## What's where
|
||||
|
||||
@@ -22,20 +53,24 @@ produced with AI assistance. Hellion-specific code lives in
|
||||
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
|
||||
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
|
||||
and `Plugin.cs`. These were developed with Pair-level assistance as
|
||||
described above; the share of human vs. AI authorship varies file by file
|
||||
and is expected to keep shifting toward more hand-written work as the
|
||||
maintainer's plugin-dev experience grows.
|
||||
described above.
|
||||
|
||||
## What AI is not used for
|
||||
## If AI-assisted development is a dealbreaker for you
|
||||
|
||||
- **Visual assets.** Logos, icons, banners, screenshots are human-drawn or
|
||||
taken from the running game.
|
||||
- **German translations.** Written by the maintainer (native speaker).
|
||||
Fair enough. There are solid alternatives that don't rely on AI in their
|
||||
development:
|
||||
|
||||
- [Chat 2](https://github.com/Infiziert90/ChatTwo), the original upstream
|
||||
this fork is based on
|
||||
- [XIV Instant Messenger](https://github.com/NightmareXIV/XIVInstantMessenger),
|
||||
a different approach to chat in FFXIV
|
||||
|
||||
Both are good projects. Use what fits you best.
|
||||
|
||||
## Tooling
|
||||
|
||||
- Claude (Anthropic) via Claude Code CLI as the main pair partner.
|
||||
- Context7 / Microsoft Learn for current Dalamud and .NET documentation.
|
||||
- Claude (Anthropic) via Claude Code CLI
|
||||
- Context7 / Microsoft Learn for current Dalamud and .NET documentation
|
||||
|
||||
## Contact
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||
|
||||
Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
||||
Original ChatTwo authors and copyright holders of the upstream
|
||||
plugin this fork is built on. Their work covers the message store,
|
||||
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||
hooks, the localisation infrastructure and most of the
|
||||
architecture HellionChat still relies on.
|
||||
|
||||
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
|
||||
HellionChat-specific modifications, including the privacy filter,
|
||||
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
|
||||
Hellion theme and font integration, German localisation and the
|
||||
EUPL-1.2 fork maintenance.
|
||||
|
||||
Licensed under the European Union Public Licence (EUPL), Version 1.2
|
||||
only. The full Licence text lives in the LICENSE file at the root of
|
||||
this repository. The official Licence website is at:
|
||||
|
||||
https://eupl.eu/1.2/en/
|
||||
|
||||
This Work is provided "AS IS" without warranties of any kind. See
|
||||
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
|
||||
Liability) of the Licence for the legally binding wording.
|
||||
|
||||
Acknowledgements directed at the upstream ChatTwo authors live in
|
||||
NOTICE.md. The manual upstream-sync workflow lives in UPSTREAM_SYNC.md.
|
||||
@@ -1,53 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0-windows</TargetFrameworks>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.6.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatTwo\ChatTwo.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
||||
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsCI)' == 'true'">
|
||||
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -1,293 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Chat2PartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
||||
|
||||
namespace ChatTwo.Tests;
|
||||
|
||||
[TestClass]
|
||||
[TestSubject(typeof(MessageStore))]
|
||||
public class MessageStoreTest {
|
||||
// From Message.cs
|
||||
private static readonly byte[] ExtraChatChannelPayloadBytes = [0, 0x27, 18, 0x20];
|
||||
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
public static string GetImportPath() {
|
||||
string[] importPaths = [
|
||||
@".\TestData",
|
||||
@"..\TestData",
|
||||
@"..\..\TestData",
|
||||
@"..\..\..\TestData",
|
||||
];
|
||||
var importPath = importPaths.FirstOrDefault(Directory.Exists);
|
||||
if (string.IsNullOrEmpty(importPath)) {
|
||||
throw new DirectoryNotFoundException("Could not find the import path");
|
||||
}
|
||||
return importPath;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(5000)]
|
||||
public void StoreAndRetrieve() {
|
||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
||||
TestContext.WriteLine("Using database path: " + dbPath);
|
||||
using var store = new MessageStore(dbPath);
|
||||
|
||||
// Write the message.
|
||||
var input = BigMessage();
|
||||
store.UpsertMessage(input);
|
||||
|
||||
// Read the message back.
|
||||
using var messageEnumerator = store.GetMostRecentMessages();
|
||||
var messages = messageEnumerator.ToList();
|
||||
Assert.AreEqual(1, messages.Count);
|
||||
AssertMessagesEqual(input, messages.First());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(5000)]
|
||||
public void RetrieveMultiple() {
|
||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
||||
TestContext.WriteLine("Using database path: " + dbPath);
|
||||
using var store = new MessageStore(dbPath);
|
||||
|
||||
// Insert 10 messages in the wrong order of date.
|
||||
var messages = new List<Message>();
|
||||
const uint receiver = 12345;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
for (var i = 0; i < 10; i++) {
|
||||
var message = BigMessage(true, receiver, now.AddSeconds(-i));
|
||||
TestContext.WriteLine($"Inserting message {i}: {message.Id}");
|
||||
store.UpsertMessage(message);
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
// Insert a message for a different receiver. This shouldn't be returned
|
||||
// because of the receiver filtering.
|
||||
var otherReceiverMsg = BigMessage(receiver: receiver + 1, dateTime: now.AddSeconds(1));
|
||||
TestContext.WriteLine($"Inserting other receiver message: {otherReceiverMsg.Id}");
|
||||
store.UpsertMessage(otherReceiverMsg);
|
||||
|
||||
// Query the most recent 5 messages. Should return the 4 newest messages
|
||||
// from the list, as well as the different receiver message because we
|
||||
// aren't filtering.
|
||||
using var unfilteredMessageEnumerator = store.GetMostRecentMessages(count: 5);
|
||||
var outputMessages = unfilteredMessageEnumerator.ToList();
|
||||
var gotIds = outputMessages.Select(m => m.Id).ToList();
|
||||
TestContext.WriteLine($"Query 1 got IDs: {string.Join(", ", gotIds)}");
|
||||
AssertGuidsEqual(new List<Guid> {
|
||||
messages[3].Id,
|
||||
messages[2].Id,
|
||||
messages[1].Id,
|
||||
messages[0].Id,
|
||||
otherReceiverMsg.Id
|
||||
}, gotIds);
|
||||
|
||||
// Query the most recent 5 messages but filter by receiver ID.
|
||||
using var filteredByReceiverMessageEnumerator = store.GetMostRecentMessages(receiver: receiver, count: 5);
|
||||
outputMessages = filteredByReceiverMessageEnumerator.ToList();
|
||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
||||
TestContext.WriteLine($"Query 2 got IDs: {string.Join(", ", gotIds)}");
|
||||
AssertGuidsEqual(new List<Guid> {
|
||||
messages[4].Id,
|
||||
messages[3].Id,
|
||||
messages[2].Id,
|
||||
messages[1].Id,
|
||||
messages[0].Id,
|
||||
}, gotIds);
|
||||
|
||||
// Query the most recent 5 messages but only since a specific date.
|
||||
using var filteredByReceiverAndDateMessageEnumerator = store.GetMostRecentMessages(receiver, since: messages[1].Date, count: 5);
|
||||
outputMessages = filteredByReceiverAndDateMessageEnumerator.ToList();
|
||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
||||
TestContext.WriteLine($"Query 3 got IDs: {string.Join(", ", gotIds)}");
|
||||
AssertGuidsEqual(new List<Guid> {
|
||||
messages[1].Id,
|
||||
messages[0].Id,
|
||||
}, gotIds);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(5000)]
|
||||
// This test guards against the data format changing in an incompatible way.
|
||||
public void RetrieveExisting() {
|
||||
var input = BigMessage(uniqId: false);
|
||||
|
||||
var dbPath = Path.Join(GetImportPath(), "existing.db");
|
||||
TestContext.WriteLine($"Using existing database: {dbPath}");
|
||||
Assert.IsTrue(File.Exists(dbPath));
|
||||
|
||||
// Uncomment this section to regenerate the existing database.
|
||||
/*
|
||||
File.Delete(dbPath);
|
||||
using (var newStore = new MessageStore(dbPath)) {
|
||||
newStore.UpsertMessage(input);
|
||||
}
|
||||
*/
|
||||
|
||||
using var store = new MessageStore(dbPath);
|
||||
using var existingMessageEnumerator = store.GetMostRecentMessages();
|
||||
var output = existingMessageEnumerator.ToList();
|
||||
Assert.AreEqual(1, output.Count);
|
||||
AssertMessagesEqual(input, output[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(30_000)]
|
||||
public void ProfileMany() {
|
||||
const int count = 20_000;
|
||||
|
||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
||||
TestContext.WriteLine("Using database path: " + dbPath);
|
||||
using var store = new MessageStore(dbPath);
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
var message = BigMessage(uniqId: true);
|
||||
store.UpsertMessage(message);
|
||||
}
|
||||
|
||||
using var messageEnumerator = store.GetMostRecentMessages(count: count);
|
||||
var messages = messageEnumerator.ToList();
|
||||
Assert.AreEqual(count, messages.Count);
|
||||
foreach (var message in messages) {
|
||||
// Load the message because they are lazily parsed.
|
||||
Assert.IsTrue(message.Id != Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Message BigMessage(bool uniqId = true, uint receiver = 12345, DateTimeOffset? dateTime = null) {
|
||||
// NOTE: These values aren't valid in the game.
|
||||
// NOTE: we can't test UiForeground, UiGlow, or AutoTranslatePayload
|
||||
// because they load data from the game.
|
||||
var senderSeString = new SeStringBuilder()
|
||||
.AddText("<")
|
||||
.Add(new PlayerPayload("Player Name", 12345))
|
||||
.AddItalics("Player Name")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddText(">: ")
|
||||
.Build();
|
||||
var extraChatId = Guid.Parse("03d9e6d4-dc1a-4005-bbe7-66b8c3529277");
|
||||
var contentSeString = new SeStringBuilder()
|
||||
.Add(new RawPayload(ExtraChatChannelPayloadBytes.Concat(extraChatId.ToByteArray()).ToArray()))
|
||||
.AddIcon(BitmapFontIcon.IslandSanctuary)
|
||||
.AddMapLink(1, 2, 3, 4)
|
||||
.AddText("map")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddQuestLink(12345)
|
||||
.AddText("quest")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.Add(new DalamudLinkPayload())
|
||||
.AddText("dalamud")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddStatusLink(12345)
|
||||
.AddText("status")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddPartyFinderLink(12345)
|
||||
.AddText("party finder")
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.Build();
|
||||
|
||||
// Add Chat 2 specific payloads (that can't be serialized into the
|
||||
// SeString).
|
||||
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, ChatType.Say).ToList();
|
||||
contentChunks = contentChunks.Concat([
|
||||
new TextChunk(ChunkSource.Content, new Chat2PartyFinderPayload(12345), "chat 2 party finder"),
|
||||
new TextChunk(ChunkSource.Content, new AchievementPayload(12345), "chat 2 achievement"),
|
||||
new TextChunk(ChunkSource.Content, new UriPayload(new Uri("https://dalamud.dev")), "chat 2 uri"),
|
||||
]).ToList();
|
||||
|
||||
var chatCode = new ChatCode((XivChatType)46, XivChatRelationKind.LocalPlayer, XivChatRelationKind.EngagedEnemy);
|
||||
return new Message(
|
||||
uniqId ? Guid.NewGuid() : Guid.Parse("f011343e-6a21-49e5-a6f9-238f0f1f8c2c"),
|
||||
receiver,
|
||||
54321,
|
||||
dateTime ?? DateTimeOffset.FromUnixTimeMilliseconds(1713520182440),
|
||||
chatCode,
|
||||
ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, ChatType.Debug).ToList(),
|
||||
contentChunks,
|
||||
senderSeString,
|
||||
contentSeString,
|
||||
extraChatId
|
||||
);
|
||||
}
|
||||
|
||||
internal static void AssertMessagesEqual(Message input, Message output) {
|
||||
// Check basic fields.
|
||||
Assert.AreEqual(input.Id, output.Id);
|
||||
Assert.AreEqual(input.Receiver, output.Receiver);
|
||||
Assert.AreEqual(input.ContentId, output.ContentId);
|
||||
// Assert time is within 1 second
|
||||
var timeDifference = Math.Abs(input.Date.ToUniversalTime().Subtract(output.Date.ToUniversalTime()).TotalSeconds);
|
||||
Assert.IsTrue(timeDifference < 1);
|
||||
Assert.AreEqual(input.Code, output.Code);
|
||||
Assert.AreEqual($"{input.SenderSource.Encode():X}", $"{output.SenderSource.Encode():X}");
|
||||
Assert.AreEqual($"{input.ContentSource.Encode():X}", $"{output.ContentSource.Encode():X}");
|
||||
Assert.AreEqual(input.SortCodeV2, output.SortCodeV2);
|
||||
Assert.AreEqual(input.ExtraChatChannel, output.ExtraChatChannel);
|
||||
|
||||
// Check chunks.
|
||||
AssertChunksEqual(input.Sender, output.Sender);
|
||||
AssertChunksEqual(input.Content, output.Content);
|
||||
}
|
||||
|
||||
private static void AssertChunksEqual(IReadOnlyList<Chunk> inputChunks, IReadOnlyList<Chunk> outputChunks) {
|
||||
Assert.AreEqual(inputChunks.Count, outputChunks.Count);
|
||||
for (var i = 0; i < inputChunks.Count; i++) {
|
||||
var inputChunk = inputChunks[i];
|
||||
var outputChunk = outputChunks[i];
|
||||
Assert.AreEqual(inputChunk.Source, outputChunk.Source);
|
||||
switch (inputChunk.Link) {
|
||||
case AchievementPayload inputAchievementPayload:
|
||||
Assert.AreEqual(inputAchievementPayload.Id, ((AchievementPayload) outputChunk.Link)!.Id);
|
||||
break;
|
||||
case Chat2PartyFinderPayload inputPartyFinderPayload:
|
||||
Assert.AreEqual(inputPartyFinderPayload.Id, ((Chat2PartyFinderPayload) outputChunk.Link)!.Id);
|
||||
break;
|
||||
case UriPayload inputUriPayload:
|
||||
Assert.AreEqual(inputUriPayload.Uri, ((UriPayload) outputChunk.Link)!.Uri);
|
||||
break;
|
||||
case null:
|
||||
Assert.IsTrue(outputChunk.Link == null);
|
||||
break;
|
||||
default:
|
||||
Assert.AreEqual($"{inputChunk.Link.Encode():X}", $"{outputChunk.Link!.Encode():X}");
|
||||
break;
|
||||
}
|
||||
|
||||
switch (inputChunk) {
|
||||
case TextChunk inputTextChunk:
|
||||
var outputTextChunk = (TextChunk)outputChunk;
|
||||
Assert.AreEqual(inputTextChunk.FallbackColour, outputTextChunk.FallbackColour);
|
||||
Assert.AreEqual(inputTextChunk.Foreground, outputTextChunk.Foreground);
|
||||
Assert.AreEqual(inputTextChunk.Glow, outputTextChunk.Glow);
|
||||
Assert.AreEqual(inputTextChunk.Italic, outputTextChunk.Italic);
|
||||
Assert.AreEqual(inputTextChunk.Content, outputTextChunk.Content);
|
||||
break;
|
||||
case IconChunk inputIconChunk:
|
||||
Assert.AreEqual(inputIconChunk.Icon, ((IconChunk) outputChunk).Icon);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown chunk type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertGuidsEqual(IReadOnlyList<Guid> expected, IReadOnlyList<Guid> got) {
|
||||
Assert.AreEqual(expected.Count, got.Count);
|
||||
for (var i = 0; i < expected.Count; i++) {
|
||||
Assert.AreEqual(expected[i].ToString(), got[i].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,370 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.GameFunctions.Types;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
namespace ChatTwo;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs.
|
||||
//
|
||||
// Spawns a session-only tab per /tell partner so a club greeter can track
|
||||
// multiple parallel conversations without losing context. Subscribes to
|
||||
// MessageManager.MessageProcessed for live tells and to ClientState.Logout
|
||||
// for the cleanup pass; everything else hangs off these two entry points.
|
||||
//
|
||||
// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault).
|
||||
internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
private readonly Plugin _plugin;
|
||||
private readonly MessageManager _messageManager;
|
||||
private readonly MessageStore _store;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
||||
{
|
||||
_plugin = plugin;
|
||||
_messageManager = messageManager;
|
||||
_store = store;
|
||||
}
|
||||
|
||||
internal int ActiveTempTabCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
return Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_messageManager.MessageProcessed += HandleTell;
|
||||
Plugin.ClientState.Logout += OnLogout;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin.ClientState.Logout -= OnLogout;
|
||||
_messageManager.MessageProcessed -= HandleTell;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
internal void HandleTell(Message message)
|
||||
{
|
||||
if (!Plugin.Config.EnableAutoTellTabs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Code.Type != ChatType.TellIncoming && message.Code.Type != ChatType.TellOutgoing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var partner = ExtractTellPartner(message);
|
||||
if (partner == null)
|
||||
{
|
||||
// Real message without a player payload — e.g. GM tells, which
|
||||
// we deliberately skip. The diagnostics make future regressions
|
||||
// (FFXIV changing tell payload shape, new edge cases) findable
|
||||
// without having to crank up debug logging at the source.
|
||||
Plugin.Log.Warning(
|
||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " +
|
||||
$"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " +
|
||||
$"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " +
|
||||
$"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
if (existing != null)
|
||||
{
|
||||
// Tab already exists; Tab.Matches has already routed this
|
||||
// message via the MessageManager pipeline (see Task 2 sender
|
||||
// filter).
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
}
|
||||
|
||||
SpawnTempTab(partner.Value, message);
|
||||
}
|
||||
}
|
||||
|
||||
private (string Name, uint World)? ExtractTellPartner(Message message)
|
||||
{
|
||||
if (message.Code.Type == ChatType.TellIncoming)
|
||||
{
|
||||
// Incoming tell: the sender is the conversation partner. The
|
||||
// PlayerPayload normally rides on a chunk's Link slot, but for
|
||||
// some tell types FFXIV only puts it in the raw SeString —
|
||||
// fall back to that before giving up.
|
||||
var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
if (fromSender != null)
|
||||
{
|
||||
return (fromSender.PlayerName, fromSender.World.RowId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Outgoing tell: the local player is the sender, the partner shows
|
||||
// up either as a payload in the content (for tells typed via the
|
||||
// Chat 2 input bar) or as the channel's tracked tell target (set by
|
||||
// the SetContextTellTarget game hook). Same SeString fallback.
|
||||
var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
if (fromContent != null)
|
||||
{
|
||||
return (fromContent.PlayerName, fromContent.World.RowId);
|
||||
}
|
||||
|
||||
var current = _plugin.CurrentTab.CurrentChannel.TellTarget
|
||||
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
|
||||
if (current != null && current.IsSet())
|
||||
{
|
||||
return (current.Name, current.World);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Tab? FindTempTab(string name, uint world)
|
||||
{
|
||||
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||
t.IsTempTab
|
||||
&& t.TellTarget != null
|
||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||
&& t.TellTarget.World == world);
|
||||
}
|
||||
|
||||
private void DropOldestTempTab()
|
||||
{
|
||||
// Greeted tabs are dropped before un-greeted ones (the user said
|
||||
// "I'm done with that conversation"), and within each bucket we
|
||||
// pick the oldest LastActivity. This protects active conversations
|
||||
// and unfinished greetings while still freeing up a slot.
|
||||
var victim = Plugin.Config.Tabs
|
||||
.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => t.Tab.IsTempTab)
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (victim.Tab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||
|
||||
// Re-anchor the active tab so the user does not silently end up on
|
||||
// a different conversation when their tab gets dropped or shifted.
|
||||
if (victim.Index <= _plugin.LastTab)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnTempTab((string Name, uint World) partner, Message currentMessage)
|
||||
{
|
||||
var tab = BuildTempTab(partner.Name, partner.World);
|
||||
|
||||
// Preload first so the tab opens with chronological history above
|
||||
// the current message — and so a slow DB query never causes a
|
||||
// visible "empty tab, then history pops in" effect on screen.
|
||||
// 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);
|
||||
Plugin.Config.Tabs.Add(tab);
|
||||
}
|
||||
|
||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||
{
|
||||
return new Tab
|
||||
{
|
||||
Name = FormatTabName(playerName, worldRowId),
|
||||
IsTempTab = true,
|
||||
AllSenderMessages = true,
|
||||
TellTarget = new TellTarget(playerName, worldRowId, 0, TellReason.Direct),
|
||||
Channel = InputChannel.Tell,
|
||||
DisplayTimestamp = true,
|
||||
UnreadMode = UnreadMode.Unseen,
|
||||
HideWhenInactive = false,
|
||||
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
|
||||
{
|
||||
[ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||
[ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatTabName(string playerName, uint worldRowId)
|
||||
{
|
||||
if (Sheets.WorldSheet.TryGetRow(worldRowId, out var worldRow))
|
||||
{
|
||||
return $"{playerName}@{worldRow.Name}";
|
||||
}
|
||||
// World sheet lookup miss is rare (only for FFXIV worlds Dalamud has
|
||||
// not yet seen). Fall back to the raw RowId so the user still has a
|
||||
// unique, readable label.
|
||||
return $"{playerName}@World{worldRowId}";
|
||||
}
|
||||
|
||||
private void PreloadHistory(Tab tab, string senderName, uint senderWorld, Guid currentMessageId)
|
||||
{
|
||||
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
|
||||
if (preloadCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Pull one extra row because the live tell that triggered this
|
||||
// spawn is already in the store and would otherwise eat one of
|
||||
// the user's preload-budget slots.
|
||||
var history = _store.GetTellHistoryWithSender(
|
||||
_messageManager.CurrentContentId,
|
||||
senderName,
|
||||
senderWorld,
|
||||
preloadCount + 1);
|
||||
|
||||
var historicMessages = history
|
||||
.Where(m => m.Id != currentMessageId)
|
||||
.Take(preloadCount)
|
||||
.ToList();
|
||||
|
||||
if (historicMessages.Count == 0)
|
||||
{
|
||||
// No prior tells with this player — leave the tab to start
|
||||
// empty so the user does not see a "history loaded" marker
|
||||
// sitting alone above the very first message.
|
||||
return;
|
||||
}
|
||||
|
||||
// The history list is already oldest-first, so a plain AddPrune
|
||||
// loop produces the chronological order the user expects to see
|
||||
// when the tab opens.
|
||||
foreach (var message in historicMessages)
|
||||
{
|
||||
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||||
}
|
||||
|
||||
// Visible separator between the loaded history and the live
|
||||
// tell that triggered this spawn. Goes in last so it sorts
|
||||
// after the historical messages but before the current one.
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
|
||||
MessageManager.MessageDisplayLimit);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-fatal: the tab still spawns, but the user gets a visible
|
||||
// notice instead of silently missing history. The error logs
|
||||
// once with full stack trace for diagnosis.
|
||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||
MessageManager.MessageDisplayLimit);
|
||||
}
|
||||
}
|
||||
|
||||
private static Message MakeSystemMarker(string text)
|
||||
{
|
||||
var seString = new SeStringBuilder().AddText(text).Build();
|
||||
var chunks = ChunkUtil.ToChunks(seString, ChunkSource.Content, ChatType.System).ToList();
|
||||
var code = new ChatCode((XivChatType)ChatType.System, 0, 0);
|
||||
return Message.FakeMessage(chunks, code);
|
||||
}
|
||||
|
||||
internal void MarkGreeted(Tab tab)
|
||||
{
|
||||
SetGreeted(tab, true);
|
||||
}
|
||||
|
||||
internal void UnmarkGreeted(Tab tab)
|
||||
{
|
||||
SetGreeted(tab, false);
|
||||
}
|
||||
|
||||
internal bool IsGreeted(Tab tab)
|
||||
{
|
||||
return tab.IsGreeted;
|
||||
}
|
||||
|
||||
private void SetGreeted(Tab tab, bool greeted)
|
||||
{
|
||||
if (tab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Frame-race guard (E5): the sidebar might still render a tab
|
||||
// that has already been removed by LRU drop or logout cleanup.
|
||||
// Silently skip the toggle so we don't mutate stale state.
|
||||
if (!Plugin.Config.Tabs.Contains(tab))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tab.IsGreeted = greeted;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Snapshot whether the active tab is about to be removed, BEFORE
|
||||
// we mutate the list — index lookups would lie to us afterwards.
|
||||
var lastIndex = _plugin.LastTab;
|
||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
||||
|
||||
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
// Force a switch to tab 0 if the active tab was a temp tab OR
|
||||
// if drops before the active index pushed LastTab out of range.
|
||||
// Otherwise the user keeps their current persistent tab.
|
||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
if (currentWasTempTab || !stillValid)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable → Regular
+13
-26
@@ -4,7 +4,7 @@
|
||||
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
|
||||
derives from. -->
|
||||
<Version>0.1.0</Version>
|
||||
<Version>0.5.4</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
||||
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
||||
@@ -17,11 +17,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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="Pidgin" Version="3.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="Watson.Lite" Version="6.3.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -58,30 +57,18 @@
|
||||
</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>
|
||||
<Folder Include="images\" />
|
||||
<None Include="images\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<!--This doesn't work until Plogon is updated to include NodeJS-->
|
||||
<!-- <Target Name="NodeJS Compile" BeforeTargets="BeforeCompile">-->
|
||||
<!-- <Exec Command="npm install" WorkingDirectory="Http\Frontend"/>-->
|
||||
<!-- <Exec Command="npm run build" WorkingDirectory="Http\Frontend"/>-->
|
||||
<!-- </Target>-->
|
||||
<!-- -->
|
||||
<!-- <Target Name="CopyFiles" AfterTargets="Build">-->
|
||||
<!-- <ItemGroup>-->
|
||||
<!-- <Files Include="$(MSBuildThisFileDirectory)\Http\Frontend\build\**" />-->
|
||||
<!-- </ItemGroup>-->
|
||||
<!-- -->
|
||||
<!-- <Copy SourceFiles="@(Files)" DestinationFolder="$(TargetDir)\Frontend\%(RecursiveDir)" />-->
|
||||
<!-- </Target>-->
|
||||
|
||||
<!-- <Target Name="NodeJS Compile" BeforeTargets="BeforeCompile" Condition="'$(Configuration)' == 'Debug'">-->
|
||||
<!-- <Exec Command="npm install" WorkingDirectory="Http\Frontend"/>-->
|
||||
<!-- <Exec Command="npm run build" WorkingDirectory="Http\Frontend"/>-->
|
||||
<!-- </Target>-->
|
||||
|
||||
<Target Name="UnzipBuild" AfterTargets="Build">
|
||||
<Unzip SourceFiles="websiteBuild.zip" DestinationFolder="$(TargetDir)\Frontend"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
+75
-22
@@ -6,6 +6,7 @@ using ChatTwo.Util;
|
||||
using Dalamud;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
@@ -33,7 +34,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 7;
|
||||
private const int LatestVersion = 10;
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
@@ -72,8 +73,10 @@ public class Configuration : IPluginConfiguration
|
||||
public bool HellionThemeEnabled = true;
|
||||
|
||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||||
// panes more glass-like so the game shines through. Default ~92%.
|
||||
public float HellionThemeWindowOpacity = 0.92f;
|
||||
// panes more glass-like so the game shines through. Default 0.5
|
||||
// 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
|
||||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
||||
@@ -81,6 +84,25 @@ public class Configuration : IPluginConfiguration
|
||||
// to fall back to the user's chosen system or Dalamud font.
|
||||
public bool UseHellionFont = true;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
|
||||
// /tell spawns a session-only tab dedicated to that conversation
|
||||
// partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
|
||||
public bool EnableAutoTellTabs = true;
|
||||
// Hard cap on simultaneously open auto tell tabs. Range enforced by the
|
||||
// settings slider (1–50). LRU drop favors greeted tabs first.
|
||||
public int AutoTellTabsLimit = 15;
|
||||
// When true the sidebar shows only a thin separator before the temp
|
||||
// tabs; when false a section header "Active Tells (n)" is rendered.
|
||||
public bool AutoTellTabsCompactDisplay;
|
||||
// Number of prior tells to preload from the message store when an
|
||||
// auto tell tab is spawned. Range 0–100; 0 disables preload.
|
||||
public int AutoTellTabsHistoryPreload = 20;
|
||||
// Show the greeter "marked-as-greeted" toggle button next to each
|
||||
// temp tab and dim the tab name when set. Off by default because the
|
||||
// workflow is specific to club-greeter use cases — most users just
|
||||
// want the auto tabs themselves without the extra UI affordance.
|
||||
public bool AutoTellTabsShowGreetedToggle;
|
||||
|
||||
public int GetRetentionDays(ChatType type)
|
||||
{
|
||||
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
|
||||
@@ -112,7 +134,12 @@ public class Configuration : IPluginConfiguration
|
||||
public bool MoreCompactPretty;
|
||||
public bool HideSameTimestamps;
|
||||
public bool ShowNoviceNetwork;
|
||||
public bool SidebarTabView;
|
||||
// Hellion Chat — vertical sidebar tab layout reads better than the
|
||||
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
|
||||
// greeter typically tracks 5–15 simultaneous conversations). Bestand
|
||||
// users keep their saved value untouched — only fresh installs pick
|
||||
// up the new default.
|
||||
public bool SidebarTabView = true;
|
||||
public bool PrintChangelog = true;
|
||||
public bool OnlyPreviewIf;
|
||||
public int PreviewMinimum = 1;
|
||||
@@ -122,7 +149,7 @@ public class Configuration : IPluginConfiguration
|
||||
public LanguageOverride LanguageOverride = LanguageOverride.None;
|
||||
public bool CanMove = true;
|
||||
public bool CanResize = true;
|
||||
public bool ShowTitleBar;
|
||||
public bool ShowTitleBar = true;
|
||||
public bool ShowPopOutTitleBar = true;
|
||||
public bool DatabaseBattleMessages;
|
||||
public bool LoadPreviousSession;
|
||||
@@ -132,8 +159,12 @@ public class Configuration : IPluginConfiguration
|
||||
public bool CollapseKeepUniqueLinks;
|
||||
public bool PlaySounds = true;
|
||||
public bool KeepInputFocus = true;
|
||||
public int MaxLinesToRender = 10_000; // 1-10000
|
||||
public bool Use24HourClock;
|
||||
public int MaxLinesToRender = 5_000; // 1-10000
|
||||
// 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 HashSet<string> BlockedEmotes = [];
|
||||
@@ -171,14 +202,6 @@ public class Configuration : IPluginConfiguration
|
||||
public ConfigKeyBind? ChatTabForward;
|
||||
public ConfigKeyBind? ChatTabBackward;
|
||||
|
||||
// Webinterface
|
||||
public bool WebinterfaceEnabled;
|
||||
public bool WebinterfaceAutoStart;
|
||||
public string WebinterfacePassword = WebinterfaceUtil.GenerateSimpleAuthCode();
|
||||
public int WebinterfacePort = 9000;
|
||||
public HashSet<string> AuthStore = [];
|
||||
public int WebinterfaceMaxLinesToSend = 1000; // 1-10000
|
||||
|
||||
public void UpdateFrom(Configuration other, bool backToOriginal)
|
||||
{
|
||||
if (backToOriginal)
|
||||
@@ -238,16 +261,21 @@ public class Configuration : IPluginConfiguration
|
||||
TooltipOffset = other.TooltipOffset;
|
||||
WindowAlpha = other.WindowAlpha;
|
||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||
Tabs = other.Tabs.Select(t => t.Clone()).ToList();
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
||||
// never present in a disk-loaded copy. Keep the live temp tabs of
|
||||
// *this* configuration alive across an UpdateFrom so a settings
|
||||
// save (or sidebar-mode toggle) does not silently destroy the
|
||||
// user's open tell conversations. Persistent tabs from `other`
|
||||
// still get the regular clone-replace treatment.
|
||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||||
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList();
|
||||
Tabs.AddRange(liveTempTabs);
|
||||
|
||||
OverrideStyle = other.OverrideStyle;
|
||||
ChosenStyle = other.ChosenStyle;
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
WebinterfaceEnabled = other.WebinterfaceEnabled;
|
||||
WebinterfaceAutoStart = other.WebinterfaceAutoStart;
|
||||
WebinterfacePassword = other.WebinterfacePassword;
|
||||
WebinterfacePort = other.WebinterfacePort;
|
||||
WebinterfaceMaxLinesToSend = other.WebinterfaceMaxLinesToSend;
|
||||
|
||||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
||||
PrivacyPersistChannels = [..other.PrivacyPersistChannels];
|
||||
@@ -262,6 +290,12 @@ public class Configuration : IPluginConfiguration
|
||||
HellionThemeEnabled = other.HellionThemeEnabled;
|
||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
|
||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,9 +371,27 @@ public class Tab
|
||||
|
||||
[NonSerialized] public Guid Identifier = Guid.NewGuid();
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
|
||||
// sidebar to mark a tell partner as already greeted in the current
|
||||
// session. NonSerialized because the temp tab itself is session-only.
|
||||
[NonSerialized] public bool IsGreeted;
|
||||
|
||||
public bool Matches(Message message)
|
||||
{
|
||||
return message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels);
|
||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auto-tell temp tabs are bound to a single conversation partner;
|
||||
// every other tell that matches the channel filter must NOT land
|
||||
// here, otherwise all temp tabs would mirror "Tell Exclusive".
|
||||
if (IsTempTab && TellTarget?.IsSet() == true)
|
||||
{
|
||||
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddMessage(Message message, bool unread = true)
|
||||
@@ -388,6 +440,7 @@ public class Tab
|
||||
IsTempTab = IsTempTab,
|
||||
AllSenderMessages = AllSenderMessages,
|
||||
TellTarget = TellTarget.From(TellTarget),
|
||||
IsGreeted = IsGreeted,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+22
-8
@@ -32,23 +32,23 @@ public static class EmoteCache
|
||||
private struct Top100()
|
||||
{
|
||||
[JsonPropertyName("emote")]
|
||||
public Emote Emote = default;
|
||||
public Emote Emote { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id = string.Empty;
|
||||
public required string Id { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct Emote()
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id = string.Empty;
|
||||
public required string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public string Code = string.Empty;
|
||||
public required string Code { get; set; }
|
||||
|
||||
[JsonPropertyName("imageType")]
|
||||
public string ImageType = string.Empty;
|
||||
public required string ImageType { get; set; }
|
||||
}
|
||||
|
||||
public enum LoadingState
|
||||
@@ -66,7 +66,7 @@ public static class EmoteCache
|
||||
|
||||
public static string[] SortedCodeArray = [];
|
||||
|
||||
public static async void LoadData()
|
||||
public static async Task LoadData()
|
||||
{
|
||||
if (State is not LoadingState.Unloaded)
|
||||
return;
|
||||
@@ -105,6 +105,11 @@ public static class EmoteCache
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
||||
// the Emotes tab after the network recovers) can retry. Without
|
||||
// this the State stays on Loading and the early-out at the top
|
||||
// of LoadData blocks every further attempt until plugin reload.
|
||||
State = LoadingState.Unloaded;
|
||||
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
|
||||
}
|
||||
}
|
||||
@@ -168,10 +173,19 @@ public static class EmoteCache
|
||||
|
||||
internal async Task<byte[]> LoadAsync(Emote emote)
|
||||
{
|
||||
var dir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1");
|
||||
// BetterTTV-supplied Id and ImageType are interpolated straight
|
||||
// into the filename. HTTPS protects the wire, but a compromised
|
||||
// upstream could still hand us "../foo" and write into the
|
||||
// pluginConfigs root (or worse). Resolve the candidate path and
|
||||
// refuse anything that escapes the cache directory.
|
||||
var dir = Path.GetFullPath(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"));
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var filePath = Path.Join(dir, $"{emote.Id}.{emote.ImageType}");
|
||||
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar) ? dir : dir + Path.DirectorySeparatorChar;
|
||||
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
|
||||
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}");
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
RawData = await File.ReadAllBytesAsync(filePath);
|
||||
|
||||
@@ -156,9 +156,6 @@ internal sealed unsafe class Chat : IDisposable
|
||||
return;
|
||||
|
||||
ChangeChannelNameDetour(agent);
|
||||
|
||||
// Inform all clients that a new login happened
|
||||
Plugin.ServerCore.SendNewLogin();
|
||||
}
|
||||
|
||||
private byte ChatLogRefreshDetour(nint log, ushort eventId, AtkValue* value)
|
||||
|
||||
+79
-30
@@ -1,20 +1,42 @@
|
||||
name: Hellion Chat
|
||||
author: JonKazama-Hellion
|
||||
punchline: GDPR-compliant, Linux-aware fork of Chat 2
|
||||
punchline: Chat 2 with privacy controls aligned to EU, US and JP rules
|
||||
description: |-
|
||||
Hellion Chat is a privacy-focused, Linux-aware fork of Chat 2.
|
||||
Hellion Chat is built on top of Chat 2 with one removal and a stack
|
||||
of privacy controls on top. Tabs, channel filters, RGB colours,
|
||||
emotes, screenshot mode, IPC integration and the chat replacement
|
||||
window itself work the same. The optional webinterface that Chat 2
|
||||
ships is intentionally not part of this fork because it serves a
|
||||
different use case from the smaller default footprint Hellion Chat
|
||||
is built around.
|
||||
|
||||
Same chat replacement you know from upstream, with extra controls
|
||||
for what actually gets stored:
|
||||
On top of that, Hellion Chat adds privacy and data-handling controls
|
||||
designed to align with the modern data protection rules that apply
|
||||
across the EU, the United States and Japan. By default only your own
|
||||
conversations are stored; messages from strangers, NPCs and system
|
||||
spam stay out of the database. Retention windows are configurable per
|
||||
channel, history can be wiped retroactively, and stored data can be
|
||||
exported on demand.
|
||||
|
||||
- Channel whitelist for database persistence (GDPR Art. 25)
|
||||
- Privacy-First defaults: only your own conversations are kept
|
||||
- Failsafe for unknown ChatTypes (default OFF)
|
||||
- Independent plugin state (own config + database directory)
|
||||
Key additions on top of Chat 2:
|
||||
|
||||
- Channel whitelist with a Privacy-First default
|
||||
- Per-channel retention with a daily background sweep
|
||||
- Retroactive cleanup with a Ctrl+Shift confirm
|
||||
- Export to Markdown, JSON or CSV
|
||||
- First-run wizard with three preset profiles (Privacy-First, Casual,
|
||||
Full History)
|
||||
- Bilingual UI (English and German) with live language switching
|
||||
- Independent plugin state — own config file and database directory,
|
||||
so Hellion Chat does not share state with the upstream plugin
|
||||
|
||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
||||
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:
|
||||
- Social
|
||||
- UI
|
||||
@@ -22,30 +44,57 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**Hellion Chat 0.1.0 — Initial fork release**
|
||||
**Hellion Chat 0.5.4 — WrapText hardening**
|
||||
|
||||
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
|
||||
(default OFF; spec defaults of 365 / 90 / 30 days)
|
||||
- Retroactive cleanup with a Ctrl+Shift confirm and VACUUM
|
||||
- Export to Markdown / JSON / CSV via Dalamud's file dialog
|
||||
(GDPR Art. 15 right of access)
|
||||
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
||||
Span- and index-based control flow. Closes the persistent CodeQL
|
||||
Critical alert "unvalidated local pointer arithmetic" that kept
|
||||
re-firing on every shape of the previous fix.
|
||||
|
||||
Onboarding
|
||||
- First-run wizard with three profiles: Privacy-First / Casual /
|
||||
Full History
|
||||
- Configuration v6 → v7 migration that seeds defaults and shows a
|
||||
notification once on update
|
||||
- One-shot migration from upstream Chat 2's pluginConfigs layout
|
||||
so the fork uses pluginConfigs/HellionChat without losing state
|
||||
- Migrate3 idempotency recovery for half-migrated databases
|
||||
Hardening:
|
||||
|
||||
Look & feel
|
||||
- Localized UI (English and German) with live language switching
|
||||
- Hellion industrial HUD theme with cyan-teal action accents,
|
||||
slate-violet tabs, amber active highlights and a window-opacity
|
||||
slider for combat-friendly transparency
|
||||
- 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
|
||||
|
||||
No user-visible behaviour change. Word-wrap output is byte-identical
|
||||
to v0.5.3.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
||||
|
||||
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
||||
encoded byte buffer length via GetByteCount before pointer
|
||||
arithmetic. Single-fix patch on top of v0.5.2.
|
||||
|
||||
**Hellion Chat 0.5.2 — Bugfix patch**
|
||||
|
||||
Auto-Tell-Tabs history-separator landed below the live tell instead
|
||||
of above (preload now excludes the trigger message). Plugin icon
|
||||
packaging fixed by removing a stale DalamudPackager.targets override
|
||||
that conflicted with the SDK 15 default. Default config aligned to
|
||||
the maintainer's daily driver: HellionThemeWindowOpacity 0.5,
|
||||
Use24HourClock true, Gruppe tab no longer auto-routes /party. Two
|
||||
earlier CodeQL findings closed (workflow permissions, empty-input
|
||||
pointer arithmetic).
|
||||
|
||||
**Hellion Chat 0.5.1 — Backlog Sweep**
|
||||
|
||||
Pure hardening and polish. Eight backlog items from the v0.5.0
|
||||
codebase review collected into one patch: cleanup-preview-stale
|
||||
detection, greeted-tab dim background, Performance HelpMarker
|
||||
consistency, Tabs/Database tab names from HellionStrings,
|
||||
FontChooser framework-thread marshalling, async-void on
|
||||
EmoteCache.LoadData, parameterised SQL via BindIntList helper.
|
||||
|
||||
---
|
||||
|
||||
Earlier history at https://github.com/JonKazama-Hellion/HellionChat/releases.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
@@ -1 +0,0 @@
|
||||
engine-strict=true
|
||||
@@ -1,38 +0,0 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
Generated
-1573
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"svelte": "^5.39.2",
|
||||
"svelte-check": "^4.0.0",
|
||||
"sveltekit-sse": "^0.14.3",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltestrap/sveltestrap": "^7.1.0"
|
||||
}
|
||||
}
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Error {
|
||||
code: string;
|
||||
id: string;
|
||||
}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
||||
interface Warning {
|
||||
hasWarning: boolean;
|
||||
content: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Element { scrollTopMax: number } // Firefox only property
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {isChannelLocked, channelOptions} from "$lib/shared.svelte";
|
||||
|
||||
let selectElement: HTMLSelectElement;
|
||||
|
||||
async function requestChannelSwitch(event: Event) {
|
||||
if (!event.currentTarget)
|
||||
return;
|
||||
|
||||
let element = (event.currentTarget as HTMLSelectElement);
|
||||
let requestedChannel = element.value;
|
||||
|
||||
console.log(element.value)
|
||||
element.value = '0';
|
||||
|
||||
const rawResponse = await fetch('/channel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ channel: requestedChannel })
|
||||
});
|
||||
// const content = await rawResponse.json();
|
||||
// TODO: use the response
|
||||
}
|
||||
|
||||
let canvas: HTMLCanvasElement | null = null;
|
||||
function getTextWidth(text: string): number {
|
||||
// re-use canvas object for better performance
|
||||
if (canvas === null)
|
||||
canvas = document.createElement("canvas");
|
||||
|
||||
const context: CanvasRenderingContext2D | null = canvas.getContext("2d");
|
||||
if (!context)
|
||||
return 0;
|
||||
|
||||
context.font = getCanvasFont(selectElement);
|
||||
const metrics = context.measureText(text);
|
||||
return metrics.width;
|
||||
}
|
||||
|
||||
function getCssStyle(element: Element, prop: string): string {
|
||||
return window.getComputedStyle(element, null).getPropertyValue(prop);
|
||||
}
|
||||
|
||||
function getCanvasFont(el = document.body) {
|
||||
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
|
||||
const fontSize = getCssStyle(el, 'font-size') || '16px';
|
||||
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
|
||||
|
||||
return `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:this={selectElement}
|
||||
id="channel-select"
|
||||
style="pointer-events: {isChannelLocked.locked ? 'none' : 'inherit'}; width: {(channelOptions.length > 1 ? getTextWidth(channelOptions[0].text) : 1) + 40}px"
|
||||
onchange={(e) => requestChannelSwitch(e)}>
|
||||
{#each channelOptions as channelOption}
|
||||
{#if channelOption.preview }
|
||||
<option selected disabled hidden value={channelOption.value}>
|
||||
{channelOption.text}
|
||||
</option>
|
||||
{:else}
|
||||
<option value={channelOption.value}>
|
||||
{channelOption.text}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<style>
|
||||
select {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { subscribe } from "$lib/utils.svelte";
|
||||
import { chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
||||
|
||||
let textarea: HTMLTextAreaElement;
|
||||
|
||||
let skipNextCheck: boolean = $state(false);
|
||||
let requiresResize: boolean = $state(true);
|
||||
|
||||
subscribe(
|
||||
() => chatInput,
|
||||
(v) => {
|
||||
if (skipNextCheck) {
|
||||
skipNextCheck = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Input box has been reset to empty, so resize it back to smaller box
|
||||
if (v.content === '') {
|
||||
console.log("Empty chatbox, resize");
|
||||
requiresResize = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove newline characters
|
||||
let original = v.content;
|
||||
v.content = v.content.replace(/(\r\n|\n|\r)/gm,"");
|
||||
|
||||
console.log(`${original.length} vs ${v.content.length}`);
|
||||
let hasChanged = original.length != v.content.length;
|
||||
if (hasChanged) {
|
||||
skipNextCheck = true;
|
||||
requiresResize = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function preventNewlines(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
// Prevent key from creating a newline
|
||||
e.preventDefault();
|
||||
|
||||
// submit the data
|
||||
const newEvent = new Event('submit', {bubbles: true, cancelable: true});
|
||||
if (e.currentTarget !== null) {
|
||||
(e.currentTarget as HTMLTextAreaElement).closest('form')?.dispatchEvent(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (!textarea)
|
||||
return;
|
||||
|
||||
const scrolledToBottom = messagesList.scrolledToBottom;
|
||||
textarea.style.height = '1px';
|
||||
textarea.style.height = `${textarea.scrollHeight + 10}px`; // with +10px extra padding
|
||||
if (scrolledToBottom)
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
console.log(`Checking effect: ${requiresResize}`)
|
||||
if (requiresResize) {
|
||||
requiresResize = false;
|
||||
resize();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
bind:value={chatInput.content}
|
||||
oninput={() => resize()}
|
||||
onkeydown={(e) => preventNewlines(e)}
|
||||
|
||||
id="chat-input"
|
||||
autocomplete="off"
|
||||
placeholder="Message"
|
||||
enterkeyhint="send"
|
||||
maxlength="500">
|
||||
</textarea>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
flex-grow: 0;
|
||||
|
||||
font-size: 1rem;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 20px;
|
||||
background-color: var(--bg-input);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
|
||||
min-height: 2.5em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { selectedTab, knownTabs, tabPaneState, tabPaneAnimationState, closeTabPane, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
||||
|
||||
async function selectTab(index: number) {
|
||||
const rawResponse = await fetch('/tab', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ index })
|
||||
});
|
||||
// const content = await rawResponse.json();
|
||||
// TODO: use the response
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
tabPaneAnimationState.noAnimation = false;
|
||||
closeTabPane();
|
||||
}
|
||||
|
||||
let scrolledToBottom = true;
|
||||
function ontransitionstart() {
|
||||
scrolledToBottom = messagesList.scrolledToBottom;
|
||||
}
|
||||
|
||||
function ontransitionend() {
|
||||
if (scrolledToBottom) {
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
id="tabs"
|
||||
class:no-animation={tabPaneAnimationState.noAnimation}
|
||||
class:hidden={!tabPaneState.visible}
|
||||
{ontransitionstart}
|
||||
{ontransitionend}
|
||||
>
|
||||
<div class="inner">
|
||||
<header>
|
||||
<span>Tabs</span>
|
||||
<button type="button" onclick={() => handleClose()}>
|
||||
<!-- "chevron-left" icon from https://github.com/feathericons/feather, under MIT license -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<hr>
|
||||
|
||||
<ol id="tabs-list">
|
||||
{#each knownTabs as tab}
|
||||
<li class:active={selectedTab.index === tab.index}>
|
||||
<button type="button" onclick={() => selectTab(tab.index)}>
|
||||
{ tab.name } {tab.unreadCount > 0 ? `(${tab.unreadCount})`: '' }
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {tabPaneState, tabPaneAnimationState, openTabPane, knownTabs} from "$lib/shared.svelte";
|
||||
|
||||
function onclick() {
|
||||
tabPaneAnimationState.noAnimation = false;
|
||||
openTabPane();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button type="button" aria-label="Open tab pane" class:visible={!tabPaneState.visible} class:unread={knownTabs.some((tab) => tab.unreadCount > 0)} {onclick} disabled={tabPaneState.visible}>
|
||||
<!-- "chevron-right" icon from https://github.com/feathericons/feather, under MIT license -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
padding: 25px 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
|
||||
button.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button.unread svg {
|
||||
stroke: var(--unread-color);
|
||||
filter: drop-shadow(0 0 2px var(--unread-color));
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>%sveltekit.error.message%</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: %sveltekit.status%</p>
|
||||
<p>Message: %sveltekit.error.message%</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,405 +0,0 @@
|
||||
import { channelOptions, isChannelLocked, selectedTab, knownTabs, chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
||||
import { WebPayloadType } from "$lib/payload";
|
||||
import { source, type Source } from "sveltekit-sse";
|
||||
|
||||
interface ChatElements {
|
||||
messagesContainer: Element | null,
|
||||
messagesList: HTMLElement | null,
|
||||
|
||||
timestampWidthProbe: HTMLElement | null,
|
||||
|
||||
inputForm: Element | null,
|
||||
}
|
||||
|
||||
// ref `DataStructure.Messages`
|
||||
interface Messages {
|
||||
messages: MessageResponse[]
|
||||
}
|
||||
|
||||
// ref `DataStructure.MessageResponse`
|
||||
interface MessageResponse {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
templates: Template[];
|
||||
}
|
||||
|
||||
// ref `DataStructure.MessageTemplate`
|
||||
interface Template {
|
||||
payloadType: WebPayloadType;
|
||||
content: string;
|
||||
iconId: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
// ref `DataStructure.SwitchChannel`
|
||||
interface SwitchChannel {
|
||||
channelName: Template[];
|
||||
channelValue: number;
|
||||
channelLocked: boolean;
|
||||
}
|
||||
|
||||
// ref `DataStructure.ChannelList`
|
||||
interface ChannelList {
|
||||
channels: {[key: string]: number};
|
||||
}
|
||||
|
||||
// ref `DataStructure.ChatTab`
|
||||
export interface ChatTab {
|
||||
name: string;
|
||||
index: number;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
// ref `DataStructure.ChatTabList`
|
||||
interface ChatTabList {
|
||||
tabs: ChatTab[];
|
||||
}
|
||||
|
||||
// ref `DataStructure.ChatTabUnreadState`
|
||||
interface ChatTabUnreadState {
|
||||
index: number;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export class ChatTwoWeb {
|
||||
elements!: ChatElements;
|
||||
maxTimestampWidth: number = 0;
|
||||
|
||||
sse!: EventSource;
|
||||
connection!: Source;
|
||||
|
||||
constructor() {
|
||||
this.setupDOMElements();
|
||||
this.setupSSEConnection();
|
||||
}
|
||||
|
||||
setupDOMElements() {
|
||||
this.elements = {
|
||||
messagesContainer: document.querySelector('#messages > .scroll-container')!,
|
||||
messagesList: document.getElementById('messages-list'),
|
||||
|
||||
timestampWidthProbe: document.getElementById('timestamp-width-probe'),
|
||||
|
||||
inputForm: document.querySelector('#input > form'),
|
||||
};
|
||||
messagesList.element = this.elements.messagesList;
|
||||
|
||||
// add indicator signaling more messages below
|
||||
this.elements.messagesContainer?.addEventListener('scroll', (event) => {
|
||||
if (event.currentTarget === null)
|
||||
return;
|
||||
|
||||
let parentElement = (event.currentTarget as HTMLDivElement).parentElement;
|
||||
if (!this.messagesAreScrolledToBottom()) {
|
||||
parentElement?.classList.add('more-messages');
|
||||
} else {
|
||||
parentElement?.classList.remove('more-messages');
|
||||
}
|
||||
});
|
||||
|
||||
// adjust scroll when the window size changes; mostly for mobile (opening/closing the keyboard)
|
||||
window.addEventListener('resize', () => {
|
||||
if (messagesList.scrolledToBottom) {
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
})
|
||||
|
||||
// handle message sending
|
||||
this.elements.inputForm?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
if (chatInput.content.length > 500) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawResponse = await fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ message: chatInput.content })
|
||||
});
|
||||
// const content = await rawResponse.json();
|
||||
// TODO: use the response
|
||||
|
||||
chatInput.content = '';
|
||||
});
|
||||
}
|
||||
|
||||
messagesAreScrolledToBottom() {
|
||||
if (this.elements.messagesContainer === null) {
|
||||
return messagesList.scrolledToBottom;
|
||||
}
|
||||
|
||||
messagesList.scrolledToBottom =
|
||||
(
|
||||
this.elements.messagesContainer.scrollHeight -
|
||||
this.elements.messagesContainer.clientHeight -
|
||||
this.elements.messagesContainer.scrollTop
|
||||
) < 1;
|
||||
|
||||
return messagesList.scrolledToBottom;
|
||||
}
|
||||
|
||||
updateChannelHint(channel: SwitchChannel) {
|
||||
// Set storage to the current lock state
|
||||
isChannelLocked.locked = channel.channelLocked;
|
||||
|
||||
const channelElement = this.processTemplate(channel.channelName);
|
||||
if (!channelElement.firstChild)
|
||||
return;
|
||||
|
||||
let channelName = (channelElement.firstChild as HTMLSpanElement).innerText;
|
||||
if (channel.channelLocked)
|
||||
channelName = `(Locked) ${channelName}`;
|
||||
|
||||
channelOptions[0] = {text: channelName, value: 0, preview: true }
|
||||
}
|
||||
|
||||
updateChannels(channelList: ChannelList) {
|
||||
channelOptions.length = 1;
|
||||
|
||||
for (const [ label, channel ] of Object.entries(channelList.channels)) {
|
||||
channelOptions.push( { text: label, value: channel, preview: false } )
|
||||
}
|
||||
}
|
||||
|
||||
// calculate timestamp width to ensure that all timestamps have the same width.
|
||||
// some typefaces have the same width across all number glyphs, others do not.
|
||||
// then there's AM/PM vs 24 hour, and so on
|
||||
calculateTimestampWidth(timestamp: string) {
|
||||
if (this.elements.timestampWidthProbe === null)
|
||||
return;
|
||||
|
||||
this.elements.timestampWidthProbe.innerText = timestamp;
|
||||
if (this.elements.timestampWidthProbe.clientWidth > this.maxTimestampWidth) {
|
||||
this.maxTimestampWidth = this.elements.timestampWidthProbe.clientWidth;
|
||||
document.body.style.setProperty('--timestamp-width', (Math.ceil(this.maxTimestampWidth) + 1) + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(messageData: MessageResponse) {
|
||||
if (this.elements.messagesList === null)
|
||||
return;
|
||||
|
||||
const scrolledToBottom = this.messagesAreScrolledToBottom();
|
||||
this.calculateTimestampWidth(messageData.timestamp);
|
||||
|
||||
const liMessage = document.createElement('li');
|
||||
const spanTimestamp = document.createElement('span');
|
||||
spanTimestamp.classList.add('timestamp');
|
||||
spanTimestamp.innerText = messageData.timestamp;
|
||||
|
||||
const spanMessage = document.createElement('span');
|
||||
spanMessage.classList.add('message');
|
||||
spanMessage.appendChild(this.processTemplate(messageData.templates))
|
||||
|
||||
liMessage.appendChild(spanTimestamp);
|
||||
liMessage.appendChild(spanMessage);
|
||||
this.elements.messagesList.appendChild(liMessage);
|
||||
|
||||
if (scrolledToBottom) {
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
processTemplate(templates: Template[]) {
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
for( const template of templates ) {
|
||||
const spanElement = document.createElement('span');
|
||||
switch (template.payloadType) {
|
||||
case WebPayloadType.RawText:
|
||||
this.processTextTemplate(template, spanElement);
|
||||
break;
|
||||
case WebPayloadType.CustomUri:
|
||||
this.processUrlTemplate(template, spanElement);
|
||||
break;
|
||||
case WebPayloadType.CustomEmote:
|
||||
this.processEmote(template, spanElement);
|
||||
break;
|
||||
case WebPayloadType.Icon:
|
||||
this.processIcon(template, spanElement);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
frag.appendChild(spanElement);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
processTextTemplate(template: Template, spanElement: HTMLSpanElement) {
|
||||
spanElement.innerText = template.content;
|
||||
if (template.color !== 0)
|
||||
{
|
||||
this.processColor(template, spanElement);
|
||||
}
|
||||
}
|
||||
|
||||
processUrlTemplate(template: Template, spanElement: HTMLSpanElement) {
|
||||
const urlElement = document.createElement('a');
|
||||
let url = template.content;
|
||||
if (!url.startsWith('https://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
urlElement.innerText = template.content;
|
||||
urlElement.href = encodeURI(url);
|
||||
urlElement.target = '_blank'
|
||||
|
||||
if (template.color !== 0)
|
||||
{
|
||||
this.processColor(template, spanElement);
|
||||
}
|
||||
|
||||
spanElement.appendChild(urlElement);
|
||||
}
|
||||
|
||||
// converts a RGBA uint number to components
|
||||
processColor(template: Template, spanElement: HTMLSpanElement) {
|
||||
const r = (template.color & 0xFF000000) >>> 24;
|
||||
const g = (template.color & 0xFF0000) >>> 16;
|
||||
const b = (template.color & 0xFF00) >>> 8;
|
||||
const a = (template.color & 0xFF) / 255.0;
|
||||
|
||||
spanElement.style.color = `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
processEmote(template: Template, spanElement: HTMLSpanElement) {
|
||||
const imgElement = document.createElement('img');
|
||||
imgElement.src = `/emote/${template.content}`;
|
||||
|
||||
spanElement.classList.add('emote-icon');
|
||||
spanElement.appendChild(imgElement);
|
||||
}
|
||||
|
||||
processIcon(template: Template, spanElement: HTMLSpanElement) {
|
||||
spanElement.classList.add('gfd-icon');
|
||||
spanElement.classList.add(`gfd-icon-hq-${template.iconId}`);
|
||||
}
|
||||
|
||||
clearAllMessages() {
|
||||
if (this.elements.messagesList === null)
|
||||
return;
|
||||
|
||||
this.elements.messagesList.innerHTML = '';
|
||||
}
|
||||
|
||||
setupSSEConnection() {
|
||||
this.connection = source('/sse')
|
||||
|
||||
this.connection.select('close').subscribe((data: string) => {
|
||||
console.log(`close: ${data}`)
|
||||
if (data) {
|
||||
console.log('Closing SSE connection.');
|
||||
this.connection.close();
|
||||
}
|
||||
});
|
||||
|
||||
// new messages to be appended to the message list
|
||||
this.connection.select('new-message').subscribe((data: string) => {
|
||||
console.log(`new-message: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
let message: MessageResponse = JSON.parse(data);
|
||||
this.addMessage(message);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// a bulk of new messages, with a clear of the message list beforehand
|
||||
this.connection.select('bulk-messages').subscribe((data: string) => {
|
||||
console.log(`bulk-messages: ${data}`)
|
||||
if (data) {
|
||||
this.clearAllMessages();
|
||||
try {
|
||||
let messages: Messages = JSON.parse(data);
|
||||
for (const message of messages.messages) {
|
||||
this.addMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.select('channel-switched').subscribe((data: string) => {
|
||||
console.log(`channel-switched: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
let channel: SwitchChannel = JSON.parse(data);
|
||||
this.updateChannelHint(channel);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// list of all channels
|
||||
this.connection.select('channel-list').subscribe((data: string) => {
|
||||
console.log(`channel-list: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
let channelList: ChannelList = JSON.parse(data);
|
||||
this.updateChannels(channelList);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// tab switched
|
||||
this.connection.select('tab-switched').subscribe((data: string) => {
|
||||
console.log(`tab-switched: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
const chatTab: ChatTab = JSON.parse(data);
|
||||
selectedTab.index = chatTab.index;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// list of all tabs
|
||||
this.connection.select('tab-list').subscribe((data: string) => {
|
||||
console.log(`tab-list: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
const chatTabList: ChatTabList = JSON.parse(data);
|
||||
knownTabs.length = 0;
|
||||
for (const tab of chatTabList.tabs) {
|
||||
knownTabs.push(tab);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// the unread state of a specific tab has changed
|
||||
this.connection.select('tab-unread-state').subscribe((data: string) => {
|
||||
console.log(`tab-unread-state`, data)
|
||||
if (data) {
|
||||
try {
|
||||
const chatTabUnreadState: ChatTabUnreadState = JSON.parse(data);
|
||||
let tab = knownTabs.find((tab) => tab.index === chatTabUnreadState.index);
|
||||
if (tab) {
|
||||
tab.unreadCount = chatTabUnreadState.unreadCount;
|
||||
}
|
||||
else {
|
||||
console.error("Unable to find tab!")
|
||||
console.error(chatTabUnreadState)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// from kizer, gfd icons
|
||||
interface GdfEntry {
|
||||
id: number,
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
unk0A: number,
|
||||
redirect: number,
|
||||
unk0E: number,
|
||||
}
|
||||
|
||||
interface StylesheetEntry {
|
||||
ids: number[],
|
||||
style1: string,
|
||||
style2: string,
|
||||
width: number,
|
||||
}
|
||||
|
||||
export async function addGfdStylesheet(gfdPath: string, texPath: string) {
|
||||
const texPromise = loadTexAsBlob(texPath);
|
||||
const gfdPromise = loadGfd(gfdPath);
|
||||
const texUrl = URL.createObjectURL(await texPromise);
|
||||
const gfd = await gfdPromise;
|
||||
|
||||
const stylesheets: {[id: number]: StylesheetEntry} = [];
|
||||
for (const entry of gfd) {
|
||||
if (entry.width * entry.height <= 0)
|
||||
continue;
|
||||
|
||||
if (entry.redirect !== 0) {
|
||||
stylesheets[entry.redirect].ids.push(entry.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
stylesheets[entry.id] = {
|
||||
ids: [entry.id],
|
||||
style1: [
|
||||
`background-position: -${entry.left}px -${entry.top}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width}px`,
|
||||
`height: ${entry.height}px`
|
||||
].join(';'),
|
||||
style2: [
|
||||
`background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width * 2}px`,
|
||||
`height: ${entry.height * 2}px`
|
||||
].join(';'),
|
||||
width: entry.width
|
||||
};
|
||||
}
|
||||
|
||||
let stylesheet = '';
|
||||
for (const entry of Object.values(stylesheets)) {
|
||||
if (!entry)
|
||||
continue;
|
||||
|
||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-${x}::before`).join(', ')}{${entry.style1};}`;
|
||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-hq-${x}::before`).join(', ')}{${entry.style2};}`;
|
||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-${x}`).join(', ')}{width:${entry.width}px;}`;
|
||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-hq-${x}`).join(', ')}{width:${entry.width * 2}px;}`;
|
||||
}
|
||||
|
||||
const styleNode = document.createElement('style');
|
||||
styleNode.appendChild(document.createTextNode(stylesheet));
|
||||
document.head.appendChild(styleNode);
|
||||
}
|
||||
|
||||
async function loadTexAsBlob(path: string) {
|
||||
const tex = parseTex(await (await fetch(path)).arrayBuffer());
|
||||
if (tex.format !== 0x1450) // B8G8R8A8
|
||||
throw 'Not supported';
|
||||
|
||||
const dataArray = new Uint8ClampedArray(tex.buffer, tex.offsetToSurface[0], tex.width * tex.height * 4);
|
||||
for (let i = 0; i < dataArray.length; i += 4) {
|
||||
const t = dataArray[i];
|
||||
dataArray[i] = dataArray[i + 2];
|
||||
dataArray[i + 2] = t;
|
||||
}
|
||||
const imageData = new ImageData(dataArray, tex.width, tex.height);
|
||||
const bitmap = await createImageBitmap(imageData);
|
||||
|
||||
const canvas = new OffscreenCanvas(tex.width, tex.height);
|
||||
canvas.getContext('bitmaprenderer')?.transferFromImageBitmap(bitmap);
|
||||
return await canvas.convertToBlob();
|
||||
}
|
||||
|
||||
async function loadGfd(path: string) {
|
||||
const buffer = new DataView(await (await fetch(path)).arrayBuffer());
|
||||
const count = buffer.getInt32(8, true);
|
||||
const entries: GdfEntry[] = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const offset = 0x10 + (i * 0x10);
|
||||
entries[i] = {
|
||||
id: buffer.getInt16(offset, true),
|
||||
left: buffer.getInt16(offset + 2, true),
|
||||
top: buffer.getInt16(offset + 4, true),
|
||||
width: buffer.getInt16(offset + 6, true),
|
||||
height: buffer.getInt16(offset + 8, true),
|
||||
unk0A: buffer.getInt16(offset + 10, true),
|
||||
redirect: buffer.getInt16(offset + 12, true),
|
||||
unk0E: buffer.getInt16(offset + 14, true),
|
||||
};
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseTex(arrayBuffer: ArrayBuffer) {
|
||||
const buffer = new DataView(arrayBuffer);
|
||||
const type = buffer.getInt32(0, true);
|
||||
const format = buffer.getInt32(4, true);
|
||||
const width = buffer.getInt16(8, true);
|
||||
const height = buffer.getInt16(10, true);
|
||||
const depth = buffer.getInt16(12, true);
|
||||
const mipsAndFlag = buffer.getInt8(14);
|
||||
const arraySize = buffer.getInt8(15);
|
||||
const lodOffsets = [buffer.getInt32(16, true), buffer.getInt32(20, true), buffer.getInt32(24, true)];
|
||||
const offsetToSurface = [buffer.getInt32(28, true), buffer.getInt32(32, true), buffer.getInt32(36, true), buffer.getInt32(40, true), buffer.getInt32(44, true), buffer.getInt32(48, true), buffer.getInt32(52, true), buffer.getInt32(56, true), buffer.getInt32(60, true), buffer.getInt32(64, true), buffer.getInt32(68, true), buffer.getInt32(72, true), buffer.getInt32(76, true)];
|
||||
|
||||
return {
|
||||
buffer: arrayBuffer,
|
||||
type,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
mipsAndFlag,
|
||||
arraySize,
|
||||
lodOffsets,
|
||||
offsetToSurface,
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -1,25 +0,0 @@
|
||||
export enum WebPayloadType {
|
||||
// Dalamud
|
||||
Unknown,
|
||||
Player,
|
||||
Item,
|
||||
Status,
|
||||
RawText,
|
||||
UIForeground,
|
||||
UIGlow,
|
||||
MapLink,
|
||||
AutoTranslateText,
|
||||
EmphasisItalic,
|
||||
Icon,
|
||||
Quest,
|
||||
DalamudLink,
|
||||
NewLine,
|
||||
SeHyphen,
|
||||
PartyFinder,
|
||||
|
||||
// Custom
|
||||
CustomPartyFinder = 0x50,
|
||||
CustomAchievement = 0x51,
|
||||
CustomUri = 0x52,
|
||||
CustomEmote = 0x53,
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { ChatTab } from "./chat.svelte";
|
||||
|
||||
export const isChannelLocked: { locked: boolean } = $state({ locked: false });
|
||||
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
|
||||
|
||||
export interface ChannelOption {
|
||||
text: string;
|
||||
value: number;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
export const selectedTab: { index: number } = $state({ index: 0 });
|
||||
export const knownTabs: ChatTab[] = $state([]);
|
||||
export const tabPaneState: { visible: boolean } = $state({ visible: true });
|
||||
export const tabPaneAnimationState: { noAnimation: boolean } = $state({ noAnimation: true });
|
||||
export const persistentTabPabeStateKey = 'chat2_tab_pane_visible';
|
||||
|
||||
export function openTabPane() {
|
||||
tabPaneState.visible = true;
|
||||
window.localStorage.setItem(persistentTabPabeStateKey, 'true');
|
||||
}
|
||||
|
||||
export function closeTabPane() {
|
||||
tabPaneState.visible = false;
|
||||
window.localStorage.setItem(persistentTabPabeStateKey, 'false');
|
||||
}
|
||||
|
||||
export const chatInput: { content: string } = $state({ content: ''} );
|
||||
export const messagesList: {
|
||||
element: HTMLElement | null,
|
||||
scrolledToBottom: boolean
|
||||
} = $state({ element: null, scrolledToBottom: true });
|
||||
|
||||
export function scrollMessagesToBottom() {
|
||||
if (messagesList.element === null)
|
||||
return;
|
||||
|
||||
messagesList.element.lastElementChild?.scrollIntoView();
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
// https://stackoverflow.com/a/79696571
|
||||
export const subscribe = <T>(functionToState: () => T, callback: (v: T) => void) => {
|
||||
let value = writable<T>(functionToState());
|
||||
value.subscribe(callback);
|
||||
|
||||
$effect(() => {
|
||||
value.set(functionToState());
|
||||
});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/static/start.css">
|
||||
</svelte:head>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -1 +0,0 @@
|
||||
export const prerender = true;
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { Alert } from '@sveltestrap/sveltestrap';
|
||||
|
||||
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
||||
$effect.pre(() => {
|
||||
if (page.url.searchParams.has('message')) {
|
||||
data = {
|
||||
hasWarning: true,
|
||||
content: page.url.searchParams.get('message') ?? '',
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
hasWarning: false,
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="auth">
|
||||
<h1>Authcode</h1>
|
||||
{#if data?.hasWarning }
|
||||
<Alert content={data.content} color="warning" dismissible={true}/>
|
||||
{/if}
|
||||
<form action="/auth" method="POST">
|
||||
<label><input type="password" name="authcode"></label>
|
||||
<button type="submit" class="submitButton">Submit</button>
|
||||
</form>
|
||||
<div data-sveltekit-preload-data="false">
|
||||
<img src="/emote/Sure" alt=":Sure:" data-sveltekit-preload-data="off">
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { Alert } from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from 'svelte';
|
||||
import { ChatTwoWeb } from '$lib/chat.svelte'
|
||||
import { tabPaneState, persistentTabPabeStateKey } from "$lib/shared.svelte";
|
||||
import { addGfdStylesheet } from "$lib/gfd";
|
||||
import DynamicTextArea from "../../components/DynamicTextArea.svelte";
|
||||
import ChannelSelector from "../../components/ChannelSelector.svelte";
|
||||
import TabPane from "../../components/TabPane.svelte";
|
||||
import TabPaneOpener from "../../components/TabPaneOpener.svelte";
|
||||
|
||||
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
||||
$effect.pre(() => {
|
||||
if (page.url.searchParams.has('message')) {
|
||||
data = {
|
||||
hasWarning: true,
|
||||
content: page.url.searchParams.get('message') ?? '',
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
hasWarning: false,
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
console.log('the component has mounted');
|
||||
|
||||
// Populate the stylesheet with gfd data
|
||||
addGfdStylesheet('/files/gfdata.gfd', '/files/fonticon_ps5.tex');
|
||||
|
||||
// read saved tab pane state from localStorage
|
||||
try {
|
||||
const tabPaneVisible = window.localStorage.getItem(persistentTabPabeStateKey);
|
||||
if (tabPaneVisible !== null) {
|
||||
tabPaneState.visible = JSON.parse(tabPaneVisible);
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON.parse() failed, let's reset what's in localStorage
|
||||
window.localStorage.removeItem(persistentTabPabeStateKey);
|
||||
}
|
||||
|
||||
// Load all web functions in the background
|
||||
const _ = new ChatTwoWeb();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="chat">
|
||||
<TabPane />
|
||||
|
||||
<div class="main-content">
|
||||
<TabPaneOpener />
|
||||
|
||||
<section id="messages">
|
||||
<div class="scroll-container">
|
||||
<ol id="messages-list"></ol>
|
||||
</div>
|
||||
|
||||
<div id="more-messages-indicator">
|
||||
<!-- "arrow-down" icon from https://github.com/feathericons/feather, under MIT license -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if data?.hasWarning }
|
||||
<section id="warnings">
|
||||
<Alert content={data.content} color="warning" dismissible={true}/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section id="input">
|
||||
<form>
|
||||
<div class="input-container">
|
||||
<DynamicTextArea />
|
||||
<ChannelSelector />
|
||||
</div>
|
||||
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="timestamp-width-probe"></div>
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,472 +0,0 @@
|
||||
/* fonts */
|
||||
@font-face {
|
||||
font-family: Lodestone;
|
||||
src: url('/files/FFXIV_Lodestone_SSF.ttf') format('truetype');
|
||||
unicode-range: U+E020-E0DB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter var';
|
||||
font-weight: 100 900;
|
||||
font-style: oblique 0deg 10deg;
|
||||
src: url('/static/Inter.var.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* variables */
|
||||
:root {
|
||||
--fg: white;
|
||||
--fg-faint: #a0a0a0;
|
||||
--fg-scrollbar: #404040;
|
||||
--bg: #101010;
|
||||
--bg-sidebar: #080808;
|
||||
--bg-input: #202020;
|
||||
--bg-input-hover: #282828;
|
||||
--focus-color: #4060a0;
|
||||
--unread-color: #beffa0;
|
||||
|
||||
--gradient-clickable: linear-gradient(to bottom, #404040, var(--bg-input) 65%, var(--bg-input));
|
||||
--gradient-clickable-hover: linear-gradient(to bottom, #505050, var(--bg-input-hover) 65%, var(--bg-input-hover));
|
||||
|
||||
--timestamp-width: 70px;
|
||||
}
|
||||
|
||||
/* reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--fg);
|
||||
font-family: Lodestone, 'Inter var', sans-serif;
|
||||
font-feature-settings: 'tnum', 'calt' 0; /* calt appears to be on by default */
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
span > a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* layout and global styles */
|
||||
body {
|
||||
padding: 25px;
|
||||
height: 100dvh;
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
|
||||
main.chat {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5);
|
||||
|
||||
& > .main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
main.auth {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
input { width: 150px; }
|
||||
|
||||
input, .submitButton {
|
||||
padding: 5px 20px;
|
||||
font-size: 1rem;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 20px;
|
||||
background-color: var(--bg-input);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
padding: 5px 15px;
|
||||
border: 3px solid var(--bg-input);
|
||||
background-image: var(--gradient-clickable);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-input-hover);
|
||||
background-color: var(--bg-input-hover);
|
||||
background-image: var(--gradient-clickable-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* tab list */
|
||||
aside#tabs {
|
||||
flex: 0 0 auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--fg-scrollbar) var(--bg-sidebar);
|
||||
background-color: var(--bg-sidebar);
|
||||
transition: width 250ms ease;
|
||||
|
||||
width: 200px;
|
||||
&.hidden { width: 0px; }
|
||||
|
||||
&.no-animation {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
div.inner {
|
||||
width: 200px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 550;
|
||||
|
||||
button {
|
||||
margin-bottom: 2px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0.6rem 0 0.75rem;
|
||||
border-color: var(--fg-faint);
|
||||
}
|
||||
|
||||
ol#tabs-list {
|
||||
margin: 0 -5px;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 3px 5px;
|
||||
color: var(--fg-faint);
|
||||
border-radius: 3px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
li:has(button:hover) {
|
||||
color: var(--fg);
|
||||
background-color: rgb(from var(--bg-input) r g b / 0.5);
|
||||
}
|
||||
|
||||
li.active {
|
||||
color: var(--fg);
|
||||
background-color: var(--bg-input);
|
||||
}
|
||||
|
||||
li.unread button {
|
||||
color: var(--unread-color);
|
||||
text-shadow: 0 0 5px var(--unread-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* message list */
|
||||
section#messages {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.5;
|
||||
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
scrollbar-color: var(--fg-scrollbar) var(--bg);
|
||||
}
|
||||
|
||||
ol#messages-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.timestamp {
|
||||
flex: 0 0 var(--timestamp-width);
|
||||
color: var(--fg-faint);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
#more-messages-indicator {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 30px;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
svg {
|
||||
filter: drop-shadow(0 0 5px #60a0ff) drop-shadow(0 0 15px #60a0ff);
|
||||
}
|
||||
}
|
||||
|
||||
&.more-messages #more-messages-indicator {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#timestamp-width-probe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* alerts */
|
||||
section#warnings {
|
||||
flex-grow: 0;
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
/* input bar, channel selector, ... */
|
||||
section#input {
|
||||
flex-grow: 0;
|
||||
padding: 20px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input, button {
|
||||
font-size: 1rem;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 20px;
|
||||
background-color: var(--bg-input);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 15px;
|
||||
border: 3px solid var(--bg-input);
|
||||
background-image: var(--gradient-clickable);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-input-hover);
|
||||
background-color: var(--bg-input-hover);
|
||||
background-image: var(--gradient-clickable-hover);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding-left: calc(20px + 1.5rem);
|
||||
|
||||
&::before {
|
||||
/* "send" icon from https://github.com/feathericons/feather, under MIT license */
|
||||
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>');
|
||||
}
|
||||
}
|
||||
|
||||
button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
background-color: var(--fg);
|
||||
mask-size: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
#chat-input {
|
||||
width: 100%;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
#channel-select {
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
left: 23px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 550;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
|
||||
padding: 5px 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-input-hover);
|
||||
background-color: var(--bg-input-hover);
|
||||
background-image: var(--gradient-clickable-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* icons, emotes */
|
||||
.gfd-icon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
zoom: 0.75;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.emote-icon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 1px;
|
||||
vertical-align: middle;
|
||||
overflow: visible;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*** mobile ***/
|
||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-height: 400px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
main.chat {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
section#messages {
|
||||
font-size: 0.9rem;
|
||||
|
||||
li {
|
||||
align-items: baseline;
|
||||
|
||||
.timestamp {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#timestamp-width-probe {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
section#input {
|
||||
button {
|
||||
max-width: 0;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
button::before {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.input-container #channel-select {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gfd-icon { zoom: 0.65; }
|
||||
.emote-icon {
|
||||
width: 1.5rem;
|
||||
|
||||
img {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
aside#tabs {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
|
||||
div.inner {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
prerender: {
|
||||
handleHttpError: 'warn'
|
||||
},
|
||||
adapter: adapter({
|
||||
// default options are shown. On some platforms
|
||||
// these options are set automatically — see below
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true,
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
using WatsonWebserver.Core;
|
||||
using WatsonWebserver.Lite;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class HostContext
|
||||
{
|
||||
public readonly ServerCore Core;
|
||||
|
||||
public bool IsActive;
|
||||
public bool IsStopping;
|
||||
|
||||
// Initialized at webserver start
|
||||
public WebserverLite Host = null!;
|
||||
public Processing Processing = null!;
|
||||
public RouteController RouteController = null!;
|
||||
|
||||
public readonly List<SSEConnection> EventConnections = [];
|
||||
|
||||
public readonly CancellationTokenSource TokenSource = new();
|
||||
public readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Frontend/");
|
||||
|
||||
public HostContext(ServerCore core)
|
||||
{
|
||||
Core = core;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
Host = new WebserverLite(new WebserverSettings("*", Plugin.Config.WebinterfacePort), DefaultRoute);
|
||||
|
||||
Processing = new Processing(this);
|
||||
RouteController = new RouteController(this);
|
||||
|
||||
Host.Routes.PreAuthentication.Content.BaseDirectory = StaticDir;
|
||||
Host.Routes.AuthenticateRequest = CheckAuthenticationCookie;
|
||||
Host.Events.ExceptionEncountered += ExceptionEncountered;
|
||||
|
||||
// Settings
|
||||
#if DEBUG
|
||||
Host.Settings.Debug.Requests = true;
|
||||
Host.Settings.Debug.Routing = true;
|
||||
Host.Settings.Debug.Responses = true;
|
||||
Host.Settings.Debug.AccessControl = true;
|
||||
#endif
|
||||
Host.Events.Logger = logMessage => Plugin.Log.Debug(logMessage);
|
||||
|
||||
IsActive = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsActive = false;
|
||||
Plugin.Log.Error(ex, "Initialization of the webserver failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
Host.Start(TokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Webserver failed to boot up.");
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> Stop()
|
||||
{
|
||||
// Is already stopped
|
||||
if (!IsActive)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
IsActive = false;
|
||||
IsStopping = true;
|
||||
Host.Stop();
|
||||
|
||||
// Save our session tokens
|
||||
Core.Plugin.SaveConfig();
|
||||
|
||||
// We get a copy, so that the original can be cleaned up successfully
|
||||
foreach (var eventServer in EventConnections.ToArray())
|
||||
await eventServer.DisposeAsync();
|
||||
|
||||
EventConnections.Clear();
|
||||
Host.Dispose();
|
||||
RouteController.Dispose();
|
||||
IsStopping = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Webserver failed to stop and dispose all resources.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Stop();
|
||||
}
|
||||
|
||||
#region GeneralHandlers
|
||||
private static void ExceptionEncountered(object? _, ExceptionEventArgs args)
|
||||
{
|
||||
Plugin.Log.Error(args.Exception, "Webserver threw an exception.");
|
||||
}
|
||||
|
||||
private async Task<bool> DefaultRoute(HttpContextBase ctx)
|
||||
{
|
||||
return await ctx.Response.Send("Nothing to see here.");
|
||||
}
|
||||
|
||||
private async Task CheckAuthenticationCookie(HttpContextBase ctx)
|
||||
{
|
||||
if (Plugin.Config.AuthStore.Count == 0)
|
||||
{
|
||||
await RouteController.Redirect(ctx, "/", ("message", "Invalid session token."));
|
||||
return;
|
||||
}
|
||||
|
||||
var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? "");
|
||||
if (!cookies.TryGetValue("ChatTwo-token", out var token) || !Plugin.Config.AuthStore.Contains(token))
|
||||
await RouteController.Redirect(ctx, "/", ("message", "Invalid session token."));
|
||||
|
||||
// Do nothing to let auth pass
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using ChatTwo.Code;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChatTwo.Http.MessageProtocol;
|
||||
|
||||
#region Outgoing SSE
|
||||
/// <summary>
|
||||
/// Contains a valid tab with its assigned index
|
||||
/// </summary>
|
||||
public struct ChatTab(string name, int index, uint unreadCount)
|
||||
{
|
||||
[JsonProperty("name")] public string Name = name;
|
||||
[JsonProperty("index")] public int Index = index;
|
||||
[JsonProperty("unreadCount")] public uint UnreadCount = unreadCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a number of tabs that are valid for the user to pick from
|
||||
/// </summary>
|
||||
public struct ChatTabList(ChatTab[] tabs)
|
||||
{
|
||||
[JsonProperty("tabs")] public ChatTab[] Tabs = tabs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a valid tab index and the current unread state as a number unread of messages
|
||||
/// </summary>
|
||||
public struct ChatTabUnreadState(int index, uint unreadCount)
|
||||
{
|
||||
[JsonProperty("index")] public int Index = index;
|
||||
[JsonProperty("unreadCount")] public uint UnreadCount = unreadCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the current channel name
|
||||
/// </summary>
|
||||
public struct SwitchChannel((MessageTemplate[] Name, bool Locked) channel)
|
||||
{
|
||||
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.Name;
|
||||
[JsonProperty("channelLocked")] public bool Locked = channel.Locked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a number of channels that are valid for the user to pick from
|
||||
/// </summary>
|
||||
public struct ChannelList(Dictionary<string, uint> channels)
|
||||
{
|
||||
[JsonProperty("channels")] public Dictionary<string, uint> Channels = channels;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains one or multiple messages
|
||||
/// </summary>
|
||||
public struct Messages(MessageResponse[] set)
|
||||
{
|
||||
[JsonProperty("messages")] public MessageResponse[] Set = set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a single message with all its templates and a timestamp
|
||||
/// </summary>
|
||||
public struct MessageResponse()
|
||||
{
|
||||
[JsonProperty("id")] public Guid Id = Guid.Empty;
|
||||
[JsonProperty("timestamp")] public string Timestamp = "";
|
||||
[JsonProperty("templates")] public MessageTemplate[] Templates = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Template that is used for the channel name or any message posted to the chatlog
|
||||
/// </summary>
|
||||
public struct MessageTemplate()
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of payload.
|
||||
/// Dalamuds enum is just a baseline, there exists more that are expressed through raw values.
|
||||
/// </summary>
|
||||
[JsonProperty("payloadType")] public WebPayloadType PayloadType = WebPayloadType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Used for text and emote.
|
||||
/// </summary>
|
||||
[JsonProperty("content")] public string Content = "";
|
||||
|
||||
/// <summary>
|
||||
/// Used for an icon.
|
||||
/// </summary>
|
||||
[JsonProperty("iconId")] public uint IconId;
|
||||
|
||||
/// <summary>
|
||||
/// Used for text and url
|
||||
///
|
||||
/// Note:
|
||||
/// 0 is used for invalid colors
|
||||
/// </summary>
|
||||
[JsonProperty("color")] public uint Color;
|
||||
|
||||
public static MessageTemplate Empty => new();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Outgoing POST
|
||||
public struct OkResponse(string message)
|
||||
{
|
||||
[JsonProperty("message")] public string Message = message;
|
||||
}
|
||||
|
||||
public struct ErrorResponse(string reason)
|
||||
{
|
||||
[JsonProperty("reason")] public string Reason = reason;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Incoming POST
|
||||
/// <summary>
|
||||
/// Message must fulfill the posting requirement
|
||||
/// Greater than or equal 2 characters
|
||||
/// Less than or equal 500 characters
|
||||
/// </summary>
|
||||
public struct IncomingMessage()
|
||||
{
|
||||
[JsonProperty("message")] public string Message = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The channel type must be a valid <see cref="InputChannel"/>
|
||||
/// </summary>
|
||||
public struct IncomingChannel()
|
||||
{
|
||||
[JsonProperty("channel")] public InputChannel Channel = InputChannel.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tabs index must be a valid int
|
||||
/// </summary>
|
||||
public struct IncomingTab()
|
||||
{
|
||||
[JsonProperty("index")] public int Index = -1;
|
||||
}
|
||||
#endregion
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChatTwo.Http.MessageProtocol;
|
||||
|
||||
// General
|
||||
public class CloseEvent() : BaseEvent("close");
|
||||
|
||||
// Tab related
|
||||
public class ChatTabListEvent(ChatTabList list) : BaseEvent("tab-list", JsonConvert.SerializeObject(list));
|
||||
public class ChatTabSwitchedEvent(ChatTab chatTab) : BaseEvent("tab-switched", JsonConvert.SerializeObject(chatTab));
|
||||
public class ChatTabUnreadStateEvent(ChatTabUnreadState unreadState) : BaseEvent("tab-unread-state", JsonConvert.SerializeObject(unreadState));
|
||||
|
||||
// Input channel related
|
||||
public class ChannelListEvent(ChannelList channelList) : BaseEvent("channel-list", JsonConvert.SerializeObject(channelList));
|
||||
public class SwitchChannelEvent(SwitchChannel switchChannel) : BaseEvent("channel-switched", JsonConvert.SerializeObject(switchChannel));
|
||||
|
||||
// Chat message related
|
||||
public class BulkMessagesEvent(Messages messages) : BaseEvent("bulk-messages", JsonConvert.SerializeObject(messages));
|
||||
public class NewMessageEvent(MessageResponse message) : BaseEvent("new-message", JsonConvert.SerializeObject(message));
|
||||
|
||||
public class BaseEvent(string eventType, string? data = null)
|
||||
{
|
||||
private string Event = eventType;
|
||||
private string Data = data ?? "0"; // SSE requires data on each response
|
||||
|
||||
public byte[] Build()
|
||||
{
|
||||
// SSE always ends with \n\n
|
||||
return Encoding.UTF8.GetBytes($"event: {Event}\ndata: {Data}\n\n");
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace ChatTwo.Http.MessageProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Baseline: <see cref="Dalamud.Game.Text.SeStringHandling.PayloadType"/>
|
||||
/// </summary>
|
||||
public enum WebPayloadType
|
||||
{
|
||||
// Dalamud
|
||||
Unknown,
|
||||
Player,
|
||||
Item,
|
||||
Status,
|
||||
RawText,
|
||||
UIForeground,
|
||||
UIGlow,
|
||||
MapLink,
|
||||
AutoTranslateText,
|
||||
EmphasisItalic,
|
||||
Icon,
|
||||
Quest,
|
||||
DalamudLink,
|
||||
NewLine,
|
||||
SeHyphen,
|
||||
PartyFinder,
|
||||
|
||||
// Custom
|
||||
CustomPartyFinder = 0x50,
|
||||
CustomAchievement = 0x51,
|
||||
CustomUri = 0x52,
|
||||
CustomEmote = 0x53,
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System.Globalization;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class Processing
|
||||
{
|
||||
private readonly HostContext HostContext;
|
||||
|
||||
public Processing(HostContext hostContext)
|
||||
{
|
||||
HostContext = hostContext;
|
||||
}
|
||||
|
||||
internal (MessageTemplate[] Name, bool Locked) ReadChannelName(Chunk[] channelName)
|
||||
{
|
||||
var locked = HostContext.Core.Plugin.CurrentTab is not { Channel: null };
|
||||
return (channelName.Select(ProcessChunk).ToArray(), locked);
|
||||
}
|
||||
|
||||
internal async Task<MessageResponse[]> ReadMessageList()
|
||||
{
|
||||
var tabMessages = await HostContext.Core.Plugin.CurrentTab.Messages.GetCopy();
|
||||
return tabMessages.TakeLast(Plugin.Config.WebinterfaceMaxLinesToSend).Select(ReadMessageContent).ToArray();
|
||||
}
|
||||
|
||||
internal MessageResponse ReadMessageContent(Message message)
|
||||
{
|
||||
var response = new MessageResponse
|
||||
{
|
||||
Id = message.Id,
|
||||
Timestamp = message.Date.ToLocalTime().ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("es-ES"))
|
||||
};
|
||||
|
||||
var sender = message.Sender.Select(ProcessChunk);
|
||||
var content = message.Content.Select(ProcessChunk);
|
||||
response.Templates = sender.Concat(content).ToArray();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private MessageTemplate ProcessChunk(Chunk chunk)
|
||||
{
|
||||
if (chunk is IconChunk { } icon)
|
||||
{
|
||||
var iconId = (uint)icon.Icon;
|
||||
return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {PayloadType = WebPayloadType.Icon, IconId = iconId}: MessageTemplate.Empty;
|
||||
}
|
||||
|
||||
if (chunk is TextChunk { } text)
|
||||
{
|
||||
if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes)
|
||||
{
|
||||
var image = EmoteCache.GetEmote(emotePayload.Code);
|
||||
|
||||
if (image is { Failed: false })
|
||||
return new MessageTemplate { PayloadType = WebPayloadType.CustomEmote, Color = 0, Content = emotePayload.Code };
|
||||
}
|
||||
|
||||
var color = text.Foreground;
|
||||
if (color == null && text.FallbackColour != null)
|
||||
{
|
||||
var type = text.FallbackColour.Value;
|
||||
color = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor();
|
||||
}
|
||||
|
||||
color ??= 0;
|
||||
|
||||
var userContent = text.Content;
|
||||
if (HostContext.Core.Plugin.ChatLogWindow.ScreenshotMode)
|
||||
{
|
||||
if (chunk.Link is PlayerPayload playerPayload)
|
||||
userContent = HostContext.Core.Plugin.ChatLogWindow.HidePlayerInString(userContent, playerPayload.PlayerName, playerPayload.World.RowId);
|
||||
else if (Plugin.PlayerState.IsLoaded)
|
||||
userContent = HostContext.Core.Plugin.ChatLogWindow.HidePlayerInString(userContent, Plugin.PlayerState.CharacterName, Plugin.PlayerState.HomeWorld.RowId);
|
||||
}
|
||||
|
||||
var isNotUrl = text.Link is not UriPayload;
|
||||
return new MessageTemplate { PayloadType = isNotUrl ? WebPayloadType.RawText : WebPayloadType.CustomUri, Color = color.Value, Content = userContent };
|
||||
}
|
||||
|
||||
return MessageTemplate.Empty;
|
||||
}
|
||||
|
||||
public async Task<Messages> GetAllMessages()
|
||||
{
|
||||
var messages = await WebserverUtil.FrameworkWrapper(ReadMessageList);
|
||||
return new Messages(messages);
|
||||
}
|
||||
|
||||
public SwitchChannel GetCurrentChannel()
|
||||
{
|
||||
var channel = ReadChannelName(HostContext.Core.Plugin.ChatLogWindow.PreviousChannel);
|
||||
return new SwitchChannel(channel);
|
||||
}
|
||||
|
||||
public ChannelList GetValidChannels()
|
||||
{
|
||||
var channels = HostContext.Core.Plugin.ChatLogWindow.GetValidChannels();
|
||||
return new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value));
|
||||
}
|
||||
|
||||
public ChatTab GetCurrentTab()
|
||||
{
|
||||
var currentTab = HostContext.Core.Plugin.CurrentTab;
|
||||
return new ChatTab(currentTab.Name, HostContext.Core.Plugin.LastTab, currentTab.Unread);
|
||||
}
|
||||
|
||||
public ChatTabList GetAllTabs()
|
||||
{
|
||||
var tabs = Plugin.Config.Tabs.Select((tab, idx) => new ChatTab(tab.Name, idx, tab.Unread)).ToArray();
|
||||
return new ChatTabList(tabs);
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Web;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using ChatTwo.Util;
|
||||
using Lumina.Data.Files;
|
||||
using Newtonsoft.Json;
|
||||
using WatsonWebserver.Core;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
using HttpMethod = WatsonWebserver.Core.HttpMethod;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class RouteController
|
||||
{
|
||||
private readonly HostContext HostContext;
|
||||
|
||||
private readonly string AuthTemplate;
|
||||
private readonly string ChatBoxTemplate;
|
||||
|
||||
private readonly ConcurrentDictionary<string, long> RateLimit = [];
|
||||
|
||||
private readonly JsonSerializerSettings JsonSettings = new()
|
||||
{
|
||||
Error = delegate(object? _, ErrorEventArgs args) { args.ErrorContext.Handled = true; }
|
||||
};
|
||||
|
||||
public RouteController(HostContext hostContext)
|
||||
{
|
||||
HostContext = hostContext;
|
||||
|
||||
AuthTemplate = File.ReadAllText(Path.Combine(HostContext.StaticDir, "index.html"));
|
||||
ChatBoxTemplate = File.ReadAllText(Path.Combine(HostContext.StaticDir, "chat.html"));
|
||||
|
||||
// Pre Auth
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/", AuthRoute, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.POST, "/auth", AuthenticateClient, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/gfdata.gfd", GetGfdData, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/fonticon_ps5.tex", GetTexData, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/FFXIV_Lodestone_SSF.ttf", GetLodestoneFont, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/favicon.ico", GetFavicon, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/emote/{name}", GetEmote, ExceptionRoute);
|
||||
|
||||
// Post Auth
|
||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/chat", ChatBoxRoute, ExceptionRoute);
|
||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/send", ReceiveMessage, ExceptionRoute);
|
||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/channel", ReceiveChannelSwitch, ExceptionRoute);
|
||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/tab", ReceiveTabSwitch, ExceptionRoute);
|
||||
|
||||
// Ship all other static files dynamically
|
||||
HostContext.Host.Routes.PreAuthentication.Content.Add("/_app/", true, ExceptionRoute);
|
||||
HostContext.Host.Routes.PreAuthentication.Content.Add("/static/", true, ExceptionRoute);
|
||||
|
||||
// Server-Sent Events Route
|
||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/sse", NewSSEConnection, ExceptionRoute);
|
||||
}
|
||||
|
||||
private async Task ExceptionRoute(HttpContextBase ctx, Exception _)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.Send("Internal Server Error, please try again");
|
||||
}
|
||||
|
||||
private async Task AuthRoute(HttpContextBase ctx)
|
||||
{
|
||||
if (Plugin.Config.AuthStore.Count > 0)
|
||||
{
|
||||
var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? "");
|
||||
if (cookies.TryGetValue("ChatTwo-token", out var value) && Plugin.Config.AuthStore.Contains(value))
|
||||
{
|
||||
await Redirect(ctx, "/chat");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.Response.Send(AuthTemplate);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#region FileHandlerRoutes
|
||||
private async Task GetTexData(HttpContextBase ctx)
|
||||
{
|
||||
var data = Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!.Data;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetGfdData(HttpContextBase ctx)
|
||||
{
|
||||
var data = Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetLodestoneFont(HttpContextBase ctx)
|
||||
{
|
||||
var data = HostContext.Core.Plugin.FontManager.GameSymFont;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetFavicon(HttpContextBase ctx)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.Send();
|
||||
}
|
||||
|
||||
private async Task GetEmote(HttpContextBase ctx)
|
||||
{
|
||||
var name = ctx.Request.Url.Parameters["name"] ?? "";
|
||||
if (name == "" || !EmoteCache.Exists(name))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send("Malformed emote name.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var emote = EmoteCache.GetEmote(name);
|
||||
if (emote is null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send("Emote not valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the emote to be loaded a maximum of 5 times
|
||||
var timeout = 5;
|
||||
while (!emote.IsLoaded && timeout > 0)
|
||||
{
|
||||
timeout--;
|
||||
await Task.Delay(25);
|
||||
}
|
||||
|
||||
ctx.Response.Headers.Add("Cache-Control", "max-age=86400");
|
||||
await ctx.Response.Send(emote.RawData);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PreAuthRoutes
|
||||
private async Task<bool> AuthenticateClient(HttpContextBase ctx)
|
||||
{
|
||||
var currentTick = Environment.TickCount64;
|
||||
if (RateLimit.TryGetValue(ctx.Request.Source.IpAddress, out var timestamp) && timestamp > currentTick)
|
||||
{
|
||||
_ = ctx.Request.DataAsString; // Temp fix for Watson.Lite bug #155
|
||||
return await Redirect(ctx, "/", ("message", "Rate limit active (10s)"));
|
||||
}
|
||||
|
||||
// The next request will be rate limited for 10s
|
||||
RateLimit[ctx.Request.Source.IpAddress] = currentTick + 10_000;
|
||||
|
||||
var authcode = HttpUtility.ParseQueryString(ctx.Request.DataAsString ?? "").Get("authcode");
|
||||
if (authcode == null || authcode != Plugin.Config.WebinterfacePassword)
|
||||
return await Redirect(ctx, "/", ("message", "Authentication failed"));
|
||||
|
||||
var token = WebinterfaceUtil.GenerateSimpleToken();
|
||||
Plugin.Config.AuthStore.Add(token);
|
||||
|
||||
ctx.Response.Headers.Add("Set-Cookie", $"ChatTwo-token={token}");
|
||||
return await Redirect(ctx, "/chat");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PostAuthRoutes
|
||||
private async Task ChatBoxRoute(HttpContextBase ctx)
|
||||
{
|
||||
await ctx.Response.Send(ChatBoxTemplate);
|
||||
}
|
||||
|
||||
private async Task ReceiveMessage(HttpContextBase ctx)
|
||||
{
|
||||
if (!await EnforceMediaType(ctx, "application/json"))
|
||||
return;
|
||||
|
||||
var content = JsonConvert.DeserializeObject<IncomingMessage>(ctx.Request.DataAsString, JsonSettings);
|
||||
if (content.Message.Length is < 2 or > 500)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid message received.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
HostContext.Core.Plugin.ChatLogWindow.Chat = content.Message;
|
||||
HostContext.Core.Plugin.ChatLogWindow.SendChatBox(HostContext.Core.Plugin.CurrentTab);
|
||||
});
|
||||
|
||||
ctx.Response.StatusCode = 201;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Message was send to the channel.")));
|
||||
}
|
||||
|
||||
private async Task ReceiveChannelSwitch(HttpContextBase ctx)
|
||||
{
|
||||
if (!await EnforceMediaType(ctx, "application/json"))
|
||||
return;
|
||||
|
||||
var channel = JsonConvert.DeserializeObject<IncomingChannel>(ctx.Request.DataAsString, JsonSettings);
|
||||
if (!Enum.IsDefined(channel.Channel))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid channel received.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await Plugin.Framework.RunOnFrameworkThread(() => { HostContext.Core.Plugin.ChatLogWindow.SetChannel(channel.Channel); });
|
||||
|
||||
ctx.Response.StatusCode = 201;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Channel switch was initiated.")));
|
||||
}
|
||||
|
||||
private async Task ReceiveTabSwitch(HttpContextBase ctx)
|
||||
{
|
||||
if (!await EnforceMediaType(ctx, "application/json"))
|
||||
return;
|
||||
|
||||
var tab = JsonConvert.DeserializeObject<IncomingTab>(ctx.Request.DataAsString, JsonSettings);
|
||||
if (tab.Index < 0 || tab.Index >= Plugin.Config.Tabs.Count)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid tab received.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await Plugin.Framework.RunOnFrameworkThread(() => { HostContext.Core.Plugin.WantedTab = tab.Index; });
|
||||
|
||||
ctx.Response.StatusCode = 201;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Tab switch was initiated.")));
|
||||
}
|
||||
|
||||
private async Task NewSSEConnection(HttpContextBase ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
Plugin.Log.Debug($"Client connected: {ctx.Guid}");
|
||||
|
||||
var sse = new SSEConnection(HostContext.TokenSource.Token);
|
||||
await HostContext.Core.PrepareNewClient(sse);
|
||||
HostContext.EventConnections.Add(sse);
|
||||
|
||||
await sse.HandleEventLoop(ctx);
|
||||
|
||||
// It should always be done after return
|
||||
if (sse.Done)
|
||||
HostContext.EventConnections.Remove(sse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed to finish the server event function");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region RedirectHelper
|
||||
public static async Task<bool> Redirect(HttpContextBase ctx, string location, params (string, string)[] parameter)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var (key, value) in parameter)
|
||||
query.Add(key, value);
|
||||
|
||||
ctx.Response.Headers.Add("Location", $"{location}?{query}");
|
||||
ctx.Response.StatusCode = 303;
|
||||
return await ctx.Response.Send();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PreChecks
|
||||
|
||||
/// <summary>
|
||||
/// Check that the request has the correct media type that the functions expects.
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="requiredMediaType"></param>
|
||||
/// <returns>True if media type is correct, otherwise handled and false</returns>
|
||||
private async Task<bool> EnforceMediaType(HttpContextBase ctx, string requiredMediaType)
|
||||
{
|
||||
if (ctx.Request.ContentType == requiredMediaType)
|
||||
return true;
|
||||
|
||||
ctx.Response.StatusCode = 415;
|
||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Request contains wrong media type.")));
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using WatsonWebserver.Core;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class SSEConnection
|
||||
{
|
||||
private bool Stopping;
|
||||
private readonly CancellationToken Token;
|
||||
|
||||
public bool Done;
|
||||
public readonly ConcurrentQueue<BaseEvent> OutboundQueue = new();
|
||||
|
||||
public SSEConnection(CancellationToken token)
|
||||
{
|
||||
Token = token;
|
||||
}
|
||||
|
||||
public async Task HandleEventLoop(HttpContextBase ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
ctx.Response.Headers.Add("Content-Type", "text/event-stream");
|
||||
ctx.Response.Headers.Add("Cache-Control", "no-cache");
|
||||
ctx.Response.Headers.Add("Connection", "keep-alive");
|
||||
|
||||
ctx.Response.ChunkedTransfer = true;
|
||||
while (!Token.IsCancellationRequested && !Stopping)
|
||||
{
|
||||
await Task.Delay(10, Token);
|
||||
if (Token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (!OutboundQueue.TryDequeue(out var outgoingEvent))
|
||||
continue;
|
||||
|
||||
if (!await ctx.Response.SendChunk(outgoingEvent.Build(), false, Token))
|
||||
{
|
||||
Plugin.Log.Debug("SSE connection was unable to send new data");
|
||||
Plugin.Log.Debug($"Client disconnected: {ctx.Guid}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "SSE handler failed.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// "No Content" (204) didn't work for Firefox, so manually closing the connection on client side
|
||||
await ctx.Response.SendChunk(new CloseEvent().Build(), true, Token);
|
||||
|
||||
// Manually confirm that we have finished our connection, even if the final response failed
|
||||
// This can happen if the client disconnects before the server does
|
||||
ctx.Response.ResponseSent = true;
|
||||
|
||||
Done = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Stopping = true;
|
||||
|
||||
var timeout = 1000; // 1000ms
|
||||
while (timeout > 0)
|
||||
{
|
||||
if (Done)
|
||||
break;
|
||||
|
||||
timeout -= 100;
|
||||
await Task.Delay(100);
|
||||
Plugin.Log.Debug("Sleeping because EventServer still alive");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class ServerCore : IAsyncDisposable
|
||||
{
|
||||
public readonly Plugin Plugin;
|
||||
private readonly HostContext HostContext;
|
||||
|
||||
public ServerCore(Plugin plugin)
|
||||
{
|
||||
Plugin = plugin;
|
||||
HostContext = new HostContext(this);
|
||||
|
||||
Plugin.Framework.Update += FrameworkUpdate;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Plugin.Framework.Update -= FrameworkUpdate;
|
||||
await HostContext.DisposeAsync();
|
||||
}
|
||||
|
||||
private void FrameworkUpdate(IFramework _)
|
||||
{
|
||||
foreach (var (idx, tab) in Plugin.Config.Tabs.Index())
|
||||
{
|
||||
if (tab.Unread == tab.LastSendUnread)
|
||||
continue;
|
||||
|
||||
tab.LastSendUnread = tab.Unread;
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
eventServer.OutboundQueue.Enqueue(new ChatTabUnreadStateEvent(new ChatTabUnreadState(idx, tab.Unread)));
|
||||
}
|
||||
}
|
||||
|
||||
#region SSE Helper
|
||||
internal async Task PrepareNewClient(SSEConnection sse)
|
||||
{
|
||||
// This takes long, so keep it outside the next frame
|
||||
var messages = await HostContext.Processing.GetAllMessages();
|
||||
|
||||
// Using the bulk message event to clear everything on the client side that may still exist
|
||||
await Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
sse.OutboundQueue.Enqueue(new BulkMessagesEvent(messages));
|
||||
|
||||
sse.OutboundQueue.Enqueue(new SwitchChannelEvent(HostContext.Processing.GetCurrentChannel()));
|
||||
sse.OutboundQueue.Enqueue(new ChannelListEvent(HostContext.Processing.GetValidChannels()));
|
||||
|
||||
sse.OutboundQueue.Enqueue(new ChatTabSwitchedEvent(HostContext.Processing.GetCurrentTab()));
|
||||
sse.OutboundQueue.Enqueue(new ChatTabListEvent(HostContext.Processing.GetAllTabs()));
|
||||
});
|
||||
}
|
||||
|
||||
internal void SendNewMessage(Message message)
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
var bundledResponse = new NewMessageEvent(HostContext.Processing.ReadMessageContent(message));
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Sending message over SSE failed.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendBulkMessageList()
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
eventServer.OutboundQueue.Enqueue(new BulkMessagesEvent(new Messages(HostContext.Processing.ReadMessageList().Result)));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendChannelSwitch(Chunk[] channelName)
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
var bundledResponse = new SwitchChannelEvent(new SwitchChannel(HostContext.Processing.ReadChannelName(channelName)));
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendChannelList()
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
var bundledResponse = new ChannelListEvent(HostContext.Processing.GetValidChannels());
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendNewLogin()
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(async () =>
|
||||
{
|
||||
foreach (var eventServer in HostContext.EventConnections)
|
||||
await HostContext.Core.PrepareNewClient(eventServer);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Preparing all clients after login failed.");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void InvalidateSessions()
|
||||
{
|
||||
if (!HostContext.IsActive)
|
||||
return;
|
||||
|
||||
Plugin.Config.AuthStore.Clear();
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
|
||||
public bool IsActive()
|
||||
{
|
||||
return HostContext is { IsActive: true, Host.IsListening: true };
|
||||
}
|
||||
|
||||
public bool IsStopping()
|
||||
{
|
||||
return HostContext is { IsActive: false, IsStopping: true };
|
||||
}
|
||||
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
return HostContext.Start();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
HostContext.Run();
|
||||
}
|
||||
|
||||
public async ValueTask<bool> Stop()
|
||||
{
|
||||
return await HostContext.Stop();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public static class WebserverUtil
|
||||
{
|
||||
public static async Task<T> FrameworkWrapper<T>(Func<Task<T>> func)
|
||||
{
|
||||
return await Plugin.Framework.RunOnTick(func).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// From: https://github.com/NancyFx/Nancy/blob/master/src/Nancy/Request.cs#L176
|
||||
/// <summary>
|
||||
/// Gets the cookie data from the provided string if it exists
|
||||
/// </summary>
|
||||
/// <param name="cookieHeader">The string containing cookie data</param>
|
||||
/// <returns>Cookies dictionary</returns>
|
||||
public static Dictionary<string, string> GetCookieData(string cookieHeader)
|
||||
{
|
||||
var cookieDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (cookieHeader.Length == 0)
|
||||
return cookieDictionary;
|
||||
|
||||
var values = cookieHeader.TrimEnd(';').Split(';');
|
||||
foreach (var parts in values.Select(c => c.Split(['='], 2)))
|
||||
{
|
||||
var cookieName = parts[0].Trim();
|
||||
var cookieValue = parts.Length == 1 ? string.Empty : parts[1]; //Cookie attribute
|
||||
|
||||
cookieDictionary[cookieName] = cookieValue;
|
||||
}
|
||||
|
||||
return cookieDictionary;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,13 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed
|
||||
// message has been routed to all matching persistent tabs and stored
|
||||
// in the database. The AutoTellTabsService subscribes to spawn or
|
||||
// refresh temp tabs without having to wedge itself into ProcessMessage
|
||||
// directly.
|
||||
public event Action<Message>? MessageProcessed;
|
||||
|
||||
internal unsafe MessageManager(Plugin plugin)
|
||||
{
|
||||
Plugin = plugin;
|
||||
@@ -258,20 +265,16 @@ internal class MessageManager : IAsyncDisposable
|
||||
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
||||
Store.UpsertMessage(message);
|
||||
|
||||
var currentTabId = Plugin.CurrentTab.Identifier;
|
||||
var currentMatches = Plugin.CurrentTab.Matches(message);
|
||||
foreach (var tab in Plugin.Config.Tabs)
|
||||
{
|
||||
var unread = !(tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches);
|
||||
|
||||
if (tab.Matches(message))
|
||||
{
|
||||
tab.AddMessage(message, unread);
|
||||
}
|
||||
|
||||
if (tab.Identifier == currentTabId)
|
||||
Plugin.ServerCore.SendNewMessage(message);
|
||||
}
|
||||
}
|
||||
MessageProcessed?.Invoke(message);
|
||||
}
|
||||
|
||||
internal class NameFormatting
|
||||
|
||||
+150
-32
@@ -239,6 +239,9 @@ internal class MessageStore : IDisposable
|
||||
|
||||
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();
|
||||
cmd.CommandText = $"PRAGMA table_info({table});";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
@@ -298,8 +301,10 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
Plugin.Log.Information($"Setting version {version}");
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// Parameters aren't supported for PRAGMA queries, and you can't set the
|
||||
// version with a pragma_ function.
|
||||
// PRAGMA does not accept SQLite parameter bindings, and there is no
|
||||
// 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.ExecuteNonQuery();
|
||||
}
|
||||
@@ -346,31 +351,44 @@ internal class MessageStore : IDisposable
|
||||
throw new ArgumentOutOfRangeException(nameof(chatTypeDaysMap), "Negative retention is not allowed.");
|
||||
|
||||
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
|
||||
// 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)
|
||||
if (chatTypeDaysMap.Count == 0 && defaultDays <= 0)
|
||||
return 0;
|
||||
|
||||
long deleted;
|
||||
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.CommandTimeout = 600;
|
||||
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.");
|
||||
}
|
||||
|
||||
var inList = string.Join(",", allowedTypes);
|
||||
long deleted;
|
||||
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;
|
||||
deleted = cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -512,15 +530,16 @@ internal class MessageStore : IDisposable
|
||||
DateTimeOffset? from,
|
||||
DateTimeOffset? to)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
var clauses = new List<string> { "deleted = false" };
|
||||
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)
|
||||
clauses.Add("Date >= $From");
|
||||
if (to is not null)
|
||||
clauses.Add("Date <= $To");
|
||||
|
||||
var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
SELECT
|
||||
Id,
|
||||
@@ -602,6 +621,84 @@ internal class MessageStore : IDisposable
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hellion Chat — Auto-Tell-Tabs history preload.
|
||||
///
|
||||
/// Returns up to <paramref name="limit"/> tells exchanged with the named
|
||||
/// player, oldest-first, ready to be added to a freshly spawned auto
|
||||
/// tell tab. The Sender column is a serialized chunk blob, so SQL on its
|
||||
/// own cannot filter by player identity; we narrow with SQL on Receiver
|
||||
/// + ChatType (cheap, indexed) and let the client do the final
|
||||
/// PlayerPayload comparison on the result set.
|
||||
///
|
||||
/// <paramref name="sqlScanLimit"/> caps how many recent tells we scan
|
||||
/// before giving up. 500 covers around 10 days for an active greeter
|
||||
/// and stays well under the 20 ms budget required to keep the spawn on
|
||||
/// the message-processing worker thread.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<Message> GetTellHistoryWithSender(
|
||||
ulong receiver,
|
||||
string senderName,
|
||||
uint senderWorld,
|
||||
int limit,
|
||||
int sqlScanLimit = 500)
|
||||
{
|
||||
if (limit <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
FROM messages
|
||||
WHERE deleted = false
|
||||
AND Receiver = $Receiver
|
||||
AND ChatType IN ($TellIncoming, $TellOutgoing)
|
||||
ORDER BY Date DESC
|
||||
LIMIT $ScanLimit;
|
||||
";
|
||||
cmd.CommandTimeout = 60;
|
||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||
cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming);
|
||||
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
|
||||
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
|
||||
|
||||
var collected = new List<Message>();
|
||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader());
|
||||
foreach (var message in enumerator)
|
||||
{
|
||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collected.Add(message);
|
||||
if (collected.Count >= limit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// SQL was DESC (newest-first) so we hit the limit on the most
|
||||
// recent matching tells. Reverse to oldest-first for chronological
|
||||
// display in the tab.
|
||||
collected.Reverse();
|
||||
return collected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a message as deleted so it won't get returned in queries.
|
||||
/// </summary>
|
||||
@@ -615,16 +712,17 @@ internal class MessageStore : IDisposable
|
||||
|
||||
internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
||||
{
|
||||
using var cmd = Connection.CreateCommand();
|
||||
|
||||
List<string> whereClauses = ["deleted = false"];
|
||||
if (receiver != null)
|
||||
whereClauses.Add("Receiver = $Receiver");
|
||||
|
||||
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);
|
||||
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
@@ -644,16 +742,17 @@ internal class MessageStore : IDisposable
|
||||
|
||||
internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
List<string> whereClauses = ["deleted = false"];
|
||||
if (receiver != null)
|
||||
whereClauses.Add("Receiver = $Receiver");
|
||||
|
||||
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 cmd = Connection.CreateCommand();
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
@@ -685,16 +784,17 @@ internal class MessageStore : IDisposable
|
||||
|
||||
internal MessageEnumerator GetPagedDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null, int page = 0)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
List<string> whereClauses = ["deleted = false"];
|
||||
if (receiver != null)
|
||||
whereClauses.Add("Receiver = $Receiver");
|
||||
|
||||
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 cmd = Connection.CreateCommand();
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
@@ -724,10 +824,28 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||
cmd.Parameters.AddWithValue("OffsetCount", DbViewer.RowPerPage);
|
||||
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||
|
||||
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
|
||||
|
||||
@@ -150,9 +150,7 @@ public sealed class PayloadHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// ScreenshotMode changed, so we inform the webinterface about the new message format
|
||||
if (ImGui.Checkbox(Language.Context_ScreenshotMode, ref LogWindow.ScreenshotMode))
|
||||
LogWindow.Plugin.ServerCore.SendBulkMessageList();
|
||||
ImGui.Checkbox(Language.Context_ScreenshotMode, ref LogWindow.ScreenshotMode);
|
||||
|
||||
if (ImGui.Selectable(Language.Context_HideChat))
|
||||
LogWindow.UserHide();
|
||||
|
||||
+152
-57
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using ChatTwo.Http;
|
||||
using System.IO;
|
||||
using ChatTwo.Ipc;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Ui;
|
||||
@@ -58,15 +58,26 @@ public sealed class Plugin : IDalamudPlugin
|
||||
internal Commands Commands { get; }
|
||||
internal GameFunctions.GameFunctions Functions { get; }
|
||||
internal MessageManager MessageManager { get; }
|
||||
internal AutoTellTabsService AutoTellTabsService { get; }
|
||||
internal IpcManager Ipc { get; }
|
||||
internal ExtraChat ExtraChat { get; }
|
||||
internal TypingIpc TypingIpc { get; }
|
||||
internal FontManager FontManager { get; }
|
||||
|
||||
public readonly ServerCore ServerCore;
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
||||
// the manual button in the Privacy tab both run on background threads;
|
||||
// without this gate, hitting the manual button moments after a fresh
|
||||
// plugin start would launch two sweeps in parallel and the second one
|
||||
// would just re-do work the first one already finished. The lock guards
|
||||
// the flag — the flag check itself bails before we touch the database.
|
||||
// Volatile because the ImGui thread reads the flag outside the lock to
|
||||
// gate the manual button; without it the JIT may cache the value in a
|
||||
// register and miss the background-thread update.
|
||||
internal readonly object RetentionSweepLock = new();
|
||||
internal volatile bool RetentionSweepRunning;
|
||||
|
||||
internal DateTime GameStarted { get; }
|
||||
|
||||
// Tab management needs to happen outside the chatlog window class for access reasons
|
||||
@@ -94,65 +105,76 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO Remove after 01.07.2026
|
||||
// Migrate old channel values
|
||||
if (Config.Version <= 5)
|
||||
// Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig
|
||||
// already strips temp tabs before persistence, but a previous
|
||||
// crash or external write could have left them in the JSON.
|
||||
// Drop them on load to guarantee the session-only invariant.
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
|
||||
// layout starts from defaults instead of mapping every previous setting
|
||||
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
|
||||
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
|
||||
if (Config.Version < 10)
|
||||
{
|
||||
foreach (var tab in Config.Tabs)
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is not null)
|
||||
{
|
||||
if (tab.ChatCodes.Count > 0)
|
||||
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
|
||||
|
||||
try
|
||||
{
|
||||
tab.SelectedChannels = tab.ChatCodes.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
|
||||
tab.ChatCodes.Clear();
|
||||
if (File.Exists(liveConfigPath))
|
||||
{
|
||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.InactivityHideChannels.Count > 0)
|
||||
Config = new Configuration
|
||||
{
|
||||
Config.InactivityHideChannelsV2 = Config.InactivityHideChannels.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
|
||||
Config.InactivityHideChannels.Clear();
|
||||
}
|
||||
|
||||
Config.Version = 6;
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
// Hellion Chat v6→v7: seed Privacy-First defaults.
|
||||
if (Config.Version <= 6)
|
||||
{
|
||||
Config.PrivacyFilterEnabled = true;
|
||||
Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist];
|
||||
Config.PrivacyPersistUnknownChannels = false;
|
||||
// Existing ChatTwo users skip the first-run wizard — the
|
||||
// migration toast already explains what changed and they
|
||||
// can reopen the wizard from Settings → Privacy if they
|
||||
// want to pick a different profile.
|
||||
Config.FirstRunCompleted = true;
|
||||
Config.Version = 7;
|
||||
Version = 10,
|
||||
FirstRunCompleted = true,
|
||||
};
|
||||
SaveConfig();
|
||||
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = HellionStrings.Migration_Notification_Title,
|
||||
Content = HellionStrings.Migration_Notification_Content,
|
||||
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
||||
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
InitialDuration = TimeSpan.FromSeconds(15),
|
||||
InitialDuration = TimeSpan.FromSeconds(25),
|
||||
});
|
||||
}
|
||||
|
||||
// Hellion default tab layout for first-run and v10-wipe.
|
||||
// General catches player chat plus active gameplay events; the
|
||||
// System tab takes the technical noise so it does not bury real
|
||||
// conversation. Beginner tab only appears when the Novice
|
||||
// Network is enabled in Audio and Notifications, otherwise it
|
||||
// would just sit empty.
|
||||
if (Config.Tabs.Count == 0)
|
||||
{
|
||||
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
||||
Config.Tabs.Add(TabsUtil.HellionSystem);
|
||||
Config.Tabs.Add(TabsUtil.HellionFreeCompany);
|
||||
Config.Tabs.Add(TabsUtil.HellionParty);
|
||||
if (Config.ShowNoviceNetwork)
|
||||
Config.Tabs.Add(TabsUtil.HellionBeginner);
|
||||
Config.Tabs.Add(TabsUtil.HellionLinkshell);
|
||||
Config.Tabs.Add(TabsUtil.VanillaTellExclusive);
|
||||
}
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
|
||||
FileDialogManager = new FileDialogManager();
|
||||
|
||||
// Function call this in its ctor if the player is already logged in
|
||||
ServerCore = new ServerCore(this);
|
||||
|
||||
Commands = new Commands();
|
||||
Functions = new GameFunctions.GameFunctions(this);
|
||||
Ipc = new IpcManager();
|
||||
@@ -162,6 +184,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
MessageManager = new MessageManager(this); // Does it require UI?
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||
// MessageManager's MessageProcessed event for live tells and
|
||||
// to ClientState.Logout for the cleanup pass. Created after
|
||||
// MessageManager so the constructor can hand off the live
|
||||
// store and event source.
|
||||
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
||||
AutoTellTabsService.Initialize();
|
||||
|
||||
// Hellion Chat — daily retention sweep, off-thread so it never
|
||||
// blocks plugin load. Skips itself when disabled or already ran
|
||||
// within the past 24 hours.
|
||||
@@ -211,7 +241,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
||||
|
||||
if (Config.ShowEmotes)
|
||||
Task.Run(EmoteCache.LoadData);
|
||||
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
||||
|
||||
#if !DEBUG
|
||||
// Avoid 300ms hitch when sending first message by preloading the
|
||||
@@ -219,16 +249,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
// profiling difficult.
|
||||
AutoTranslate.PreloadCache();
|
||||
#endif
|
||||
|
||||
// Automatically start the webserver if requested
|
||||
if (Config.WebinterfaceAutoStart)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
ServerCore.Start();
|
||||
ServerCore.Run();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -262,17 +282,18 @@ public sealed class Plugin : IDalamudPlugin
|
||||
TypingIpc?.Dispose();
|
||||
ExtraChat?.Dispose();
|
||||
Ipc?.Dispose();
|
||||
// Dispose the Auto-Tell-Tabs service before MessageManager so it
|
||||
// can cleanly unsubscribe from the MessageProcessed event before
|
||||
// its source goes away.
|
||||
AutoTellTabsService?.Dispose();
|
||||
MessageManager?.DisposeAsync().AsTask().Wait();
|
||||
Functions?.Dispose();
|
||||
Commands?.Dispose();
|
||||
|
||||
EmoteCache.Dispose();
|
||||
ServerCore?.DisposeAsync().AsTask().Wait();
|
||||
}
|
||||
|
||||
private static void MigrateFromChatTwoLayout()
|
||||
{
|
||||
try
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is null)
|
||||
@@ -283,18 +304,36 @@ public sealed class Plugin : IDalamudPlugin
|
||||
var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json");
|
||||
var ourConfigDir = Interface.ConfigDirectory.FullName;
|
||||
|
||||
// Track whether anything legitimately blocked us. The most common
|
||||
// cause is upstream Chat 2 still being loaded — its SQLite handle
|
||||
// keeps chat-sqlite.db locked and File.Move throws IOException.
|
||||
var lockedBlocker = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(ourConfigFile) && File.Exists(legacyConfigFile))
|
||||
{
|
||||
File.Move(legacyConfigFile, ourConfigFile);
|
||||
Log.Information($"HellionChat: migrated config file {legacyConfigFile} → {ourConfigFile}");
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: config file move blocked, leaving {legacyConfigFile} in place");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
|
||||
// The plugin's ConfigDirectory may already exist on first load
|
||||
// (Dalamud creates it), so check at the file level instead of
|
||||
// skipping when the directory is present. Move every legacy
|
||||
// entry whose target name is not occupied yet, then remove the
|
||||
// source dir if it ends up empty.
|
||||
if (Directory.Exists(legacyConfigDir))
|
||||
// source dir if it ends up empty. Each move is wrapped on its
|
||||
// own so a single locked file (the SQLite db while ChatTwo still
|
||||
// runs) does not abandon the rest of the migration.
|
||||
if (!Directory.Exists(legacyConfigDir))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ourConfigDir);
|
||||
|
||||
@@ -303,18 +342,34 @@ public sealed class Plugin : IDalamudPlugin
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(file));
|
||||
if (File.Exists(target))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
File.Move(file, target);
|
||||
Log.Information($"HellionChat: migrated file {file} → {target}");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: file move blocked for {file}, will retry on next load");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.EnumerateDirectories(legacyConfigDir))
|
||||
{
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(dir));
|
||||
if (Directory.Exists(target))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
Directory.Move(dir, target);
|
||||
Log.Information($"HellionChat: migrated subdir {dir} → {target}");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: subdir move blocked for {dir}, will retry on next load");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(legacyConfigDir).Any())
|
||||
{
|
||||
@@ -322,11 +377,26 @@ public sealed class Plugin : IDalamudPlugin
|
||||
Log.Information($"HellionChat: removed empty legacy dir {legacyConfigDir}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "HellionChat: layout migration failed, continuing with whatever exists");
|
||||
}
|
||||
|
||||
if (lockedBlocker)
|
||||
{
|
||||
// Surface the most common cause to the user as a notification
|
||||
// so they don't think Hellion Chat lost their history when in
|
||||
// fact upstream Chat 2 was still holding the database file.
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content = "Could not migrate the Chat 2 database — the file appears to be in use. " +
|
||||
"Disable Chat 2, fully close the game, then start it again. " +
|
||||
"See the README troubleshooting section if the issue persists.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(30),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenMainUi()
|
||||
@@ -356,6 +426,16 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
// Bail out cheaply if a manual sweep is already in flight; the
|
||||
// lock around the actual work would queue us up otherwise and
|
||||
// we would just re-do whatever the manual run already did.
|
||||
lock (RetentionSweepLock)
|
||||
{
|
||||
if (RetentionSweepRunning)
|
||||
return;
|
||||
RetentionSweepRunning = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
||||
@@ -380,6 +460,11 @@ public sealed class Plugin : IDalamudPlugin
|
||||
{
|
||||
Log.Error(e, "Retention sweep failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (RetentionSweepLock)
|
||||
RetentionSweepRunning = false;
|
||||
}
|
||||
}) { IsBackground = true }.Start();
|
||||
}
|
||||
|
||||
@@ -418,7 +503,17 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
internal void SaveConfig()
|
||||
{
|
||||
// Hellion Chat — Auto-Tell-Tabs are session-only. Strip them out
|
||||
// before serialization so a crash mid-session can never persist
|
||||
// them. We snapshot the full tab list first and restore it after
|
||||
// the save, preserving the user's order and open conversations.
|
||||
var snapshot = Config.Tabs.ToList();
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
Interface.SavePluginConfig(Config);
|
||||
|
||||
Config.Tabs.Clear();
|
||||
Config.Tabs.AddRange(snapshot);
|
||||
}
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
|
||||
+108
-4
@@ -44,6 +44,8 @@ internal class HellionStrings
|
||||
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
|
||||
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
|
||||
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
|
||||
internal static string Privacy_FilterEnabled_StorageOnly_Help => Get(nameof(Privacy_FilterEnabled_StorageOnly_Help));
|
||||
internal static string Privacy_Filter_Tree_Heading => Get(nameof(Privacy_Filter_Tree_Heading));
|
||||
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
|
||||
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
|
||||
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
|
||||
@@ -62,6 +64,8 @@ internal class HellionStrings
|
||||
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
|
||||
internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro));
|
||||
internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote));
|
||||
internal static string Cleanup_Preview_Stale => Get(nameof(Cleanup_Preview_Stale));
|
||||
internal static string Retention_Help_SavedNote => Get(nameof(Retention_Help_SavedNote));
|
||||
internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview));
|
||||
internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview));
|
||||
internal static string Cleanup_TotalStored => Get(nameof(Cleanup_TotalStored));
|
||||
@@ -97,9 +101,6 @@ internal class HellionStrings
|
||||
internal static string Retention_Success => Get(nameof(Retention_Success));
|
||||
internal static string Retention_Error => Get(nameof(Retention_Error));
|
||||
|
||||
internal static string Migration_Notification_Title => Get(nameof(Migration_Notification_Title));
|
||||
internal static string Migration_Notification_Content => Get(nameof(Migration_Notification_Content));
|
||||
|
||||
internal static string Wizard_Title => Get(nameof(Wizard_Title));
|
||||
internal static string Wizard_Intro => Get(nameof(Wizard_Intro));
|
||||
internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading));
|
||||
@@ -131,11 +132,114 @@ internal class HellionStrings
|
||||
internal static string Export_Empty => Get(nameof(Export_Empty));
|
||||
internal static string Export_Error => Get(nameof(Export_Error));
|
||||
|
||||
internal static string Theme_Heading => Get(nameof(Theme_Heading));
|
||||
internal static string Theme_Enabled_Name => Get(nameof(Theme_Enabled_Name));
|
||||
internal static string Theme_Enabled_Description => Get(nameof(Theme_Enabled_Description));
|
||||
internal static string Theme_WindowOpacity_Label => Get(nameof(Theme_WindowOpacity_Label));
|
||||
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
|
||||
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
|
||||
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
|
||||
|
||||
internal static string About_Maintainer_Heading => Get(nameof(About_Maintainer_Heading));
|
||||
internal static string About_Maintainer_Body => Get(nameof(About_Maintainer_Body));
|
||||
internal static string About_Maintainer_Website_Label => Get(nameof(About_Maintainer_Website_Label));
|
||||
internal static string About_Mission_Heading => Get(nameof(About_Mission_Heading));
|
||||
internal static string About_Mission_P1 => Get(nameof(About_Mission_P1));
|
||||
internal static string About_Mission_P2 => Get(nameof(About_Mission_P2));
|
||||
internal static string About_Mission_P3 => Get(nameof(About_Mission_P3));
|
||||
internal static string About_BuiltOn_Heading => Get(nameof(About_BuiltOn_Heading));
|
||||
internal static string About_BuiltOn_P1 => Get(nameof(About_BuiltOn_P1));
|
||||
internal static string About_BuiltOn_P2 => Get(nameof(About_BuiltOn_P2));
|
||||
internal static string About_BuiltOn_Upstream_Label => Get(nameof(About_BuiltOn_Upstream_Label));
|
||||
internal static string About_License_Heading => Get(nameof(About_License_Heading));
|
||||
internal static string About_License_P1 => Get(nameof(About_License_P1));
|
||||
internal static string About_License_P2 => Get(nameof(About_License_P2));
|
||||
internal static string About_License_P3 => Get(nameof(About_License_P3));
|
||||
internal static string About_SE_Heading => Get(nameof(About_SE_Heading));
|
||||
internal static string About_SE_P1 => Get(nameof(About_SE_P1));
|
||||
internal static string About_SE_P2 => Get(nameof(About_SE_P2));
|
||||
internal static string About_Localization_Heading => Get(nameof(About_Localization_Heading));
|
||||
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
|
||||
internal static string About_Localization_P2 => Get(nameof(About_Localization_P2));
|
||||
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs runtime strings
|
||||
internal static string AutoTellTabs_SectionHeader => Get(nameof(AutoTellTabs_SectionHeader));
|
||||
internal static string AutoTellTabs_HistorySeparator => Get(nameof(AutoTellTabs_HistorySeparator));
|
||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||
internal static string ChatLog_AutoTellTabs_Enable_Name => Get(nameof(ChatLog_AutoTellTabs_Enable_Name));
|
||||
internal static string ChatLog_AutoTellTabs_Enable_Description => Get(nameof(ChatLog_AutoTellTabs_Enable_Description));
|
||||
internal static string ChatLog_AutoTellTabs_Limit_Name => Get(nameof(ChatLog_AutoTellTabs_Limit_Name));
|
||||
internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description));
|
||||
internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name));
|
||||
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description));
|
||||
internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name));
|
||||
internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description));
|
||||
internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
|
||||
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Privacy settings tab
|
||||
internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title));
|
||||
internal static string Privacy_AutoTellTabs_Preload_Name => Get(nameof(Privacy_AutoTellTabs_Preload_Name));
|
||||
internal static string Privacy_AutoTellTabs_Preload_Description => Get(nameof(Privacy_AutoTellTabs_Preload_Description));
|
||||
internal static string Privacy_AutoTellTabs_Preload_Hint => Get(nameof(Privacy_AutoTellTabs_Preload_Hint));
|
||||
|
||||
// Hellion Chat — Settings UX Polish v10 wipe migration
|
||||
internal static string SettingsRefactor_Migration_Title => Get(nameof(SettingsRefactor_Migration_Title));
|
||||
internal static string SettingsRefactor_Migration_Content => Get(nameof(SettingsRefactor_Migration_Content));
|
||||
|
||||
// Hellion Chat — Settings UX Polish 8-tab structure
|
||||
internal static string Settings_Tab_General => Get(nameof(Settings_Tab_General));
|
||||
internal static string Settings_Tab_Appearance => Get(nameof(Settings_Tab_Appearance));
|
||||
internal static string Settings_Tab_Window => Get(nameof(Settings_Tab_Window));
|
||||
internal static string Settings_Tab_Chat => Get(nameof(Settings_Tab_Chat));
|
||||
internal static string Settings_Tab_Tabs => Get(nameof(Settings_Tab_Tabs));
|
||||
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
|
||||
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
||||
|
||||
// Hellion Chat — General-Tab section headings
|
||||
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
|
||||
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
|
||||
internal static string Settings_General_Performance_Heading => Get(nameof(Settings_General_Performance_Heading));
|
||||
internal static string Settings_General_Language_Heading => Get(nameof(Settings_General_Language_Heading));
|
||||
|
||||
// Hellion Chat — Appearance-Tab section headings
|
||||
internal static string Settings_Appearance_Theme_Heading => Get(nameof(Settings_Appearance_Theme_Heading));
|
||||
internal static string Settings_Appearance_Fonts_Heading => Get(nameof(Settings_Appearance_Fonts_Heading));
|
||||
internal static string Settings_Appearance_Colours_Heading => Get(nameof(Settings_Appearance_Colours_Heading));
|
||||
internal static string Settings_Appearance_Timestamps_Heading => Get(nameof(Settings_Appearance_Timestamps_Heading));
|
||||
|
||||
// Hellion Chat — Window-Tab section headings
|
||||
internal static string Settings_Window_Hide_Heading => Get(nameof(Settings_Window_Hide_Heading));
|
||||
internal static string Settings_Window_InactivityHide_Heading => Get(nameof(Settings_Window_InactivityHide_Heading));
|
||||
internal static string Settings_Window_Frame_Heading => Get(nameof(Settings_Window_Frame_Heading));
|
||||
internal static string Settings_Window_Tooltips_Heading => Get(nameof(Settings_Window_Tooltips_Heading));
|
||||
|
||||
// Hellion Chat — Chat-Tab section headings
|
||||
internal static string Settings_Chat_AutoTellTabs_Heading => Get(nameof(Settings_Chat_AutoTellTabs_Heading));
|
||||
internal static string Settings_Chat_Behaviour_Heading => Get(nameof(Settings_Chat_Behaviour_Heading));
|
||||
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
|
||||
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
|
||||
|
||||
// Hellion Chat — Database-Tab section headings
|
||||
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
|
||||
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
|
||||
internal static string Settings_Database_Stats_Heading => Get(nameof(Settings_Database_Stats_Heading));
|
||||
|
||||
// Hellion Chat — Information-Tab section headings
|
||||
internal static string Settings_Information_VersionInfo_Heading => Get(nameof(Settings_Information_VersionInfo_Heading));
|
||||
internal static string Settings_Information_About_Heading => Get(nameof(Settings_Information_About_Heading));
|
||||
internal static string Settings_Information_Changelog_Heading => Get(nameof(Settings_Information_Changelog_Heading));
|
||||
|
||||
// Hellion Chat — Default tab presets (channel-themed)
|
||||
internal static string Tabs_Presets_System => Get(nameof(Tabs_Presets_System));
|
||||
internal static string Tabs_Presets_FreeCompany => Get(nameof(Tabs_Presets_FreeCompany));
|
||||
internal static string Tabs_Presets_Party => Get(nameof(Tabs_Presets_Party));
|
||||
internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner));
|
||||
internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell));
|
||||
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
||||
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value>
|
||||
</data>
|
||||
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
||||
<value>Der Filter steuert nur, was in die lokale Datenbank geschrieben wird. Im Chat-Log siehst du weiterhin jede Nachricht live, ausgeschlossene Kanäle werden nur nicht mehr gespeichert. Wenn du Kanäle auch aus der sichtbaren Anzeige entfernen willst, nutze die normalen Chat-Tab-Filter im Spiel.</value>
|
||||
</data>
|
||||
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
|
||||
<value>Privacy-Filter und Whitelist</value>
|
||||
</data>
|
||||
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
||||
<value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value>
|
||||
</data>
|
||||
@@ -72,6 +78,12 @@
|
||||
<data name="Cleanup_Help_SavedNote" xml:space="preserve">
|
||||
<value>Das Aufräumen nutzt deine GESPEICHERTE Whitelist (Plugin.Config), nicht ungespeicherte Änderungen oben. Klicke zuerst Speichern, wenn du deine aktuellen Änderungen anwenden willst.</value>
|
||||
</data>
|
||||
<data name="Retention_Help_SavedNote" xml:space="preserve">
|
||||
<value>Der manuelle Lauf nutzt deine GESPEICHERTE Retention-Policy, nicht die Slider-Werte oben. Klicke zuerst Speichern, wenn der Lauf deine aktuellen Änderungen anwenden soll.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Preview_Stale" xml:space="preserve">
|
||||
<value>Vorschau veraltet, deine Whitelist hat sich seit dem letzten Aktualisieren geändert. Klicke Aktualisieren, um neu zu berechnen.</value>
|
||||
</data>
|
||||
<data name="Cleanup_RefreshPreview" xml:space="preserve">
|
||||
<value>Vorschau aktualisieren</value>
|
||||
</data>
|
||||
@@ -171,12 +183,6 @@
|
||||
<data name="Retention_Error" xml:space="preserve">
|
||||
<value>Aufbewahrungs-Bereinigung fehlgeschlagen, siehe /xllog</value>
|
||||
</data>
|
||||
<data name="Migration_Notification_Title" xml:space="preserve">
|
||||
<value>Hellion Chat</value>
|
||||
</data>
|
||||
<data name="Migration_Notification_Content" xml:space="preserve">
|
||||
<value>Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen.</value>
|
||||
</data>
|
||||
<data name="Wizard_Title" xml:space="preserve">
|
||||
<value>Hellion Chat — Willkommen</value>
|
||||
</data>
|
||||
@@ -264,14 +270,11 @@
|
||||
<data name="Export_Error" xml:space="preserve">
|
||||
<value>Export fehlgeschlagen, siehe /xllog</value>
|
||||
</data>
|
||||
<data name="Theme_Heading" xml:space="preserve">
|
||||
<value>Erscheinungsbild</value>
|
||||
</data>
|
||||
<data name="Theme_Enabled_Name" xml:space="preserve">
|
||||
<value>Hellion-Theme für alle Plugin-Fenster verwenden</value>
|
||||
</data>
|
||||
<data name="Theme_Enabled_Description" xml:space="preserve">
|
||||
<value>Industrielle HUD-Palette mit cyan-blauen Aktionsfarben, schiefer-violetten Tabs und Bernstein-Akzenten für aktive Zustände, global angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value>
|
||||
<value>Hellion-Online-Media-Palette aus Arctic Cyan und Ember Orange, angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value>
|
||||
</data>
|
||||
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
||||
<value>Fenster-Deckkraft</value>
|
||||
@@ -285,4 +288,271 @@
|
||||
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
||||
<value>Rendert Chat und UI in Exo 2 (SIL Open Font License 1.1), die mit dem Plugin ausgeliefert wird. Deaktivieren, um auf die unter Einstellungen → Schrift gewählte Schriftart zurückzufallen.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Maintainer_Heading" xml:space="preserve">
|
||||
<value>Maintainer</value>
|
||||
</data>
|
||||
<data name="About_Maintainer_Body" xml:space="preserve">
|
||||
<value>Ich pflege Hellion Chat über Hellion Online Media. Auf der Website findest du die Kontaktdaten für lizenzrechtliche, rechtliche oder geschäftliche Fragen.</value>
|
||||
</data>
|
||||
<data name="About_Maintainer_Website_Label" xml:space="preserve">
|
||||
<value>Website:</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Mission_Heading" xml:space="preserve">
|
||||
<value>Warum es diesen Fork gibt</value>
|
||||
</data>
|
||||
<data name="About_Mission_P1" xml:space="preserve">
|
||||
<value>Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie, die für Filter, Suche und Replay zur Verfügung steht. Dieser Default ist für die meisten Nutzer der richtige. Dieser Fork wählt einen anderen Ansatz: einen kleineren Default-Footprint, mit zusätzlichen Stellschrauben für Nutzer, die weniger fremden Chat auf der Festplatte behalten möchten.</value>
|
||||
</data>
|
||||
<data name="About_Mission_P2" xml:space="preserve">
|
||||
<value>Der Wunsch nach diesem engeren Default war persönlich. Nach zwei Jahren mit Chat 2 lag meine Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von Fremden in Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie gerne. Mein eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut.</value>
|
||||
</data>
|
||||
<data name="About_Mission_P3" xml:space="preserve">
|
||||
<value>Ich strebe keine große Zielgruppe an, und der Fork steht nicht in Konkurrenz zu Chat 2. Der Code liegt offen unter derselben EUPL-1.2-Lizenz wie das Original. Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder das Projekt einfach ignorieren. Alles drei ist für mich in Ordnung.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_BuiltOn_Heading" xml:space="preserve">
|
||||
<value>Aufbauend auf Chat 2</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_P1" xml:space="preserve">
|
||||
<value>Hellion Chat ist ein Fork von Chat 2 von Infi und Anna (ascclemens). Das Chat-Replacement-Fenster, die IPC-Integration, die Render-Engine und der komplette Storage-Kern stammen aus dem Original.</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_P2" xml:space="preserve">
|
||||
<value>Das Webinterface ist das einzige größere Teil, das ich entfernt habe. Es ist für den Remote-Zugriff auf den Chat von einem zweiten Gerät gebaut, also für einen anderen Fokus als der kleinere Default-Footprint, den dieser Fork verfolgt. Es an diese Defaults anzupassen hätte einen erheblichen Umbau bedeutet, also war die Entfernung der saubere Weg für genau diesen Fork.</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
|
||||
<value>Upstream-Repository:</value>
|
||||
</data>
|
||||
|
||||
<data name="About_License_Heading" xml:space="preserve">
|
||||
<value>Lizenz</value>
|
||||
</data>
|
||||
<data name="About_License_P1" xml:space="preserve">
|
||||
<value>Hellion Chat und Chat 2 stehen beide unter der European Union Public Licence v1.2 (EUPL-1.2).</value>
|
||||
</data>
|
||||
<data name="About_License_P2" xml:space="preserve">
|
||||
<value>© 2023 bis 2026, die Chat-2-Autoren (Infi, Anna und die Upstream-Mitwirkenden).</value>
|
||||
</data>
|
||||
<data name="About_License_P3" xml:space="preserve">
|
||||
<value>© 2026 Hellion Online Media für die Erweiterungen in diesem Fork.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_SE_Heading" xml:space="preserve">
|
||||
<value>FINAL FANTASY XIV-Hinweis</value>
|
||||
</data>
|
||||
<data name="About_SE_P1" xml:space="preserve">
|
||||
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. Alle Rechte vorbehalten.</value>
|
||||
</data>
|
||||
<data name="About_SE_P2" xml:space="preserve">
|
||||
<value>Hellion Chat ist ein inoffizielles Fan-Plugin. Es steht in keiner Verbindung zu Square Enix und wird von ihnen weder unterstützt, gesponsert noch genehmigt.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Localization_Heading" xml:space="preserve">
|
||||
<value>Lokalisierung</value>
|
||||
</data>
|
||||
<data name="About_Localization_P1" xml:space="preserve">
|
||||
<value>Die deutschen Übersetzungen der Hellion-spezifischen Strings stammen von mir. Weitere Sprachen sind aktuell nicht verfügbar.</value>
|
||||
</data>
|
||||
<data name="About_Localization_P2" xml:space="preserve">
|
||||
<value>Die Übersetzerliste weiter unten gehört zu den Chat-2-Strings auf Crowdin. Diese Freiwilligen haben Chat 2 übersetzt, nicht die Hellion-Erweiterungen.</value>
|
||||
</data>
|
||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||
<value>Chat-2-Community-Übersetzer (Upstream)</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Runtime-Strings) -->
|
||||
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
|
||||
<value>Aktive Tells</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
|
||||
<value>— Frühere Unterhaltungen —</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
|
||||
<value>Verlauf konnte nicht geladen werden.</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
|
||||
<value>Als begrüßt markiert. Klicken um die Markierung zu entfernen.</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Als begrüßt markieren.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
<value>Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
|
||||
<value>Bei jedem /tell automatisch einen Tab pro Gesprächspartner öffnen</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
|
||||
<value>Sobald du einen /tell empfängst oder sendest, wird automatisch ein temporärer Tab für diesen Spieler geöffnet. Die Tabs verschwinden beim Logout.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
|
||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Kompakte Anzeige</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
|
||||
<value>Zeigt nur einen dünnen Separator zwischen normalen Tabs und Auto-Tell-Tabs, ohne Sektions-Header.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
|
||||
<value>„Als begrüßt markieren"-Button anzeigen</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
|
||||
<value>Fügt neben jedem Auto-Tell-Tab einen Klick-Button hinzu, um einen Gesprächspartner als bereits begrüßt zu markieren — der Tab-Name wird dann gedimmt. Nützlich für Club-Greeter, die parallel viele Konversationen führen. Standardmäßig aus.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
|
||||
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
|
||||
<value>Hinweis: Falls XIV Messanger oder ein ähnliches Plugin Tells unterdrückt, dort die Option „Suppress DMs" deaktivieren, damit Hellion Chat Tells empfangen und die Auto-Tabs öffnen kann.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Datenschutz-Einstellungstab) -->
|
||||
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
<value>Tell-Verlauf in Auto-Tabs</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
|
||||
<value>Anzahl der vorgeladenen Tells</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
|
||||
<value>Wie viele frühere Tell-Nachrichten beim Öffnen eines Auto-Tell-Tabs aus der Datenbank geladen werden. 0 deaktiviert die Vorladung.</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
|
||||
<value>Greift nur, wenn Auto-Tell-Tabs im Chat-Tab aktiviert sind.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Settings UX Polish v10 Wipe-Migration -->
|
||||
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
|
||||
<value>Settings umstrukturiert</value>
|
||||
</data>
|
||||
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
|
||||
<value>Hellion Chat 0.5.0 hat die Settings in thematische Tabs umstrukturiert. Deine Chat-Datenbank und dein Nachrichtenverlauf bleiben unverändert. Settings wurden auf Defaults zurückgesetzt. Falls du das Privacy-Profil neu wählen willst, findest du den Reopen-Button im Datenschutz-Tab. Ein Backup der vorherigen Config liegt unter HellionChat.json.pre-v10-backup neben der aktiven Config-Datei.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Settings UX Polish 8-Tab-Struktur -->
|
||||
<data name="Settings_Tab_General" xml:space="preserve">
|
||||
<value>Allgemein</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Appearance" xml:space="preserve">
|
||||
<value>Aussehen</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Window" xml:space="preserve">
|
||||
<value>Fenster</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Chat" xml:space="preserve">
|
||||
<value>Chat</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Tabs" xml:space="preserve">
|
||||
<value>Kanäle</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Database" xml:space="preserve">
|
||||
<value>Datenbank</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Information" xml:space="preserve">
|
||||
<value>Über</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Allgemein-Tabs -->
|
||||
<data name="Settings_General_Input_Heading" xml:space="preserve">
|
||||
<value>Eingabe</value>
|
||||
</data>
|
||||
<data name="Settings_General_Audio_Heading" xml:space="preserve">
|
||||
<value>Audio & Benachrichtigungen</value>
|
||||
</data>
|
||||
<data name="Settings_General_Performance_Heading" xml:space="preserve">
|
||||
<value>Performance</value>
|
||||
</data>
|
||||
<data name="Settings_General_Language_Heading" xml:space="preserve">
|
||||
<value>Sprache & Eingabe-Hilfen</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Aussehen-Tabs -->
|
||||
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
|
||||
<value>Schriftarten</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
|
||||
<value>Chat-Farben</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
|
||||
<value>Zeitstempel</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Fenster-Tabs -->
|
||||
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
|
||||
<value>Verstecken</value>
|
||||
</data>
|
||||
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
|
||||
<value>Inaktivitäts-Verstecken</value>
|
||||
</data>
|
||||
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
|
||||
<value>Fenster-Rahmen</value>
|
||||
</data>
|
||||
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
|
||||
<value>Tooltips</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Chat-Tabs -->
|
||||
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
|
||||
<value>Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
|
||||
<value>Nachrichten-Verhalten</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
|
||||
<value>Vorschau</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
|
||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||
<value>Speicherung</value>
|
||||
</data>
|
||||
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
|
||||
<value>Übersicht</value>
|
||||
</data>
|
||||
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
|
||||
<value>Wartung</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Sektions-Überschriften des Information-Tabs -->
|
||||
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
|
||||
<value>Versionsinfo</value>
|
||||
</data>
|
||||
<data name="Settings_Information_About_Heading" xml:space="preserve">
|
||||
<value>Über HellionChat</value>
|
||||
</data>
|
||||
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
|
||||
<value>Changelog</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Default-Tab-Presets (kanalspezifisch) -->
|
||||
<data name="Tabs_Presets_System" xml:space="preserve">
|
||||
<value>System</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
|
||||
<value>Free Company</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Party" xml:space="preserve">
|
||||
<value>Gruppe</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Beginner" xml:space="preserve">
|
||||
<value>Neulinge</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
|
||||
<value>Linkshell</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
|
||||
<value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
||||
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value>
|
||||
</data>
|
||||
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
||||
<value>The filter only controls what is written to the local database. The chat log itself keeps showing every message live, disallowed channels just stop being saved. Use the channel hide options in your in-game chat tabs if you want to remove channels from the visible chat.</value>
|
||||
</data>
|
||||
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
|
||||
<value>Privacy filter and whitelist</value>
|
||||
</data>
|
||||
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
||||
<value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value>
|
||||
</data>
|
||||
@@ -72,6 +78,12 @@
|
||||
<data name="Cleanup_Help_SavedNote" xml:space="preserve">
|
||||
<value>Cleanup uses your SAVED whitelist (Plugin.Config), not unsaved edits above. Click Save first if you want to apply your current edits.</value>
|
||||
</data>
|
||||
<data name="Retention_Help_SavedNote" xml:space="preserve">
|
||||
<value>The manual sweep uses your SAVED retention policy, not the slider values above. Click Save first if you want the run to apply your current edits.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Preview_Stale" xml:space="preserve">
|
||||
<value>Preview is out of date — your whitelist has changed since the last refresh. Click Refresh to recalculate.</value>
|
||||
</data>
|
||||
<data name="Cleanup_RefreshPreview" xml:space="preserve">
|
||||
<value>Refresh preview</value>
|
||||
</data>
|
||||
@@ -121,7 +133,7 @@
|
||||
<value>Auto-delete messages after a per-channel retention window</value>
|
||||
</data>
|
||||
<data name="Retention_Enabled_Description" xml:space="preserve">
|
||||
<value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default — the plugin never deletes history without your explicit consent.</value>
|
||||
<value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default. The plugin never deletes history without your explicit consent.</value>
|
||||
</data>
|
||||
<data name="Retention_Default_Label" xml:space="preserve">
|
||||
<value>Default retention (days, 0 = never)</value>
|
||||
@@ -171,12 +183,6 @@
|
||||
<data name="Retention_Error" xml:space="preserve">
|
||||
<value>Retention sweep failed, see /xllog</value>
|
||||
</data>
|
||||
<data name="Migration_Notification_Title" xml:space="preserve">
|
||||
<value>Hellion Chat</value>
|
||||
</data>
|
||||
<data name="Migration_Notification_Content" xml:space="preserve">
|
||||
<value>Privacy filter activated by default. Settings → Privacy to adjust.</value>
|
||||
</data>
|
||||
<data name="Wizard_Title" xml:space="preserve">
|
||||
<value>Hellion Chat — Welcome</value>
|
||||
</data>
|
||||
@@ -264,14 +270,11 @@
|
||||
<data name="Export_Error" xml:space="preserve">
|
||||
<value>Export failed, see /xllog</value>
|
||||
</data>
|
||||
<data name="Theme_Heading" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="Theme_Enabled_Name" xml:space="preserve">
|
||||
<value>Use the Hellion theme across all plugin windows</value>
|
||||
</data>
|
||||
<data name="Theme_Enabled_Description" xml:space="preserve">
|
||||
<value>Industrial HUD palette with cyan-teal action accents, slate-violet tabs and amber active highlights, applied globally to chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value>
|
||||
<value>Hellion Online Media palette of Arctic Cyan plus Ember Orange, applied across the chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value>
|
||||
</data>
|
||||
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
||||
<value>Window opacity</value>
|
||||
@@ -285,4 +288,271 @@
|
||||
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
||||
<value>Renders chat and UI in Exo 2 (SIL Open Font License 1.1) which ships with the plugin. Disable to fall back to whatever font you picked under Settings → Fonts.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Maintainer_Heading" xml:space="preserve">
|
||||
<value>Maintainer</value>
|
||||
</data>
|
||||
<data name="About_Maintainer_Body" xml:space="preserve">
|
||||
<value>I maintain Hellion Chat through Hellion Online Media. The website has the contact details for licensing, legal or business questions.</value>
|
||||
</data>
|
||||
<data name="About_Maintainer_Website_Label" xml:space="preserve">
|
||||
<value>Website:</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Mission_Heading" xml:space="preserve">
|
||||
<value>Why this fork exists</value>
|
||||
</data>
|
||||
<data name="About_Mission_P1" xml:space="preserve">
|
||||
<value>Hellion Chat is not trying to replace Chat 2. Chat 2 ships a complete chat experience with full history available for filtering, search and replay. That default is the right one for most users. This fork takes a different stance: a smaller default footprint, with extra knobs for users who want to keep less third-party chat on disk.</value>
|
||||
</data>
|
||||
<data name="About_Mission_P2" xml:space="preserve">
|
||||
<value>The reason I wanted that narrower default was personal. After two years on Chat 2 my database had grown past two million messages, most of them /say, /shout and /yell from strangers in Limsa. That data is exactly what makes Chat 2's full-history view powerful and most users are happy to keep it. For my own taste I wanted a smaller default. So I built this fork.</value>
|
||||
</data>
|
||||
<data name="About_Mission_P3" xml:space="preserve">
|
||||
<value>I am not chasing a big audience and the fork is not in competition with Chat 2. The code is open under the same EUPL-1.2 licence as the upstream plugin. Infi, Anna or anyone else are welcome to read it, borrow ideas, ask questions, or ignore the project. All three are fine by me.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_BuiltOn_Heading" xml:space="preserve">
|
||||
<value>Built on Chat 2</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_P1" xml:space="preserve">
|
||||
<value>Hellion Chat is a fork of Chat 2 by Infi and Anna (ascclemens). The chat replacement window, the IPC integration, the rendering engine and the entire storage core come from upstream Chat 2.</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_P2" xml:space="preserve">
|
||||
<value>The webinterface is the only major piece I removed. It is built for remote access to chat from a second device, which is a different focus than the smaller default footprint this fork is built around. Aligning it with these defaults would have meant a substantial rebuild, so removing it was the cleaner path for this particular fork.</value>
|
||||
</data>
|
||||
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
|
||||
<value>Upstream repository:</value>
|
||||
</data>
|
||||
|
||||
<data name="About_License_Heading" xml:space="preserve">
|
||||
<value>License</value>
|
||||
</data>
|
||||
<data name="About_License_P1" xml:space="preserve">
|
||||
<value>Hellion Chat and Chat 2 both ship under the European Union Public Licence v1.2 (EUPL-1.2).</value>
|
||||
</data>
|
||||
<data name="About_License_P2" xml:space="preserve">
|
||||
<value>© 2023 to 2026, the Chat 2 authors (Infi, Anna and the upstream contributors).</value>
|
||||
</data>
|
||||
<data name="About_License_P3" xml:space="preserve">
|
||||
<value>© 2026 Hellion Online Media for the additions made in this fork.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_SE_Heading" xml:space="preserve">
|
||||
<value>FINAL FANTASY XIV disclaimer</value>
|
||||
</data>
|
||||
<data name="About_SE_P1" xml:space="preserve">
|
||||
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved.</value>
|
||||
</data>
|
||||
<data name="About_SE_P2" xml:space="preserve">
|
||||
<value>Hellion Chat is an unofficial, fan-made plugin. It has no affiliation with Square Enix and is not endorsed, sponsored or approved by them.</value>
|
||||
</data>
|
||||
|
||||
<data name="About_Localization_Heading" xml:space="preserve">
|
||||
<value>Localization</value>
|
||||
</data>
|
||||
<data name="About_Localization_P1" xml:space="preserve">
|
||||
<value>The German translations of the Hellion-specific strings come from me. Other languages are not provided yet.</value>
|
||||
</data>
|
||||
<data name="About_Localization_P2" xml:space="preserve">
|
||||
<value>The translator list below covers the upstream Chat 2 strings on Crowdin. Those volunteers translated Chat 2, not the Hellion additions.</value>
|
||||
</data>
|
||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||
<value>Chat 2 community translators (upstream)</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (runtime strings) -->
|
||||
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
|
||||
<value>Active Tells</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
|
||||
<value>— Earlier conversations —</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
|
||||
<value>History could not be loaded.</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
|
||||
<value>Marked as greeted. Click to remove the marker.</value>
|
||||
</data>
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Mark as greeted.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
<value>Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
|
||||
<value>Open a tab automatically for each tell partner</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
|
||||
<value>When you receive or send a /tell, a temporary tab dedicated to that player is opened automatically. Tabs vanish on logout.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
|
||||
<value>Maximum number of auto tell tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>When the limit is reached, greeted tabs with the oldest activity are dropped first. The change applies on the next /tell.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Compact display</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
|
||||
<value>Show only a thin separator between persistent tabs and auto tell tabs, without the section header.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
|
||||
<value>Show "mark as greeted" button</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
|
||||
<value>Adds a click-to-toggle button next to each auto tell tab to mark a partner as already greeted, dimming the tab name when set. Useful for club greeters tracking many parallel conversations; off by default.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
|
||||
<value>The number of preloaded tells is configured in the Privacy tab.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
|
||||
<value>Heads-up: if XIV Messanger or a similar plugin is suppressing direct messages, turn its "Suppress DMs" option off so Hellion Chat can receive tells and open the auto tabs.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Privacy settings tab) -->
|
||||
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
<value>Tell history in auto tabs</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
|
||||
<value>Number of preloaded tells</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
|
||||
<value>How many earlier tell messages are loaded from the database when an auto tell tab is opened. 0 disables the preload.</value>
|
||||
</data>
|
||||
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
|
||||
<value>Only takes effect when auto tell tabs are enabled in the Chat tab.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Settings UX Polish v10 wipe migration -->
|
||||
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
|
||||
<value>Settings reorganised</value>
|
||||
</data>
|
||||
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
|
||||
<value>Hellion Chat 0.5.0 reorganised the settings into themed tabs. Your chat database and your message history stay untouched. Settings have been reset to defaults; if you want to pick a privacy profile again, the reopen button is in the Privacy tab. A backup of your previous config is at HellionChat.json.pre-v10-backup next to the live config file.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Settings UX Polish 8-tab structure -->
|
||||
<data name="Settings_Tab_General" xml:space="preserve">
|
||||
<value>General</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Window" xml:space="preserve">
|
||||
<value>Window</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Chat" xml:space="preserve">
|
||||
<value>Chat</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Tabs" xml:space="preserve">
|
||||
<value>Tabs</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Database" xml:space="preserve">
|
||||
<value>Database</value>
|
||||
</data>
|
||||
<data name="Settings_Tab_Information" xml:space="preserve">
|
||||
<value>Information</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — General-Tab section headings -->
|
||||
<data name="Settings_General_Input_Heading" xml:space="preserve">
|
||||
<value>Input</value>
|
||||
</data>
|
||||
<data name="Settings_General_Audio_Heading" xml:space="preserve">
|
||||
<value>Audio & Notifications</value>
|
||||
</data>
|
||||
<data name="Settings_General_Performance_Heading" xml:space="preserve">
|
||||
<value>Performance</value>
|
||||
</data>
|
||||
<data name="Settings_General_Language_Heading" xml:space="preserve">
|
||||
<value>Language & Input Helpers</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Appearance-Tab section headings -->
|
||||
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
|
||||
<value>Fonts</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
|
||||
<value>Chat Colours</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
|
||||
<value>Timestamps</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Window-Tab section headings -->
|
||||
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
|
||||
<value>Hide</value>
|
||||
</data>
|
||||
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
|
||||
<value>Inactivity Hide</value>
|
||||
</data>
|
||||
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
|
||||
<value>Window Frame</value>
|
||||
</data>
|
||||
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
|
||||
<value>Tooltips</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Chat-Tab section headings -->
|
||||
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
|
||||
<value>Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
|
||||
<value>Message Behaviour</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
|
||||
<value>Preview</value>
|
||||
</data>
|
||||
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Database-Tab section headings -->
|
||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||
<value>Storage</value>
|
||||
</data>
|
||||
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
|
||||
<value>Overview</value>
|
||||
</data>
|
||||
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
|
||||
<value>Maintenance</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Information-Tab section headings -->
|
||||
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
|
||||
<value>Version Info</value>
|
||||
</data>
|
||||
<data name="Settings_Information_About_Heading" xml:space="preserve">
|
||||
<value>About HellionChat</value>
|
||||
</data>
|
||||
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
|
||||
<value>Changelog</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Default tab presets (channel-themed) -->
|
||||
<data name="Tabs_Presets_System" xml:space="preserve">
|
||||
<value>System</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
|
||||
<value>Free Company</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Party" xml:space="preserve">
|
||||
<value>Party</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Beginner" xml:space="preserve">
|
||||
<value>Beginner</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
|
||||
<value>Linkshell</value>
|
||||
</data>
|
||||
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
|
||||
<value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+100
@@ -131,6 +131,15 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Loading logs ....
|
||||
/// </summary>
|
||||
internal static string ChatExport_Initial {
|
||||
get {
|
||||
return ResourceManager.GetString("ChatExport_Initial", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Input is disabled for this tab.
|
||||
/// </summary>
|
||||
@@ -1454,6 +1463,34 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Database migration has failed, a new database will be created.
|
||||
///Your old database can still be recovered, please contact the plugin author for help..
|
||||
/// </summary>
|
||||
internal static string Database_Migration_Error_Desc {
|
||||
get {
|
||||
return ResourceManager.GetString("Database_Migration_Error_Desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Chat2 Database Migration Error.
|
||||
/// </summary>
|
||||
internal static string Database_Migration_Error_Title {
|
||||
get {
|
||||
return ResourceManager.GetString("Database_Migration_Error_Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open date picker.
|
||||
/// </summary>
|
||||
internal static string DatePicker_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("DatePicker_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fri.
|
||||
/// </summary>
|
||||
@@ -1643,6 +1680,15 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset date selection..
|
||||
/// </summary>
|
||||
internal static string DbViewer_Date_Reset_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("DbViewer_Date_Reset_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to FromTo:.
|
||||
/// </summary>
|
||||
@@ -1733,6 +1779,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export the message history to a json file..
|
||||
/// </summary>
|
||||
internal static string Export_Json_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Export_Json_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export the message history to a text file..
|
||||
/// </summary>
|
||||
internal static string Export_Txt_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Export_Txt_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Chinese (full).
|
||||
/// </summary>
|
||||
@@ -1796,6 +1860,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pick a folder location for export..
|
||||
/// </summary>
|
||||
internal static string Folder_Export_Location_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Folder_Export_Location_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pick an export location.
|
||||
/// </summary>
|
||||
internal static string Folder_Selection_Header {
|
||||
get {
|
||||
return ResourceManager.GetString("Folder_Selection_Header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Source.
|
||||
/// </summary>
|
||||
@@ -3713,6 +3795,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Previous page.
|
||||
/// </summary>
|
||||
internal static string Page_ArrowLeft_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Page_ArrowLeft_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Next page.
|
||||
/// </summary>
|
||||
internal static string Page_ArrowRight_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Page_ArrowRight_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unable to find ID for this message, please try another one..
|
||||
/// </summary>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1435,4 +1435,38 @@ Nachdem du 'Aktiviert' angeklickt und auf 'Start' gedrückt hast, wird die einge
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1432,4 +1432,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+34
@@ -1433,4 +1433,38 @@
|
||||
<data name="ChannelSelector_Select" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Title" xml:space="preserve">
|
||||
<value>Chat2 Database Migration Error</value>
|
||||
</data>
|
||||
<data name="Database_Migration_Error_Desc" xml:space="preserve">
|
||||
<value>Database migration has failed, a new database will be created.
|
||||
Your old database can still be recovered, please contact the plugin author for help.</value>
|
||||
</data>
|
||||
<data name="DbViewer_Date_Reset_Tooltip" xml:space="preserve">
|
||||
<value>Reset date selection.</value>
|
||||
</data>
|
||||
<data name="Folder_Export_Location_Tooltip" xml:space="preserve">
|
||||
<value>Pick a folder location for export.</value>
|
||||
</data>
|
||||
<data name="Folder_Selection_Header" xml:space="preserve">
|
||||
<value>Pick an export location</value>
|
||||
</data>
|
||||
<data name="Export_Txt_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a text file.</value>
|
||||
</data>
|
||||
<data name="Export_Json_Tooltip" xml:space="preserve">
|
||||
<value>Export the message history to a json file.</value>
|
||||
</data>
|
||||
<data name="Page_ArrowLeft_Tooltip" xml:space="preserve">
|
||||
<value>Previous page</value>
|
||||
</data>
|
||||
<data name="Page_ArrowRight_Tooltip" xml:space="preserve">
|
||||
<value>Next page</value>
|
||||
</data>
|
||||
<data name="DatePicker_Tooltip" xml:space="preserve">
|
||||
<value>Open date picker</value>
|
||||
</data>
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
+91
-14
@@ -99,8 +99,8 @@ public sealed class ChatLogWindow : Window
|
||||
SetUpTextCommandChannels();
|
||||
SetUpAllCommands();
|
||||
|
||||
Plugin.Commands.Register("/clearlog2", "Clear the Chat 2 chat log").Execute += ClearLog;
|
||||
Plugin.Commands.Register("/chat2").Execute += ToggleChat;
|
||||
Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
|
||||
Plugin.Commands.Register("/hellion").Execute += ToggleChat;
|
||||
|
||||
Plugin.ClientState.Login += Login;
|
||||
Plugin.ClientState.Logout += Logout;
|
||||
@@ -115,8 +115,8 @@ public sealed class ChatLogWindow : Window
|
||||
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
||||
Plugin.ClientState.Logout -= Logout;
|
||||
Plugin.ClientState.Login -= Login;
|
||||
Plugin.Commands.Register("/chat2").Execute -= ToggleChat;
|
||||
Plugin.Commands.Register("/clearlog2").Execute -= ClearLog;
|
||||
Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
|
||||
Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
|
||||
}
|
||||
|
||||
private void Logout(int _, int __)
|
||||
@@ -375,10 +375,6 @@ public sealed class ChatLogWindow : Window
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel;
|
||||
|
||||
SetChannel(newTab.CurrentChannel.Channel);
|
||||
|
||||
// Inform the webinterface about tab switch
|
||||
// TODO implement tabs in the webinterface
|
||||
Plugin.ServerCore.SendNewLogin();
|
||||
}
|
||||
|
||||
private enum HideState
|
||||
@@ -455,7 +451,9 @@ public sealed class ChatLogWindow : Window
|
||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||
|
||||
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
||||
BgAlpha = Plugin.Config.WindowAlpha / 100f;
|
||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
||||
? Plugin.Config.HellionThemeWindowOpacity
|
||||
: Plugin.Config.WindowAlpha / 100f;
|
||||
|
||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||
WasDocked = ImGui.IsWindowDocked();
|
||||
@@ -772,10 +770,7 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
var currentChannel = ReadChannelName(activeTab);
|
||||
if (!currentChannel.SequenceEqual(PreviousChannel))
|
||||
{
|
||||
PreviousChannel = currentChannel;
|
||||
Plugin.ServerCore.SendChannelSwitch(currentChannel);
|
||||
}
|
||||
|
||||
DrawChunks(currentChannel);
|
||||
}
|
||||
@@ -1195,7 +1190,13 @@ public sealed class ChatLogWindow : Window
|
||||
if (tab.DisplayTimestamp)
|
||||
{
|
||||
var localTime = message.Date.ToLocalTime();
|
||||
var timestamp = localTime.ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("de-DE"));
|
||||
// Force the format explicitly per setting. Relying on the
|
||||
// current culture meant a German system locale always
|
||||
// produced 24h regardless of the toggle, so the checkbox
|
||||
// looked dead.
|
||||
var timestamp = Plugin.Config.Use24HourClock
|
||||
? localTime.ToString("HH:mm", CultureInfo.InvariantCulture)
|
||||
: localTime.ToString("h:mm tt", CultureInfo.InvariantCulture);
|
||||
if (isTable)
|
||||
{
|
||||
if (!Plugin.Config.HideSameTimestamps || timestamp != lastTimestamp)
|
||||
@@ -1310,14 +1311,90 @@ public sealed class ChatLogWindow : Window
|
||||
if (child)
|
||||
{
|
||||
var previousTab = Plugin.CurrentTab;
|
||||
// Hellion Chat — auto-tell-tabs section divider rendered
|
||||
// exactly once before the first temp tab, with a live unit
|
||||
// counter pulled directly from the tab list.
|
||||
var tempTabHeaderRendered = false;
|
||||
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
||||
|
||||
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
|
||||
{
|
||||
var tab = Plugin.Config.Tabs[tabI];
|
||||
if (tab.PopOut)
|
||||
continue;
|
||||
|
||||
if (tab.IsTempTab && !tempTabHeaderRendered)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||
{
|
||||
ImGui.TextDisabled($"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})");
|
||||
}
|
||||
tempTabHeaderRendered = true;
|
||||
}
|
||||
|
||||
var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})";
|
||||
var clicked = ImGui.Selectable($"{tab.Name}{unread}###log-tab-{tabI}", Plugin.LastTab == tabI || Plugin.WantedTab == tabI);
|
||||
var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}";
|
||||
var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI;
|
||||
|
||||
var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
|
||||
|
||||
if (showGreetedAffordance)
|
||||
{
|
||||
// Greeted toggle sits left of the selectable so the
|
||||
// click areas stay separate. The icon also doubles
|
||||
// as the visual "I'm done with this person" cue.
|
||||
// Compact frame padding keeps the icon dezent next
|
||||
// to the tab name instead of a chunky button block.
|
||||
var greetedIcon = tab.IsGreeted ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.Check;
|
||||
var greetedTooltip = tab.IsGreeted
|
||||
? HellionStrings.AutoTellTabs_GreetedTooltip
|
||||
: HellionStrings.AutoTellTabs_UnGreetedTooltip;
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(2, 1)))
|
||||
using (ImRaii.PushColor(ImGuiCol.Button, 0))
|
||||
{
|
||||
if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip))
|
||||
{
|
||||
if (tab.IsGreeted)
|
||||
{
|
||||
Plugin.AutoTellTabsService.UnmarkGreeted(tab);
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.AutoTellTabsService.MarkGreeted(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
bool clicked;
|
||||
if (showGreetedAffordance && tab.IsGreeted)
|
||||
{
|
||||
// Dim the tab name once the user marked the partner
|
||||
// as greeted, so a glance at the sidebar tells them
|
||||
// who still needs attention. Selectable has no idle
|
||||
// background slot in ImGui, so the dim only applies
|
||||
// to the selected and hovered states — the text dim
|
||||
// alone signals greeted in the idle state.
|
||||
var headerBase = ImGui.GetColorU32(ImGuiCol.Header);
|
||||
var hoverBase = ImGui.GetColorU32(ImGuiCol.HeaderHovered);
|
||||
var dimHeader = (headerBase & 0xFF000000u) | ((headerBase & 0x00FEFEFEu) >> 1);
|
||||
var dimHover = (hoverBase & 0xFF000000u) | ((hoverBase & 0x00FEFEFEu) >> 1);
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)))
|
||||
using (ImRaii.PushColor(ImGuiCol.Header, dimHeader))
|
||||
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, dimHover))
|
||||
{
|
||||
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
|
||||
}
|
||||
|
||||
DrawTabContextMenu(tab, tabI);
|
||||
|
||||
if (!clicked && Plugin.WantedTab != tabI)
|
||||
|
||||
+35
-167
@@ -3,7 +3,6 @@ using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
@@ -18,13 +17,12 @@ using Dalamud.Interface.ImGuiNotification;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using MoreLinq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChatTwo.Ui;
|
||||
|
||||
public class DbViewer : Window
|
||||
{
|
||||
public const float RowPerPage = 1000.0f;
|
||||
public const int RowPerPage = 1000;
|
||||
|
||||
private readonly Plugin Plugin;
|
||||
|
||||
@@ -56,6 +54,8 @@ public class DbViewer : Window
|
||||
private string InputPath = string.Empty;
|
||||
private IActiveNotification Notification = null!;
|
||||
|
||||
private bool NeedsScrollReset;
|
||||
|
||||
public DbViewer(Plugin plugin) : base("DBViewer###chat2-dbviewer")
|
||||
{
|
||||
Plugin = plugin;
|
||||
@@ -76,19 +76,19 @@ public class DbViewer : Window
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
|
||||
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
|
||||
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
|
||||
}
|
||||
|
||||
private void Toggle(string _, string __) => Toggle();
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
var totalPages = (int)Math.Ceiling(Count / RowPerPage);
|
||||
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
||||
if (totalPages < 1)
|
||||
totalPages = 1;
|
||||
|
||||
@@ -104,11 +104,17 @@ public class DbViewer : Window
|
||||
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
|
||||
DateWidget.DatePickerWithInput("##FromDate", 1, ref MinDateString, ref AfterDate, DateFormat);
|
||||
DateWidget.DatePickerWithInput("##ToDate", 2, ref MaxDateString, ref BeforeDate, DateFormat, true);
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Recycle))
|
||||
DateReset();
|
||||
ImGuiUtil.DrawArrows(ref CurrentPage, 1, totalPages, spacing);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Language.DbViewer_Date_Reset_Tooltip);
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
|
||||
ChannelSelection();
|
||||
|
||||
var skipText = Language.DbViewer_CharacterOption;
|
||||
@@ -128,12 +134,12 @@ public class DbViewer : Window
|
||||
ImGui.OpenPopup("InputPathDialog");
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Pick a folder location for export.");
|
||||
ImGui.SetTooltip(Language.Folder_Export_Location_Tooltip);
|
||||
|
||||
using (var innerPopup = ImRaii.Popup("InputPathDialog"))
|
||||
{
|
||||
if (innerPopup.Success)
|
||||
Plugin.FileDialogManager.OpenFolderDialog("Pick an export location", (b, s) => { if (b) InputPath = s; }, null, true);
|
||||
Plugin.FileDialogManager.OpenFolderDialog(Language.Folder_Selection_Header, (b, s) => { if (b) InputPath = s; }, null, true);
|
||||
}
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
@@ -145,7 +151,7 @@ public class DbViewer : Window
|
||||
new Notification
|
||||
{
|
||||
Title = "Chat2 Text Export",
|
||||
Content = "Loading logs ...",
|
||||
Content = Language.ChatExport_Initial,
|
||||
Type = NotificationType.Info,
|
||||
Minimized = false,
|
||||
UserDismissable = false,
|
||||
@@ -157,36 +163,22 @@ public class DbViewer : Window
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Export the message history to a text file.");
|
||||
ImGui.SetTooltip(Language.Export_Txt_Tooltip);
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
using (ImRaii.Disabled(InputPath.Length == 0 || IsExporting))
|
||||
{
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.FileExport))
|
||||
{
|
||||
Notification = Plugin.Notification.AddNotification(
|
||||
new Notification
|
||||
{
|
||||
Title = "Chat2 Json Export",
|
||||
Content = "Loading logs ...",
|
||||
Type = NotificationType.Info,
|
||||
Minimized = false,
|
||||
UserDismissable = false,
|
||||
InitialDuration = TimeSpan.FromSeconds(10000),
|
||||
Progress = 0.0f,
|
||||
});
|
||||
CreateTempJsonFile();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip("Export the message history to a json file.");
|
||||
// Hellion Chat: the JSON export button used to dump the database in
|
||||
// the upstream webinterface's wire format. With the webinterface
|
||||
// removed there is no consumer for that format any more, so the
|
||||
// button is dropped. The Privacy tab's MessageExporter covers the
|
||||
// same ground (Markdown / JSON / CSV) with channel and date filters
|
||||
// and is the supported way to get history out of the plugin.
|
||||
|
||||
var width = 350 * ImGuiHelpers.GlobalScale;
|
||||
var loadingIndicator = IsProcessing && ProcessingStart < Environment.TickCount64;
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(string.Format(Language.DbViewer_Page, CurrentPage, totalPages, Count, loadingIndicator ? Language.DbViewer_LoadingIndicator : ""));
|
||||
ImGuiUtil.DrawArrows(ref CurrentPage, 1, totalPages, spacing, tooltipLeft: Language.Page_ArrowLeft_Tooltip, tooltipRight: Language.Page_ArrowRight_Tooltip);
|
||||
|
||||
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
|
||||
ImGui.SetNextItemWidth(width);
|
||||
if (ImGui.InputTextWithHint("##searchbar", Language.DbViewer_SearcHint, ref SimpleSearchTerm, 30))
|
||||
@@ -222,6 +214,7 @@ public class DbViewer : Window
|
||||
Messages = rangeMessageEnumerator.ToArray();
|
||||
|
||||
Filtered = Filter(Messages);
|
||||
NeedsScrollReset = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -246,6 +239,12 @@ public class DbViewer : Window
|
||||
if (!child.Success)
|
||||
return;
|
||||
|
||||
if (NeedsScrollReset)
|
||||
{
|
||||
NeedsScrollReset = false;
|
||||
ImGui.SetScrollY(0.0f);
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table("##messageHistory", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable);
|
||||
if (!table.Success)
|
||||
return;
|
||||
@@ -281,6 +280,7 @@ public class DbViewer : Window
|
||||
private void ChannelSelection()
|
||||
{
|
||||
const string addTabPopup = "add-channel-popup";
|
||||
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
if (ImGui.Button("Channels"))
|
||||
ImGui.OpenPopup(addTabPopup);
|
||||
@@ -306,7 +306,7 @@ public class DbViewer : Window
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Select all");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SameLine(0, spacing);
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
|
||||
{
|
||||
@@ -317,7 +317,7 @@ public class DbViewer : Window
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Unselect all");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SameLine(0, spacing);
|
||||
|
||||
using var headerNode = ImRaii.TreeNode(header);
|
||||
if (!headerNode.Success)
|
||||
@@ -444,136 +444,4 @@ public class DbViewer : Window
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateTempJsonFile()
|
||||
{
|
||||
IsExporting = true;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = SelectedChannels.Select(pair => (byte)pair.Key).ToArray();
|
||||
|
||||
var rangeMessageEnumerator = Plugin.MessageManager.Store.GetDateRange(AfterDate, BeforeDate, channels);
|
||||
var messageHistory = rangeMessageEnumerator.ToArray();
|
||||
await rangeMessageEnumerator.DisposeAsync();
|
||||
|
||||
var filteredHistory = Filter(messageHistory);
|
||||
|
||||
await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.json"));
|
||||
|
||||
var batch = 0;
|
||||
var messageContainer = new Messages();
|
||||
List<MessageResponse> templates = [];
|
||||
foreach (var messages in filteredHistory.Batch(5000))
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
templates.Add(ReadMessageContent(message));
|
||||
batch++;
|
||||
}
|
||||
|
||||
Notification.Progress = (float)batch / filteredHistory.Count;
|
||||
Notification.Content = $"Exported {batch} of {filteredHistory.Count} messages";
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
messageContainer.Set = templates.ToArray();
|
||||
await stream.WriteAsync(JsonConvert.SerializeObject(messageContainer));
|
||||
templates.Clear();
|
||||
|
||||
await using (var fileStream = File.Open(Path.Join(InputPath, "gfdata.gfd"), FileMode.OpenOrCreate))
|
||||
{
|
||||
await using var byteWriter = new BinaryWriter(fileStream);
|
||||
byteWriter.Write(Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data);
|
||||
}
|
||||
|
||||
await using (var fileStream = File.Open(Path.Join(InputPath, "fonticon_ps5.tex"), FileMode.OpenOrCreate))
|
||||
{
|
||||
await using var byteWriter = new BinaryWriter(fileStream);
|
||||
byteWriter.Write(Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!.Data);
|
||||
}
|
||||
|
||||
await using (var fileStream = File.Open(Path.Join(InputPath, "FFXIV_Lodestone_SSF.ttf"), FileMode.OpenOrCreate))
|
||||
{
|
||||
await using var byteWriter = new BinaryWriter(fileStream);
|
||||
byteWriter.Write(Plugin.FontManager.GameSymFont);
|
||||
}
|
||||
|
||||
Notification.Progress = 1.0f;
|
||||
Notification.Content = "Done!!!";
|
||||
Notification.Type = NotificationType.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed creating txt backup");
|
||||
|
||||
Notification.Content = "Error ...";
|
||||
Notification.Type = NotificationType.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsExporting = false;
|
||||
Notification.UserDismissable = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MessageResponse ReadMessageContent(Message message)
|
||||
{
|
||||
var response = new MessageResponse
|
||||
{
|
||||
Id = message.Id,
|
||||
Timestamp = message.Date.ToLocalTime().ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("es-ES"))
|
||||
};
|
||||
|
||||
var sender = message.Sender.Select(ProcessChunk);
|
||||
var content = message.Content.Select(ProcessChunk);
|
||||
response.Templates = sender.Concat(content).ToArray();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private MessageTemplate ProcessChunk(Chunk chunk)
|
||||
{
|
||||
if (chunk is IconChunk { } icon)
|
||||
{
|
||||
var iconId = (uint)icon.Icon;
|
||||
return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {PayloadType = WebPayloadType.Icon, IconId = iconId}: MessageTemplate.Empty;
|
||||
}
|
||||
|
||||
if (chunk is TextChunk { } text)
|
||||
{
|
||||
if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes)
|
||||
{
|
||||
var image = EmoteCache.GetEmote(emotePayload.Code);
|
||||
|
||||
if (image is { Failed: false })
|
||||
return new MessageTemplate { PayloadType = WebPayloadType.CustomEmote, Color = 0, Content = emotePayload.Code };
|
||||
}
|
||||
|
||||
var color = text.Foreground;
|
||||
if (color == null && text.FallbackColour != null)
|
||||
{
|
||||
var type = text.FallbackColour.Value;
|
||||
color = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor();
|
||||
}
|
||||
|
||||
color ??= 0;
|
||||
|
||||
var userContent = text.Content;
|
||||
if (Plugin.ChatLogWindow.ScreenshotMode)
|
||||
{
|
||||
if (chunk.Link is PlayerPayload playerPayload)
|
||||
userContent = Plugin.ChatLogWindow.HidePlayerInString(userContent, playerPayload.PlayerName, playerPayload.World.RowId);
|
||||
else if (Plugin.PlayerState.IsLoaded)
|
||||
userContent = Plugin.ChatLogWindow.HidePlayerInString(userContent, Plugin.PlayerState.CharacterName, Plugin.PlayerState.HomeWorld.RowId);
|
||||
}
|
||||
|
||||
var isNotUrl = text.Link is not UriPayload;
|
||||
return new MessageTemplate { PayloadType = isNotUrl ? WebPayloadType.RawText : WebPayloadType.CustomUri, Color = color.Value, Content = userContent };
|
||||
}
|
||||
|
||||
return MessageTemplate.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ public class DebuggerWindow : Window
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute += Toggle;
|
||||
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute -= Toggle;
|
||||
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
|
||||
}
|
||||
|
||||
private void Toggle(string _, string __) => Toggle();
|
||||
|
||||
+63
-47
@@ -21,63 +21,79 @@ internal static class HellionStyle
|
||||
{
|
||||
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
|
||||
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
|
||||
// expects.
|
||||
// expects. Hex values are sourced from the Hellion Online Media brand
|
||||
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
|
||||
|
||||
// Primary — cyan-teal for actionable controls (buttons, checks, sliders).
|
||||
private const uint PrimaryRgba = 0x00B8D4FF;
|
||||
private const uint PrimaryHoverRgba = 0x26C6DAFF;
|
||||
private const uint PrimaryActiveRgba = 0x00838FFF;
|
||||
// Primary — Arctic Cyan, used for every interactive control (buttons,
|
||||
// checks, sliders, separators when hovered). Three brand stages plus a
|
||||
// hover that lifts to brand-color-light and a press that drops to
|
||||
// brand-color-dark.
|
||||
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
|
||||
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
|
||||
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
|
||||
|
||||
// Secondary — industrial amber, used as a warm highlight for active
|
||||
// states (tab borders, resize grips, scrollbar grabs).
|
||||
private const uint SecondaryRgba = 0xFFB300FF;
|
||||
private const uint SecondaryHoverRgba = 0xFFC940FF;
|
||||
private const uint SecondaryActiveRgba = 0xC68400FF;
|
||||
// Identity — brand-color-dark teal for window title bars and the
|
||||
// active tab. Sits visibly below the primary cyan on buttons so the
|
||||
// user sees "where am I" (deep teal) versus "what can I click"
|
||||
// (brand cyan) without leaving the cyan family.
|
||||
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
|
||||
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
|
||||
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
|
||||
|
||||
// Tertiary — slate violet, reserved for title bars and the active tab
|
||||
// background so identity beats out the cyan accent without competing
|
||||
// with it on action controls.
|
||||
private const uint TertiaryRgba = 0x7B61FFFF;
|
||||
private const uint TertiaryHoverRgba = 0x9580FFFF;
|
||||
private const uint TertiaryActiveRgba = 0x5E45D9FF;
|
||||
// Accent — Ember Orange for warm highlights on grips and scrollbar
|
||||
// pulls. Replaces the previous industrial amber so the plugin matches
|
||||
// the website's CTA palette. AccentActive is reserved for any future
|
||||
// pressed-state on accent surfaces; the current slots only need
|
||||
// AccentRgba and AccentHoverRgba.
|
||||
private const uint AccentRgba = 0xF97316FF; // accent-color
|
||||
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
|
||||
|
||||
// Surfaces — deep slate window/frame backgrounds, steel borders.
|
||||
private const uint WindowBgRgba = 0x0E1A20FF;
|
||||
private const uint ChildBgRgba = 0x102027FF;
|
||||
private const uint PopupBgRgba = 0x102027FF;
|
||||
private const uint FrameBgRgba = 0x162831FF;
|
||||
private const uint FrameBgHoverRgba = 0x1F3540FF;
|
||||
private const uint FrameBgActiveRgba = 0x274250FF;
|
||||
private const uint BorderRgba = 0x37474FFF;
|
||||
// Surfaces — Hellion brand background ladder. Window darkest, frame
|
||||
// hover ladder climbs into surface tones. Matches the website's
|
||||
// background / background-medium / background-light / surface vars.
|
||||
private const uint WindowBgRgba = 0x070B12FF; // background
|
||||
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
|
||||
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
|
||||
private const uint FrameBgRgba = 0x141E30FF; // background-light
|
||||
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
|
||||
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
|
||||
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
|
||||
private const uint BorderRgba = 0x00BED266;
|
||||
private const uint BorderShadowRgba = 0x00000000;
|
||||
|
||||
// Headers / collapsing-headers / tree nodes / selectables.
|
||||
private const uint HeaderRgba = 0x1B2C36FF;
|
||||
private const uint HeaderHoverRgba = 0x263A45FF;
|
||||
private const uint HeaderActiveRgba = 0x324A57FF;
|
||||
// Headers / collapsing-headers / tree nodes / selectables — same
|
||||
// surface ladder as frames so panels feel consistent.
|
||||
private const uint HeaderRgba = 0x141E30FF;
|
||||
private const uint HeaderHoverRgba = 0x1A2538FF;
|
||||
private const uint HeaderActiveRgba = 0x22303FFF;
|
||||
|
||||
// Title bars — tertiary identity for the active state.
|
||||
private const uint TitleBgRgba = 0x0E1A20FF;
|
||||
private const uint TitleBgActiveRgba = 0x5E45D9FF;
|
||||
private const uint TitleBgCollapsedRgba = 0x0A1318FF;
|
||||
// Title bars — Identity teal on active so the focused window reads
|
||||
// as "yours" without using accent or primary slots.
|
||||
private const uint TitleBgRgba = 0x070B12FF;
|
||||
private const uint TitleBgActiveRgba = IdentityRgba;
|
||||
private const uint TitleBgCollapsedRgba = 0x05080EFF;
|
||||
|
||||
// Tabs — tertiary tint, secondary highlight while hovered/unfocused.
|
||||
private const uint TabRgba = 0x162831FF;
|
||||
private const uint TabHoveredRgba = 0x9580FFFF;
|
||||
private const uint TabActiveRgba = 0x7B61FFFF;
|
||||
private const uint TabUnfocusedRgba = 0x12222AFF;
|
||||
private const uint TabUnfocusedActiveRgba = 0x5E45D9FF;
|
||||
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
|
||||
// active. Unfocused-active uses the deeper Identity stage so an
|
||||
// unfocused window's active tab still reads but does not pull focus.
|
||||
private const uint TabRgba = 0x141E30FF;
|
||||
private const uint TabHoveredRgba = IdentityHoverRgba;
|
||||
private const uint TabActiveRgba = IdentityRgba;
|
||||
private const uint TabUnfocusedRgba = 0x0C1220FF;
|
||||
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
|
||||
|
||||
// Scrollbar — slate base, secondary amber on grab.
|
||||
private const uint ScrollbarBgRgba = 0x0E1A20FF;
|
||||
private const uint ScrollbarGrabRgba = 0x37474FFF;
|
||||
private const uint ScrollbarGrabHoveredRgba = 0xFFC940FF;
|
||||
private const uint ScrollbarGrabActiveRgba = 0xFFB300FF;
|
||||
// Scrollbar — Ember on grab so the pull stands out without competing
|
||||
// with the cyan action buttons. Idle grab is a subtle surface tone,
|
||||
// hover/active climb into accent.
|
||||
private const uint ScrollbarBgRgba = 0x070B12FF;
|
||||
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
|
||||
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
|
||||
private const uint ScrollbarGrabActiveRgba = AccentRgba;
|
||||
|
||||
// Resize grip — secondary amber for the active corner pull.
|
||||
private const uint ResizeGripRgba = 0x37474FFF;
|
||||
private const uint ResizeGripHoveredRgba = 0xFFC940FF;
|
||||
private const uint ResizeGripActiveRgba = 0xFFB300FF;
|
||||
// Resize grip — same Ember treatment as the scrollbar.
|
||||
private const uint ResizeGripRgba = 0x141E30FF;
|
||||
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
|
||||
private const uint ResizeGripActiveRgba = AccentRgba;
|
||||
|
||||
// Separator and check mark / slider follow the primary cyan.
|
||||
|
||||
|
||||
+10
-2
@@ -70,8 +70,16 @@ internal class Popout : Window
|
||||
|
||||
if (!ChatLogWindow.PopOutDocked[Idx])
|
||||
{
|
||||
var alpha = Tab.IndependentOpacity ? Tab.Opacity : Plugin.Config.WindowAlpha;
|
||||
BgAlpha = alpha / 100f;
|
||||
if (Tab.IndependentOpacity)
|
||||
{
|
||||
BgAlpha = Tab.Opacity / 100f;
|
||||
}
|
||||
else
|
||||
{
|
||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
||||
? Plugin.Config.HellionThemeWindowOpacity
|
||||
: Plugin.Config.WindowAlpha / 100f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,14 +30,14 @@ public class SeStringDebugger : Window
|
||||
DisableWindowSounds = true;
|
||||
|
||||
#if DEBUG
|
||||
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute += Toggle;
|
||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if DEBUG
|
||||
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute -= Toggle;
|
||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+15
-18
@@ -9,7 +9,7 @@ using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui;
|
||||
|
||||
public sealed class SettingsWindow : Window
|
||||
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
|
||||
@@ -33,19 +33,14 @@ public sealed class SettingsWindow : Window
|
||||
|
||||
Tabs =
|
||||
[
|
||||
new Display(Mutable),
|
||||
new ChatLog(Plugin, Mutable),
|
||||
new Emote(Plugin, Mutable),
|
||||
new Preview(Mutable),
|
||||
new Fonts(Mutable),
|
||||
new ChatColours(Plugin, Mutable),
|
||||
new Tabs(Plugin, Mutable),
|
||||
new General(Plugin, Mutable),
|
||||
new Appearance(Plugin, Mutable),
|
||||
new SettingsTabs.Window(Plugin, Mutable),
|
||||
new Chat(Plugin, Mutable),
|
||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||
new SettingsTabs.Privacy(Plugin, Mutable),
|
||||
new Database(Plugin, Mutable),
|
||||
new Webinterface(Plugin, Mutable),
|
||||
new Miscellaneous(Mutable),
|
||||
new Changelog(Mutable),
|
||||
new About()
|
||||
new Information(Mutable),
|
||||
];
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
@@ -53,14 +48,14 @@ public sealed class SettingsWindow : Window
|
||||
|
||||
Initialise();
|
||||
|
||||
Plugin.Commands.Register("/chat2", "Perform various actions with Chat 2.").Execute += Command;
|
||||
Plugin.Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute += Command;
|
||||
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
|
||||
Plugin.Commands.Register("/chat2").Execute -= Command;
|
||||
Plugin.Commands.Register("/hellion").Execute -= Command;
|
||||
}
|
||||
|
||||
private void Command(string command, string args)
|
||||
@@ -115,14 +110,16 @@ public sealed class SettingsWindow : Window
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button(Language.Settings_SaveAndClose)) {
|
||||
if (ImGui.Button(Language.Settings_SaveAndClose))
|
||||
{
|
||||
save = true;
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button(Language.Settings_Discard)) {
|
||||
if (ImGui.Button(Language.Settings_Discard))
|
||||
{
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
@@ -136,7 +133,7 @@ public sealed class SettingsWindow : Window
|
||||
{
|
||||
var buttonWidth = ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var buttonWidth2 = ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2);
|
||||
ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2 - ImGui.GetStyle().ItemSpacing.X);
|
||||
|
||||
if (ImGui.Button(buttonLabel2))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
||||
@@ -180,7 +177,7 @@ public sealed class SettingsWindow : Window
|
||||
GameFunctions.GameFunctions.SetChatInteractable(true);
|
||||
|
||||
if (Plugin.Config.ShowEmotes)
|
||||
Task.Run(EmoteCache.LoadData);
|
||||
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
||||
|
||||
Initialise();
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
using System.Numerics;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class About : ISettingsTab
|
||||
{
|
||||
public string Name => string.Format(Language.Options_About_Tab, Plugin.PluginName) + "###tabs-about";
|
||||
|
||||
private readonly List<string> Translators =
|
||||
[
|
||||
"q673135110", "Akizem", "d0tiKs",
|
||||
"Moonlight_Everlit", "Dark32", "andreycout",
|
||||
"Button_", "Cali666", "cassandra308",
|
||||
"lokinmodar", "jtabox", "AkiraYorumoto",
|
||||
"MKhayle", "elena.space", "imlisa",
|
||||
"andrei5125", "ShivaMaheshvara", "aislinn87",
|
||||
"nishinatsu051", "lichuyuan", "Risu64",
|
||||
"yummypillow", "witchymary", "Yuzumi",
|
||||
"zomsakura", "Sirayuki"
|
||||
];
|
||||
|
||||
internal About()
|
||||
{
|
||||
Translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Authors);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Interface.Manifest.Author);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Discord);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, "@infi");
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Version);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Interface.Manifest.AssemblyVersion.ToString(3));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Discord_Thread);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "discordThread"))
|
||||
Dalamud.Utility.Util.OpenLink("https://canary.discord.com/channels/581875019861328007/1224865018789761126");
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo/issues");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_CrowdIn);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "crowdin"))
|
||||
Dalamud.Utility.Util.OpenLink("https://crowdin.com/project/chattwo");
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
var height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize("A").Y - ImGui.GetStyle().ItemSpacing.Y * 2;
|
||||
using (var aboutChild = ImRaii.Child("about", new Vector2(-1, height)))
|
||||
{
|
||||
if (aboutChild)
|
||||
{
|
||||
using var treeNode = ImRaii.TreeNode(Language.Options_About_Translators);
|
||||
if (treeNode)
|
||||
{
|
||||
using var translatorChild = ImRaii.Child("translators");
|
||||
if (translatorChild)
|
||||
{
|
||||
foreach (var translator in Translators)
|
||||
ImGui.TextUnformatted(translator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Appearance : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => HellionStrings.Settings_Tab_Appearance + "###tabs-appearance";
|
||||
|
||||
internal Appearance(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
DrawThemeSection();
|
||||
ImGui.Spacing();
|
||||
DrawFontsSection();
|
||||
ImGui.Spacing();
|
||||
DrawColoursSection();
|
||||
ImGui.Spacing();
|
||||
DrawTimestampsSection();
|
||||
}
|
||||
|
||||
private void DrawThemeSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Theme_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
||||
|
||||
// Clamp 0.5–1.0 stays consistent with Privacy.cs which already
|
||||
// shipped this slider; lower values would let chat windows
|
||||
// disappear behind game UI.
|
||||
using (ImRaii.Disabled(!Mutable.HellionThemeEnabled))
|
||||
{
|
||||
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||
var opacity = Mutable.HellionThemeWindowOpacity;
|
||||
if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f"))
|
||||
{
|
||||
Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f);
|
||||
}
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_WindowOpacity_Help);
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle);
|
||||
ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc);
|
||||
|
||||
if (Mutable.OverrideStyle)
|
||||
{
|
||||
DrawStyleCombo();
|
||||
}
|
||||
|
||||
// The Bestand-Slider WindowAlpha targets the chat log window's
|
||||
// background only. The Hellion theme opacity above already covers
|
||||
// every plugin window globally, so the two sliders fight each
|
||||
// other when the theme is active. Disable the legacy slider in
|
||||
// that case to make Hellion theme the single source of truth.
|
||||
using (ImRaii.Disabled(Mutable.HellionThemeEnabled))
|
||||
{
|
||||
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawStyleCombo()
|
||||
{
|
||||
var styles = StyleModel.GetConfiguredStyles();
|
||||
if (styles == null)
|
||||
{
|
||||
ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected;
|
||||
using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle);
|
||||
if (!combo)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var style in styles)
|
||||
{
|
||||
if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name))
|
||||
{
|
||||
Mutable.ChosenStyle = style.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFontsSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Fonts_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
if (ImGui.Checkbox(HellionStrings.Theme_UseHellionFont_Name, ref Mutable.UseHellionFont))
|
||||
{
|
||||
// Mutex with the Bestand custom-font stack. Leaving FontsEnabled
|
||||
// checked alongside UseHellionFont made both checkboxes look
|
||||
// active even though the lower stack was greyed out, which
|
||||
// confused the user during the v0.5.0 walkthrough.
|
||||
if (Mutable.UseHellionFont)
|
||||
Mutable.FontsEnabled = false;
|
||||
}
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
using var fontDisabled = ImRaii.Disabled(Mutable.UseHellionFont);
|
||||
|
||||
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
|
||||
ImGui.Spacing();
|
||||
|
||||
var unused = false;
|
||||
if (!Mutable.FontsEnabled)
|
||||
{
|
||||
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var globalChooser = ImGuiUtil.FontChooser(Language.Options_Font_Name, Mutable.GlobalFontV2, false, ref unused);
|
||||
globalChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
{
|
||||
Plugin.Framework.Run(() => Mutable.GlobalFontV2 = r.Result);
|
||||
}
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##global"))
|
||||
{
|
||||
Mutable.GlobalFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_Font_Description, Plugin.PluginName));
|
||||
ImGuiUtil.WarningText(Language.Options_Font_Warning);
|
||||
ImGui.Spacing();
|
||||
|
||||
// LocaleNames being null means it is likely a game font which all support JP symbols.
|
||||
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref unused, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
|
||||
japaneseChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
{
|
||||
Plugin.Framework.Run(() => Mutable.JapaneseFontV2 = r.Result);
|
||||
}
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##japanese"))
|
||||
{
|
||||
Mutable.JapaneseFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f };
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_JapaneseFont_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
var italicChooser = ImGuiUtil.FontChooser(Language.Options_ItalicFont_Name, Mutable.ItalicFontV2, true, ref Mutable.ItalicEnabled);
|
||||
italicChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
{
|
||||
Plugin.Framework.Run(() => Mutable.ItalicFontV2 = r.Result);
|
||||
}
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##italic"))
|
||||
{
|
||||
Mutable.ItalicEnabled = false;
|
||||
Mutable.ItalicFontV2 = new SingleFontSpec { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_Italic_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
|
||||
{
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName));
|
||||
|
||||
var range = (int)Mutable.ExtraGlyphRanges;
|
||||
foreach (var extra in Enum.GetValues<ExtraGlyphRanges>())
|
||||
{
|
||||
ImGui.CheckboxFlags(extra.Name(), ref range, (int)extra);
|
||||
}
|
||||
|
||||
Mutable.ExtraGlyphRanges = (ExtraGlyphRanges)range;
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
|
||||
ImGuiUtil.HelpMarker(Language.Options_SymbolsFontSize_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawColoursSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Colours_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
||||
{
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset))
|
||||
{
|
||||
Mutable.ChatColours.Remove(type);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import))
|
||||
{
|
||||
var gameColour = Plugin.Functions.Chat.GetChannelColor(type);
|
||||
Mutable.ChatColours[type] = gameColour ?? type.DefaultColor() ?? 0;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var vec = Mutable.ChatColours.TryGetValue(type, out var colour)
|
||||
? ColourUtil.RgbaToVector3(colour)
|
||||
: ColourUtil.RgbaToVector3(type.DefaultColor() ?? 0);
|
||||
if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs))
|
||||
{
|
||||
Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTimestampsSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_PrettierTimestamps_Name, ref Mutable.PrettierTimestamps);
|
||||
ImGuiUtil.HelpMarker(Language.Options_PrettierTimestamps_Description);
|
||||
|
||||
if (Mutable.PrettierTimestamps)
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty);
|
||||
ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description);
|
||||
|
||||
ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps);
|
||||
ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description);
|
||||
}
|
||||
|
||||
ImGui.Checkbox(Language.Options_Use24HourClock_Name, ref Mutable.Use24HourClock);
|
||||
ImGuiUtil.HelpMarker(Language.Options_Use24HourClock_Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Changelog : ISettingsTab
|
||||
{
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_Changelog_Tab + "###tabs-changelog";
|
||||
|
||||
internal Changelog(Configuration mutable)
|
||||
{
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Warning_NotImplemented);
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.PrintChangelog, Language.Options_PrintChangelog_Name, Language.Options_PrintChangelog_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
var changelog = Plugin.Interface.Manifest.Changelog;
|
||||
if (changelog != null)
|
||||
{
|
||||
ImGui.TextUnformatted(Language.Options_Changelog_Header);
|
||||
ImGui.TextUnformatted($"Version {Plugin.Interface.Manifest.AssemblyVersion.ToString(3)}");
|
||||
ImGui.Spacing();
|
||||
foreach (var sentence in changelog.Split("\n"))
|
||||
{
|
||||
if (sentence == string.Empty)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var condition = sentence.StartsWith('-') || sentence.StartsWith(" -");
|
||||
using var indent = ImRaii.PushIndent(10.0f, true, condition);
|
||||
ImGui.TextUnformatted(sentence);
|
||||
}
|
||||
}
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using System.Numerics;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
// Chat-Tab — vier eigenständige Sektionen: Auto-Tell-Tabs, Behaviour,
|
||||
// Preview, Emotes. Der Emotes-Block ist 1:1 aus der Bestand-Datei
|
||||
// Emote.cs übernommen; die Datei wird in Plan-Task 11 (Settings UX
|
||||
// Polish v0.5.0) entfernt, sobald alle Tabs migriert sind.
|
||||
internal sealed class Chat : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
||||
|
||||
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
||||
|
||||
internal Chat(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
|
||||
WordPopupOptions = RefillSheet();
|
||||
}
|
||||
|
||||
private SearchSelector.SelectorPopupOptions RefillSheet()
|
||||
{
|
||||
return new SearchSelector.SelectorPopupOptions
|
||||
{
|
||||
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
DrawAutoTellTabsSection();
|
||||
ImGui.Spacing();
|
||||
DrawBehaviourSection();
|
||||
ImGui.Spacing();
|
||||
DrawPreviewSection();
|
||||
ImGui.Spacing();
|
||||
DrawEmotesSection();
|
||||
}
|
||||
|
||||
private void DrawAutoTellTabsSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_AutoTellTabs_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Enable_Name, ref Mutable.EnableAutoTellTabs);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description);
|
||||
|
||||
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||
var limit = Mutable.AutoTellTabsLimit;
|
||||
if (ImGui.SliderInt(HellionStrings.ChatLog_AutoTellTabs_Limit_Name, ref limit, 1, 50))
|
||||
{
|
||||
Mutable.AutoTellTabsLimit = limit;
|
||||
}
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
|
||||
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
|
||||
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBehaviourSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Behaviour_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_CollapseDuplicateMessages_Name, ref Mutable.CollapseDuplicateMessages);
|
||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMessages_Description);
|
||||
|
||||
if (Mutable.CollapseDuplicateMessages)
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_CollapseDuplicateMsgUniqueLink_Name, ref Mutable.CollapseKeepUniqueLinks);
|
||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPreviewSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Preview_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Preview_Name, Mutable.PreviewPosition.Name()))
|
||||
{
|
||||
if (combo)
|
||||
{
|
||||
foreach (var position in Enum.GetValues<PreviewPosition>())
|
||||
{
|
||||
if (ImGui.Selectable(position.Name(), Mutable.PreviewPosition == position))
|
||||
{
|
||||
Mutable.PreviewPosition = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGuiUtil.HelpMarker(Language.Options_Preview_Description);
|
||||
|
||||
if (ImGuiUtil.InputIntVertical(Language.Options_PreviewMinimum_Name, Language.Options_PreviewMinimum_Description, ref Mutable.PreviewMinimum))
|
||||
{
|
||||
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
|
||||
}
|
||||
|
||||
ImGui.Checkbox(Language.Options_PreviewOnlyIf_Name, ref Mutable.OnlyPreviewIf);
|
||||
ImGuiUtil.HelpMarker(Language.Options_PreviewOnlyIf_Description);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEmotesSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Chat_Emotes_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_ShowEmotes_Name, ref Mutable.ShowEmotes);
|
||||
ImGuiUtil.HelpMarker(Language.Options_ShowEmotes_Desc);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
|
||||
{
|
||||
WordPopupOptions = RefillSheet();
|
||||
}
|
||||
|
||||
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
|
||||
|
||||
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
|
||||
{
|
||||
Mutable.BlockedEmotes.Add(newWord);
|
||||
}
|
||||
|
||||
using (var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
{
|
||||
if (table)
|
||||
{
|
||||
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
|
||||
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var copiedList = Mutable.BlockedEmotes.ToArray();
|
||||
foreach (var word in copiedList)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(word);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
|
||||
{
|
||||
Mutable.BlockedEmotes.Remove(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Emote_EmoteStats);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (EmoteCache.State is EmoteCache.LoadingState.Done)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
|
||||
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
{
|
||||
if (emoteTable)
|
||||
{
|
||||
ImGui.TableSetupColumn("##word1");
|
||||
ImGui.TableSetupColumn("##word2");
|
||||
ImGui.TableSetupColumn("##word3");
|
||||
ImGui.TableSetupColumn("##word4");
|
||||
ImGui.TableSetupColumn("##word5");
|
||||
|
||||
foreach (var word in EmoteCache.SortedCodeArray)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class ChatColours : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_ChatColours_Tab + "###tabs-chat-colours";
|
||||
|
||||
internal ChatColours(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
|
||||
#if DEBUG
|
||||
// Users can set colours for ExtraChat linkshells in the ExtraChat plugin directly.
|
||||
var sortable = ChatTypeExt.SortOrder
|
||||
.SelectMany(entry => entry.Item2)
|
||||
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
|
||||
.ToHashSet();
|
||||
var total = Enum.GetValues<ChatType>()
|
||||
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
|
||||
.ToHashSet();
|
||||
if (sortable.Count != total.Count)
|
||||
{
|
||||
Plugin.Log.Warning($"There are {sortable.Count} sortable channels, but there are {total.Count} total channels.");
|
||||
total.ExceptWith(sortable);
|
||||
foreach (var missing in total)
|
||||
Plugin.Log.Information($"Missing {missing}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
||||
{
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset))
|
||||
Mutable.ChatColours.Remove(type);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import))
|
||||
{
|
||||
var gameColour = Plugin.Functions.Chat.GetChannelColor(type);
|
||||
Mutable.ChatColours[type] = gameColour ?? type.DefaultColor() ?? 0;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var vec = Mutable.ChatColours.TryGetValue(type, out var colour)
|
||||
? ColourUtil.RgbaToVector3(colour)
|
||||
: ColourUtil.RgbaToVector3(type.DefaultColor() ?? 0);
|
||||
if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs))
|
||||
Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class ChatLog : ISettingsTab
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_ChatLog_Tab + "###tabs-chatlog";
|
||||
|
||||
internal ChatLog(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using (ImRaii.TextWrapPos(0.0f))
|
||||
{
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.KeepInputFocus, Language.Options_KeepInputFocus_Name, Language.Options_KeepInputFocus_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.PlaySounds, Language.Options_PlaySounds_Name, Language.Options_PlaySounds_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.SidebarTabView, Language.Options_SidebarTabView_Name, string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowNoviceNetwork, Language.Options_ShowNoviceNetwork_Name, Language.Options_ShowNoviceNetwork_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowHideButton, Language.Options_ShowHideButton_Name, Language.Options_ShowHideButton_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.NativeItemTooltips, Language.Options_NativeItemTooltips_Name, string.Format(Language.Options_NativeItemTooltips_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
if (Mutable.NativeItemTooltips)
|
||||
{
|
||||
ImGuiUtil.DragFloatVertical(Language.Options_TooltipOffset_Name, Language.Options_TooltipOffset_Desc, ref Mutable.TooltipOffset, 1, 0f, 400f, $"{Mutable.TooltipOffset:N0}px", ImGuiSliderFlags.AlwaysClamp);
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGuiUtil.InputIntVertical(Language.Options_MaxLinesToShow_Name, Language.Options_MaxLinesToShow_Description, ref Mutable.MaxLinesToRender))
|
||||
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.CanMove, Language.Options_CanMove_Name);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.CanResize, Language.Options_CanResize_Name);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowTitleBar, Language.Options_ShowTitleBar_Name);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowPopOutTitleBar, Language.Options_ShowPopOutTitleBar_Name);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.OverrideStyle, Language.Options_OverrideStyle_Name, Language.Options_OverrideStyle_Name_Desc);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_AdjustPosition_Name);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
var pos = Plugin.ChatLogWindow.LastWindowPos;
|
||||
if (ImGui.DragFloat2($"##{Language.Options_AdjustPosition_Name}", ref pos, 1, 0, float.MaxValue, "%.0fpx"))
|
||||
Plugin.ChatLogWindow.Position = pos;
|
||||
ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning);
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
if (!Mutable.OverrideStyle)
|
||||
return;
|
||||
|
||||
var styles = StyleModel.GetConfiguredStyles();
|
||||
if (styles == null)
|
||||
{
|
||||
ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable);
|
||||
ImGui.Spacing();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected;
|
||||
using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle);
|
||||
if (combo)
|
||||
{
|
||||
foreach (var style in styles)
|
||||
if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name))
|
||||
Mutable.ChosenStyle = style.Name;
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ internal sealed class Database : ISettingsTab
|
||||
private Plugin Plugin { get; }
|
||||
private 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)
|
||||
{
|
||||
@@ -33,30 +33,47 @@ internal sealed class Database : ISettingsTab
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
// Shift-on-open keeps the Advanced tools available without a permanent
|
||||
// toggle in the UI, mirroring upstream Chat 2 behaviour.
|
||||
if (changed)
|
||||
ShowAdvanced = ImGui.GetIO().KeyShift;
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description);
|
||||
DrawStorageSection();
|
||||
ImGui.Spacing();
|
||||
DrawViewerSection();
|
||||
ImGui.Spacing();
|
||||
DrawStatsSection();
|
||||
}
|
||||
|
||||
if (ImGuiUtil.OptionCheckbox(ref Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description))
|
||||
private void DrawStorageSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Storage_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages);
|
||||
ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description);
|
||||
|
||||
if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession))
|
||||
if (Mutable.LoadPreviousSession)
|
||||
Mutable.FilterIncludePreviousSessions = true;
|
||||
ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGuiUtil.OptionCheckbox(ref Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description))
|
||||
if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions))
|
||||
if (!Mutable.FilterIncludePreviousSessions)
|
||||
Mutable.LoadPreviousSession = false;
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description);
|
||||
|
||||
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
|
||||
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
|
||||
if (old.Exists || migratedOld.Exists)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
|
||||
ImGui.Spacing();
|
||||
|
||||
@@ -76,13 +93,16 @@ internal sealed class Database : ISettingsTab
|
||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Database_Metadata_Heading);
|
||||
private void DrawViewerSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Viewer_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
// Refresh the database size and message count every 5 seconds to avoid
|
||||
@@ -132,13 +152,19 @@ internal sealed class Database : ISettingsTab
|
||||
WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
private void DrawStatsSection()
|
||||
{
|
||||
if (!ShowAdvanced)
|
||||
return;
|
||||
|
||||
using var treeNode = ImRaii.TreeNode(Language.Options_Database_Advanced);
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Stats_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
|
||||
@@ -153,7 +179,7 @@ internal sealed class Database : ISettingsTab
|
||||
|
||||
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
|
||||
new Thread(() => InsertMessages(10_000)).Start();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertMessages(int count)
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Display : ISettingsTab
|
||||
{
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_Display_Tab + "###tabs-display";
|
||||
|
||||
internal Display(Configuration mutable)
|
||||
{
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideChat, Language.Options_HideChat_Name, Language.Options_HideChat_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideDuringCutscenes, Language.Options_HideDuringCutscenes_Name, string.Format(Language.Options_HideDuringCutscenes_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenNotLoggedIn, Language.Options_HideWhenNotLoggedIn_Name, string.Format(Language.Options_HideWhenNotLoggedIn_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenUiHidden, Language.Options_HideWhenUiHidden_Name, string.Format(Language.Options_HideWhenUiHidden_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideInLoadingScreens, Language.Options_HideInLoadingScreens_Name, string.Format(Language.Options_HideInLoadingScreens_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenInactive, Language.Options_HideWhenInactive_Name, Language.Options_HideWhenInactive_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (Mutable.HideWhenInactive)
|
||||
{
|
||||
using var _ = ImRaii.PushIndent();
|
||||
ImGuiUtil.InputIntVertical(Language.Options_InactivityHideTimeout_Name,
|
||||
Language.Options_InactivityHideTimeout_Description, ref Mutable.InactivityHideTimeout, 1, 10);
|
||||
// Enforce a minimum of 2 seconds to avoid people soft locking
|
||||
// themselves.
|
||||
Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout);
|
||||
ImGui.Spacing();
|
||||
|
||||
// This setting conflicts with HideInBattle, so it's disabled.
|
||||
using (ImRaii.Disabled(Mutable.HideInBattle))
|
||||
{
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.InactivityHideActiveDuringBattle,
|
||||
Language.Options_InactivityHideActiveDuringBattle_Name,
|
||||
Language.Options_InactivityHideActiveDuringBattle_Description);
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
using var channelTree = ImRaii.TreeNode(Language.Options_InactivityHideChannels_Name);
|
||||
if (channelTree.Success)
|
||||
{
|
||||
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_All_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
|
||||
{
|
||||
Mutable.InactivityHideChannelsV2 = TabsUtil.AllChannels();
|
||||
Mutable.InactivityHideExtraChatAll = true;
|
||||
Mutable.InactivityHideExtraChatChannels = [];
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_None_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
|
||||
{
|
||||
Mutable.InactivityHideChannelsV2 = [];
|
||||
Mutable.InactivityHideExtraChatAll = false;
|
||||
Mutable.InactivityHideExtraChatChannels = [];
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, Mutable.InactivityHideChannelsV2);
|
||||
ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels,
|
||||
ref Mutable.InactivityHideExtraChatAll, Mutable.InactivityHideExtraChatChannels);
|
||||
}
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.Use24HourClock, Language.Options_Use24HourClock_Name, Language.Options_Use24HourClock_Description);
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description);
|
||||
|
||||
if (Mutable.PrettierTimestamps)
|
||||
{
|
||||
using var _ = ImRaii.PushIndent();
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.MoreCompactPretty, Language.Options_MoreCompactPretty_Name, Language.Options_MoreCompactPretty_Description);
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.HideSameTimestamps, Language.Options_HideSameTimestamps_Name, Language.Options_HideSameTimestamps_Description);
|
||||
}
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseDuplicateMessages, Language.Options_CollapseDuplicateMessages_Name, Language.Options_CollapseDuplicateMessages_Description);
|
||||
if (Mutable.CollapseDuplicateMessages)
|
||||
{
|
||||
using var _ = ImRaii.PushIndent();
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseKeepUniqueLinks, Language.Options_CollapseDuplicateMsgUniqueLink_Name, Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
||||
}
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using System.Numerics;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Emote : ISettingsTab
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_Emote_Tab + "###tabs-emote";
|
||||
|
||||
private static SearchSelector.SelectorPopupOptions? WordPopupOptions;
|
||||
|
||||
internal Emote(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
|
||||
WordPopupOptions = new SearchSelector.SelectorPopupOptions
|
||||
{
|
||||
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private SearchSelector.SelectorPopupOptions RefillSheet()
|
||||
{
|
||||
return new SearchSelector.SelectorPopupOptions
|
||||
{
|
||||
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowEmotes, Language.Options_ShowEmotes_Name, Language.Options_ShowEmotes_Desc);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||
ImGui.Spacing();
|
||||
|
||||
WordPopupOptions ??= RefillSheet();
|
||||
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
|
||||
WordPopupOptions = RefillSheet();
|
||||
|
||||
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
|
||||
|
||||
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
|
||||
Mutable.BlockedEmotes.Add(newWord);
|
||||
|
||||
using(var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
{
|
||||
if (table)
|
||||
{
|
||||
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
|
||||
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var copiedList = Mutable.BlockedEmotes.ToArray();
|
||||
foreach (var word in copiedList)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(word);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
|
||||
Mutable.BlockedEmotes.Remove(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Emote_EmoteStats);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (EmoteCache.State is EmoteCache.LoadingState.Done)
|
||||
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
|
||||
else
|
||||
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
|
||||
|
||||
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
|
||||
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
{
|
||||
if (emoteTable)
|
||||
{
|
||||
ImGui.TableSetupColumn("##word1");
|
||||
ImGui.TableSetupColumn("##word2");
|
||||
ImGui.TableSetupColumn("##word3");
|
||||
ImGui.TableSetupColumn("##word4");
|
||||
ImGui.TableSetupColumn("##word5");
|
||||
|
||||
foreach (var word in EmoteCache.SortedCodeArray)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
public class Fonts : ISettingsTab
|
||||
{
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_Fonts_Tab + "###tabs-fonts";
|
||||
|
||||
internal Fonts(Configuration mutable)
|
||||
{
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool _)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (!Mutable.FontsEnabled)
|
||||
{
|
||||
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var globalChooser = ImGuiUtil.FontChooser(Language.Options_Font_Name, Mutable.GlobalFontV2, false, ref _);
|
||||
globalChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
Mutable.GlobalFontV2 = r.Result;
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##global"))
|
||||
Mutable.GlobalFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Font_Description, Plugin.PluginName));
|
||||
ImGuiUtil.WarningText(Language.Options_Font_Warning);
|
||||
ImGui.Spacing();
|
||||
|
||||
// LocaleNames being null means it is likely a game font which all support JP symbols
|
||||
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref _, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
|
||||
japaneseChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
Mutable.JapaneseFontV2 = r.Result;
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##japanese"))
|
||||
Mutable.JapaneseFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f };
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_JapaneseFont_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
var italicChooser = ImGuiUtil.FontChooser(Language.Options_ItalicFont_Name, Mutable.ItalicFontV2, true, ref Mutable.ItalicEnabled);
|
||||
italicChooser?.ResultTask.ContinueWith(r =>
|
||||
{
|
||||
if (r.IsCompletedSuccessfully)
|
||||
Mutable.ItalicFontV2 = r.Result;
|
||||
});
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset##italic"))
|
||||
{
|
||||
Mutable.ItalicEnabled = false;
|
||||
Mutable.ItalicFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Italic_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
|
||||
{
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName));
|
||||
|
||||
var range = (int) Mutable.ExtraGlyphRanges;
|
||||
foreach (var extra in Enum.GetValues<ExtraGlyphRanges>())
|
||||
ImGui.CheckboxFlags(extra.Name(), ref range, (int) extra);
|
||||
|
||||
Mutable.ExtraGlyphRanges = (ExtraGlyphRanges) range;
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
|
||||
ImGuiUtil.HelpText(Language.Options_SymbolsFontSize_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class General : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => HellionStrings.Settings_Tab_General + "###tabs-general";
|
||||
|
||||
internal General(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
DrawInputSection();
|
||||
ImGui.Spacing();
|
||||
DrawAudioSection();
|
||||
ImGui.Spacing();
|
||||
DrawPerformanceSection();
|
||||
ImGui.Spacing();
|
||||
DrawLanguageSection();
|
||||
}
|
||||
|
||||
private void DrawInputSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Input_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_KeepInputFocus_Name, ref Mutable.KeepInputFocus);
|
||||
ImGuiUtil.HelpMarker(Language.Options_KeepInputFocus_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAudioSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Audio_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_PlaySounds_Name, ref Mutable.PlaySounds);
|
||||
ImGuiUtil.HelpMarker(Language.Options_PlaySounds_Description);
|
||||
|
||||
ImGui.Checkbox(Language.Options_ShowNoviceNetwork_Name, ref Mutable.ShowNoviceNetwork);
|
||||
ImGuiUtil.HelpMarker(Language.Options_ShowNoviceNetwork_Description);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPerformanceSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Performance_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.InputInt(Language.Options_MaxLinesToShow_Name, ref Mutable.MaxLinesToRender))
|
||||
{
|
||||
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
|
||||
}
|
||||
ImGuiUtil.HelpMarker(Language.Options_MaxLinesToShow_Description);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLanguageSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_General_Language_Heading);
|
||||
if (!tree.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var language in Enum.GetValues<LanguageOverride>())
|
||||
{
|
||||
if (ImGui.Selectable(language.Name()))
|
||||
{
|
||||
Mutable.LanguageOverride = language;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_Language_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var side in Enum.GetValues<CommandHelpSide>())
|
||||
{
|
||||
if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side))
|
||||
{
|
||||
Mutable.CommandHelpSide = side;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var mode in Enum.GetValues<KeybindMode>())
|
||||
{
|
||||
if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode))
|
||||
{
|
||||
Mutable.KeybindMode = mode;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGuiUtil.Tooltip(mode.Tooltip() ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
|
||||
ImGuiUtil.HelpMarker(Language.Options_SortAutoTranslate_Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
// Information-Tab vereint die früheren About- und Changelog-Tabs in
|
||||
// drei kollabierbaren Sektionen. Der About-Inhalt ist 1:1 aus About.cs
|
||||
// übernommen, die Changelog-Render-Logik aus Changelog.cs.
|
||||
internal sealed class Information : ISettingsTab
|
||||
{
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => HellionStrings.Settings_Tab_Information + "###tabs-information";
|
||||
|
||||
private readonly List<string> Translators =
|
||||
[
|
||||
"q673135110", "Akizem", "d0tiKs",
|
||||
"Moonlight_Everlit", "Dark32", "andreycout",
|
||||
"Button_", "Cali666", "cassandra308",
|
||||
"lokinmodar", "jtabox", "AkiraYorumoto",
|
||||
"MKhayle", "elena.space", "imlisa",
|
||||
"andrei5125", "ShivaMaheshvara", "aislinn87",
|
||||
"nishinatsu051", "lichuyuan", "Risu64",
|
||||
"yummypillow", "witchymary", "Yuzumi",
|
||||
"zomsakura", "Sirayuki"
|
||||
];
|
||||
|
||||
internal Information(Configuration mutable)
|
||||
{
|
||||
Mutable = mutable;
|
||||
Translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
DrawVersionInfoSection();
|
||||
ImGui.Spacing();
|
||||
DrawAboutSection();
|
||||
ImGui.Spacing();
|
||||
DrawChangelogSection();
|
||||
}
|
||||
|
||||
private void DrawVersionInfoSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_VersionInfo_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Authors);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Interface.Manifest.Author);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Discord);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, "@j.j_kazama");
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Version);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Interface.Manifest.AssemblyVersion.ToString(3));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAboutSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_About_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Maintainer_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Body);
|
||||
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
||||
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Mission_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_Mission_P1);
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.About_Mission_P2);
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.About_Mission_P3);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_BuiltOn_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P1);
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P2);
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_License_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_License_P1);
|
||||
ImGui.TextUnformatted(HellionStrings.About_License_P2);
|
||||
ImGui.TextUnformatted(HellionStrings.About_License_P3);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.DalamudOrange, HellionStrings.About_SE_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_SE_P1);
|
||||
ImGui.TextUnformatted(HellionStrings.About_SE_P2);
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Localization_Heading);
|
||||
ImGui.TextUnformatted(HellionStrings.About_Localization_P1);
|
||||
ImGui.TextUnformatted(HellionStrings.About_Localization_P2);
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var translatorTree = ImRaii.TreeNode(HellionStrings.About_Translators_TreeNode))
|
||||
{
|
||||
if (translatorTree)
|
||||
{
|
||||
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
||||
foreach (var translator in Translators)
|
||||
ImGui.TextUnformatted(translator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawChangelogSection()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_Changelog_Heading);
|
||||
if (!tree.Success)
|
||||
return;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_PrintChangelog_Name, ref Mutable.PrintChangelog);
|
||||
ImGuiUtil.HelpMarker(Language.Options_PrintChangelog_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
var changelog = Plugin.Interface.Manifest.Changelog;
|
||||
if (changelog == null)
|
||||
return;
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Changelog_Header);
|
||||
ImGui.TextUnformatted($"Version {Plugin.Interface.Manifest.AssemblyVersion.ToString(3)}");
|
||||
ImGui.Spacing();
|
||||
foreach (var sentence in changelog.Split("\n"))
|
||||
{
|
||||
if (sentence == string.Empty)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var indented = sentence.StartsWith('-') || sentence.StartsWith(" -");
|
||||
using var indent = ImRaii.PushIndent(10.0f, true, indented);
|
||||
ImGui.TextUnformatted(sentence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Miscellaneous(Configuration mutable) : ISettingsTab
|
||||
{
|
||||
private Configuration Mutable { get; } = mutable;
|
||||
public string Name => Language.Options_Miscellaneous_Tab + "###tabs-miscellaneous";
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var language in Enum.GetValues<LanguageOverride>())
|
||||
if (ImGui.Selectable(language.Name()))
|
||||
Mutable.LanguageOverride = language;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Language_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var side in Enum.GetValues<CommandHelpSide>())
|
||||
if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side))
|
||||
Mutable.CommandHelpSide = side;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
|
||||
{
|
||||
if (combo.Success)
|
||||
{
|
||||
foreach (var mode in Enum.GetValues<KeybindMode>())
|
||||
{
|
||||
if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode))
|
||||
Mutable.KeybindMode = mode;
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(mode.Tooltip() ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
|
||||
ImGuiUtil.HelpText(Language.Options_SortAutoTranslate_Description);
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user