Compare commits
420
Commits
v1.5.1
...
522545c7a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522545c7a2 | ||
|
|
63c68327dd | ||
|
|
0f9ced8f86 | ||
|
|
60e3dd87a2 | ||
|
|
5ab4312cad | ||
|
|
c1d6ca3bbd | ||
|
|
4ab9202533 | ||
|
|
5e15d34eeb | ||
|
|
b9feb8650f | ||
|
|
0f9858a3d3 | ||
|
|
a3379818eb | ||
|
|
d5c7db9f43 | ||
|
|
6e24885241 | ||
|
|
16730d5ed9 | ||
|
|
39e2d91288 | ||
|
|
3309386fbe | ||
|
|
38c6707970 | ||
|
|
a4c4e15c3b | ||
|
|
226174bf12 | ||
|
|
e316a9b400 | ||
|
|
97d31bb8e4 | ||
|
|
89c73be71c | ||
|
|
16557213cd | ||
|
|
5b738e6885 | ||
|
|
52237fda7e | ||
|
|
5958bceb1c | ||
|
|
6fadbb1659 | ||
|
|
dcc8ff1867 | ||
|
|
9e62ed762d | ||
|
|
eeffb5bc6b | ||
|
|
12a445e08f | ||
|
|
8a9692a71d | ||
|
|
3e46600fc3 | ||
|
|
d7a308b522 | ||
|
|
6bdfecb1df | ||
|
|
df6296024b | ||
|
|
6449a51daa | ||
|
|
0abbbc8f8a | ||
|
|
de094a8d4f | ||
|
|
b57ca23b9f | ||
|
|
64f48b1131 | ||
|
|
3b5001e616 | ||
|
|
439c919d77 | ||
|
|
e2b6e7a992 | ||
|
|
80ec7450c8 | ||
|
|
b63e1eda9b | ||
|
|
1604186aa1 | ||
|
|
8fea9113b9 | ||
|
|
0399b68d8c | ||
|
|
63d1b34b00 | ||
|
|
81e4c9367a | ||
|
|
ba16ab59e3 | ||
|
|
611dd368cb | ||
|
|
a841942b41 | ||
|
|
ad7421fbed | ||
|
|
56a9f3f474 | ||
|
|
e810d2479d | ||
|
|
dfc0cda806 | ||
|
|
100290ea4d | ||
|
|
74f9ff7f16 | ||
|
|
0551116a0c | ||
|
|
64f7d9b975 | ||
|
|
81456c981b | ||
|
|
0916f8d1fb | ||
|
|
147034bda5 | ||
|
|
34e343d8f2 | ||
|
|
6d2bb95528 | ||
|
|
58830aecec | ||
|
|
fc67c0852a | ||
|
|
b388dcb2de | ||
|
|
e0c9efca05 | ||
|
|
fdb1a98519 | ||
|
|
9ea9e96145 | ||
|
|
e7b76fb21b | ||
|
|
bbfb9fc630 | ||
|
|
3a1b863def | ||
|
|
5991f49c59 | ||
|
|
b3c0ec73ca | ||
|
|
83306f1f47 | ||
|
|
6bb020547d | ||
|
|
8e38e3e805 | ||
|
|
6829a80ff2 | ||
|
|
e8d06e05fa | ||
|
|
3cb20b6f65 | ||
|
|
75c4acd19a | ||
|
|
1bb968211b | ||
|
|
4f3bdc3c7b | ||
|
|
9b634f54e9 | ||
|
|
4f4f5fc86a | ||
|
|
06ef0bfb1c | ||
|
|
0279a1a9d6 | ||
|
|
1987d745d8 | ||
|
|
e24ea79302 | ||
|
|
1ab7ba8377 | ||
|
|
94af81a961 | ||
|
|
d59ee62223 | ||
|
|
90bf986f76 | ||
|
|
22de2de234 | ||
|
|
6a3bbe2357 | ||
|
|
d0eb2934ed | ||
|
|
125a57167e | ||
|
|
8bf351ba02 | ||
|
|
f205dd4e54 | ||
|
|
d1e12ce866 | ||
|
|
051fd64857 | ||
|
|
9c7be4106d | ||
|
|
d30922af8c | ||
|
|
04f8f1ace8 | ||
|
|
33bcc38581 | ||
|
|
ae9b503776 | ||
|
|
57780351d4 | ||
|
|
7df18bd552 | ||
|
|
05b203c85c | ||
|
|
c1f1c0563c | ||
|
|
8b96ffb2ec | ||
|
|
8ebed6846d | ||
|
|
76416afe3a | ||
|
|
ced7ea0c55 | ||
|
|
2f4518a6b1 | ||
|
|
5a4c3b6707 | ||
|
|
18d43f9afa | ||
|
|
6397916712 | ||
|
|
588b886537 | ||
|
|
64db8986e6 | ||
|
|
0c3023c8cb | ||
|
|
c5a0c4d0d7 | ||
|
|
1c78a84b9b | ||
|
|
f999036e50 | ||
|
|
fa15130468 | ||
|
|
688c05cd77 | ||
|
|
cade398b4e | ||
|
|
cbc4b17e1e | ||
|
|
86992c70f2 | ||
|
|
5b51f7dcfd | ||
|
|
9a31bbba03 | ||
|
|
81b68a1681 | ||
|
|
af242db6f6 | ||
|
|
2686a8f74b | ||
|
|
30c495849d | ||
|
|
ca60c851cd | ||
|
|
d79188c7b0 | ||
|
|
929188e5eb | ||
|
|
891f8ac0ec | ||
|
|
bfc61909cf | ||
|
|
0d6877ddc6 | ||
|
|
a7ed8e1146 | ||
|
|
f2dd49e454 | ||
|
|
918cdc8111 | ||
|
|
5696eac55a | ||
|
|
e4659bc597 | ||
|
|
5caa266287 | ||
|
|
9936638bb3 | ||
|
|
293d56bdbe | ||
|
|
eacefb355d | ||
|
|
464bd52887 | ||
|
|
1e8a60ac80 | ||
|
|
10b345821f | ||
|
|
460217459c | ||
|
|
d6ad81e8ca | ||
|
|
31fa410875 | ||
|
|
bf0c3e2bd7 | ||
|
|
28b97b3f70 | ||
|
|
0ef33934a2 | ||
|
|
39a8e95581 | ||
|
|
3583dfc032 | ||
|
|
c34024a18b | ||
|
|
eaed0b13e0 | ||
|
|
2b4243599e | ||
|
|
24dff3cc2e | ||
|
|
89c66e0d3d | ||
|
|
d2da51a4f7 | ||
|
|
99dca8cb31 | ||
|
|
ce5973aea9 | ||
|
|
fd8e5a1a17 | ||
|
|
d50f2cea90 | ||
|
|
de9d11ba4a | ||
|
|
618e029ff4 | ||
|
|
93f4fbba72 | ||
|
|
1d69d0cc30 | ||
|
|
b048a51534 | ||
|
|
7d2fd1ab65 | ||
|
|
2c5c40524d | ||
|
|
0508a05bab | ||
|
|
32013babaf | ||
|
|
430c8f235a | ||
|
|
68c4e28495 | ||
|
|
458df3b4bd | ||
|
|
6733c7f92e | ||
|
|
fa20b53455 | ||
|
|
ea3f00f107 | ||
|
|
5bdf4217d6 | ||
|
|
97e58934d6 | ||
|
|
f05d7104af | ||
|
|
e98cefa0b2 | ||
|
|
c1765ce8ca | ||
|
|
7ead120213 | ||
|
|
f5ba1c0246 | ||
|
|
b372ab84c3 | ||
|
|
c27785f9f6 | ||
|
|
6f71b09331 | ||
|
|
49f5119b17 | ||
|
|
6578c10b13 | ||
|
|
3878869904 | ||
|
|
88491902eb | ||
|
|
47a49de8c0 | ||
|
|
7b6871fea4 | ||
|
|
8e2d333130 | ||
|
|
4db2ad99b9 | ||
|
|
a73f4d0d0c | ||
|
|
b397591ba4 | ||
|
|
6813b80d58 | ||
|
|
0352e6c199 | ||
|
|
8cbf9c554f | ||
|
|
18834cddae | ||
|
|
fe0414dd2b | ||
|
|
ec92cf2c04 | ||
|
|
512533ed3a | ||
|
|
a2d8a9c223 | ||
|
|
dcf089b886 | ||
|
|
c4562dd6a0 | ||
|
|
74f7bb3fc1 | ||
|
|
8afcb87624 | ||
|
|
f9ed487ae2 | ||
|
|
72099c8871 | ||
|
|
fe766285ad | ||
|
|
8db2dee38c | ||
|
|
a38afbcf67 | ||
|
|
5dfe8e3b49 | ||
|
|
0ca8513065 | ||
|
|
ad635c77c1 | ||
|
|
593eb30c9f | ||
|
|
11ef5951a4 | ||
|
|
c28e3f72a1 | ||
|
|
dc51e295ea | ||
|
|
540cb7ac52 | ||
|
|
1f2c354471 | ||
|
|
ac7f74b227 | ||
|
|
c7047407f5 | ||
|
|
e5a17ee798 | ||
|
|
83de7cd989 | ||
|
|
51c8f79846 | ||
|
|
d40b120b91 | ||
|
|
99901b64ed | ||
|
|
7ef1337ea0 | ||
|
|
a13713752e | ||
|
|
a9f42e32c5 | ||
|
|
e372afc8ac | ||
|
|
b0bee25770 | ||
|
|
92f1736ea9 | ||
|
|
83b1708d5d | ||
|
|
8fcb10cf51 | ||
|
|
71cf6234b5 | ||
|
|
78d56f0e3e | ||
|
|
ca00f528d6 | ||
|
|
a7a5aee982 | ||
|
|
caacb87a6a | ||
|
|
336f722eef | ||
|
|
7792b327dc | ||
|
|
6581943bf1 | ||
|
|
c84891e75d | ||
|
|
6af9e05664 | ||
|
|
ea549ebcd0 | ||
|
|
d33c25e77a | ||
|
|
ad892cbcb6 | ||
|
|
a9e70ce2af | ||
|
|
db47708264 | ||
|
|
e786257cb3 | ||
|
|
b221a6e418 | ||
|
|
0319636fc5 | ||
|
|
b954a19b67 | ||
|
|
29fb4b92eb | ||
|
|
24d3f69041 | ||
|
|
f6749d206b | ||
|
|
d1bfddd9b8 | ||
|
|
cba6a16f8e | ||
|
|
c652a1c450 | ||
|
|
830d247eda | ||
|
|
8431fbcf80 | ||
|
|
931a152d00 | ||
|
|
4fb5ee6128 | ||
|
|
2e143686af | ||
|
|
d6012a9459 | ||
|
|
121c96f79e | ||
|
|
6da8ac93fe | ||
|
|
abc0617d58 | ||
|
|
63f5a28834 | ||
|
|
e46b6a7520 | ||
|
|
2067a54467 | ||
|
|
61a1e6bf87 | ||
|
|
dec0daf30c | ||
|
|
7e541ac842 | ||
|
|
94fdef38ad | ||
|
|
01fc69efda | ||
|
|
f5f9a4e4da | ||
|
|
ced22d73b2 | ||
|
|
d2b2ebf17f | ||
|
|
125a41c6a0 | ||
|
|
dc8d2ae8b7 | ||
|
|
22bf1dd110 | ||
|
|
4595de5efc | ||
|
|
730e15d108 | ||
|
|
545ddfbfcc | ||
|
|
f510d01f46 | ||
|
|
490da3e908 | ||
|
|
262fb3022a | ||
|
|
0512d4c9d2 | ||
|
|
1f9afe182a | ||
|
|
2c23a88e9d | ||
|
|
2507ed5197 | ||
|
|
7e933cb8ba | ||
|
|
26c395419a | ||
|
|
12bf83b826 | ||
|
|
6bbdc67089 | ||
|
|
13ca241ac7 | ||
|
|
304e1aab20 | ||
|
|
cce56610fc | ||
|
|
eed919e961 | ||
|
|
21689655da | ||
|
|
c6f7266194 | ||
|
|
a73cad1534 | ||
|
|
bb37e19176 | ||
|
|
ffef5634ed | ||
|
|
947c4b2061 | ||
|
|
d156ff4c56 | ||
|
|
7979568165 | ||
|
|
93bfd408bc | ||
|
|
e01de0403a | ||
|
|
11eb7b9e90 | ||
|
|
4f81cd1f24 | ||
|
|
8e7149cadc | ||
|
|
2f099fd4e1 | ||
|
|
52b0fa7c67 | ||
|
|
0109bfd222 | ||
|
|
cf4705e01f | ||
|
|
c9e746a8e3 | ||
|
|
ec02a5f381 | ||
|
|
576cd6dafd | ||
|
|
0fc2512f3e | ||
|
|
36afce21e5 | ||
|
|
bdfb1298ad | ||
|
|
eb95968750 | ||
|
|
6ab2e9cece | ||
|
|
0e0c563608 | ||
|
|
9c490fa066 | ||
|
|
44c14ace2c | ||
|
|
53ed154103 | ||
|
|
620dfe9ea0 | ||
|
|
cd9e43c183 | ||
|
|
b8299a90ca | ||
|
|
d89540de52 | ||
|
|
1d3b429f1b | ||
|
|
c640a05a8a | ||
|
|
73a8532e26 | ||
|
|
32840623ff | ||
|
|
2acac78b4c | ||
|
|
ce4c5d9cf9 | ||
|
|
4cf7aa5501 | ||
|
|
0da4751b0f | ||
|
|
ee39fd0eec | ||
|
|
78efd654e6 | ||
|
|
d3cea8c6c0 | ||
|
|
3058e6bc6d | ||
|
|
8a8c6ccae2 | ||
|
|
eafa20748c | ||
|
|
b3fc96f424 | ||
|
|
a18ac130b3 | ||
|
|
c652b102fc | ||
|
|
ba4cd918da | ||
|
|
a6e2a75422 | ||
|
|
d05770fd6d | ||
|
|
921dd701c4 | ||
|
|
ba30b1e742 | ||
|
|
5771573a94 | ||
|
|
d4bcbc93e2 | ||
|
|
ca801a006a | ||
|
|
cc1c05add0 | ||
|
|
969d5e6aa6 | ||
|
|
aaeca76bfd | ||
|
|
4f6c916bd9 | ||
|
|
ce7dda9e48 | ||
|
|
80699b27e4 | ||
|
|
3296a12516 | ||
|
|
81123ccddf | ||
|
|
636a62814f | ||
|
|
b5aebaad35 | ||
|
|
bd75f2453c | ||
|
|
c909d1646b | ||
|
|
5781be2e41 | ||
|
|
65fea0e5f5 | ||
|
|
3de6e4a3cb | ||
|
|
e0289962b1 | ||
|
|
95375c8516 | ||
|
|
36ea8ddcfc | ||
|
|
246f0e2511 | ||
|
|
2e81c42e3b | ||
|
|
a46d89c197 | ||
|
|
57b6ead003 | ||
|
|
a42cc2a97e | ||
|
|
96ff4ddfd8 | ||
|
|
0bfe3a62cb | ||
|
|
01a7f9b4ec | ||
|
|
0237602ab7 | ||
|
|
a600f014eb | ||
|
|
a35067f80a | ||
|
|
74b07519f5 | ||
|
|
8dade8c4b2 | ||
|
|
35e8d3a7fe | ||
|
|
38586db9d8 | ||
|
|
c357873604 | ||
|
|
67bec11f10 | ||
|
|
35efdd4628 | ||
|
|
271a6ae650 | ||
|
|
003bd5c695 | ||
|
|
e1f84a9b10 | ||
|
|
9745abea0c | ||
|
|
1e418ab86f | ||
|
|
1c820b7f53 | ||
|
|
2cc260170e | ||
|
|
de86084dbc | ||
|
|
f56b968768 |
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
|
||||
@@ -147,10 +147,14 @@ jobs:
|
||||
}
|
||||
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||
|
||||
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||
# Sharing the same `url` tells Discord to render both embeds as a
|
||||
# single contiguous card block. The title sits on the first embed,
|
||||
# the footer + timestamp on the last so it reads as one post.
|
||||
# ---------- Embed-Payload bauen (zwei gestapelte Embeds) ----------
|
||||
# Discord MERGES embeds in one message that share the same `url`
|
||||
# (the image-gallery merge) and then renders only the FIRST embed's
|
||||
# description — every following embed contributes images only. So
|
||||
# only the DE embed carries the release URL; the EN embed stays
|
||||
# url-less, which makes Discord stack both as separate cards with
|
||||
# both descriptions visible. Title sits on the first embed, footer
|
||||
# + timestamp on the last so it still reads as one post.
|
||||
$payload = [ordered]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
@@ -167,7 +171,8 @@ jobs:
|
||||
description = $deDesc
|
||||
},
|
||||
[ordered]@{
|
||||
url = $releaseUrl
|
||||
# Deliberately no `url` — a shared url would make Discord
|
||||
# merge this embed into the first and drop the EN body.
|
||||
color = 12730636
|
||||
description = $enDesc
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
|
||||
@@ -22,9 +22,9 @@ on:
|
||||
- 'v*'
|
||||
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||
# release-action reads GITHUB_REF directly and rejects anything that
|
||||
# does not start with refs/tags/.
|
||||
# ref step below hard-fails if a non-tag ref is selected: the release
|
||||
# name and body are both derived from the tag, so a branch ref would
|
||||
# publish a release named after a branch.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -37,11 +37,8 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||
# does not declare a tag_name input). Validate up-front so manual
|
||||
# dispatches from a branch ref fail loud here instead of burning
|
||||
# a full build before the final step errors out with "ref X is
|
||||
# not a tag".
|
||||
# Validate up-front so a manual dispatch from a branch ref fails loud
|
||||
# here instead of burning a full build before the publish step notices.
|
||||
- name: Validate tag ref
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
|
||||
@@ -54,7 +51,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
@@ -156,28 +153,70 @@ jobs:
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
|
||||
# release-action@main only declares files/title/body/pre_release/
|
||||
# draft/api_key/insecure as inputs (see its action.yml). It silently
|
||||
# ignores anything else, including body_path and tag_name. The tag
|
||||
# itself comes from GITHUB_REF, the body must be passed inline via
|
||||
# body:, so we re-emit release-body.md as a step output first.
|
||||
- name: Expose release body for release-action
|
||||
id: body
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo 'content<<RELEASE_BODY_EOF'
|
||||
cat release-body.md
|
||||
echo 'RELEASE_BODY_EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one with latest.zip attached
|
||||
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
||||
# Actions has Gitea-API scope and is sufficient for release write.
|
||||
# The tag comes from GITHUB_REF, the body from the step above. Posted with
|
||||
# curl rather than gitea.com/actions/release-action, which declares
|
||||
# `using: go` and has to be compiled by the runner -- act cannot do that
|
||||
# here and the step dies with exec: "go": executable file not found, exit
|
||||
# 127, after a build that otherwise succeeded. This runs as a plain shell
|
||||
# step in the job image, which has curl and python3.
|
||||
#
|
||||
# Idempotent on purpose: a re-run against an existing release reuses it and
|
||||
# replaces the asset instead of failing on the duplicate.
|
||||
- name: Attach to Gitea release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body: ${{ steps.body.outputs.content }}
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ZIP_PATH: ${{ steps.locate.outputs.path }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
|
||||
auth="Authorization: token ${GITEA_TOKEN}"
|
||||
|
||||
# Responses land in a file before anything reads them, rather than
|
||||
# being piped straight into an interpreter. The interpreter is inline
|
||||
# either way and the server only ever supplies data, but a scanner
|
||||
# cannot tell those apart from curl-pipe-shell -- and holding the
|
||||
# response makes it inspectable when a call misbehaves.
|
||||
get_release_id() {
|
||||
if curl -sf -H "$auth" "$api/releases/tags/${TAG_NAME}" -o release.json; then
|
||||
python3 -c 'import json; print(json.load(open("release.json")).get("id",""))'
|
||||
fi
|
||||
}
|
||||
|
||||
rel_id="$(get_release_id || true)"
|
||||
|
||||
if [ -z "$rel_id" ]; then
|
||||
python3 - <<'PYCREATE' > create.json
|
||||
import json, os
|
||||
body = open("release-body.md", encoding="utf-8").read()
|
||||
print(json.dumps({
|
||||
"tag_name": os.environ["TAG_NAME"],
|
||||
"name": os.environ["TAG_NAME"],
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PYCREATE
|
||||
curl -sf -X POST -H "$auth" -H "Content-Type: application/json" \
|
||||
-d @create.json "$api/releases" -o created.json
|
||||
rel_id="$(python3 -c 'import json; print(json.load(open("created.json"))["id"])')"
|
||||
echo "Created release $rel_id for ${TAG_NAME}"
|
||||
else
|
||||
echo "Reusing release $rel_id for ${TAG_NAME}"
|
||||
fi
|
||||
|
||||
# A same-named asset from an earlier attempt has to go, or the upload
|
||||
# collides with it. This is the state a recovery run finds.
|
||||
curl -sf -H "$auth" "$api/releases/${rel_id}/assets" -o assets.json
|
||||
old_id="$(python3 -c 'import json; print(next((a["id"] for a in json.load(open("assets.json")) if a["name"]=="latest.zip"), ""))')"
|
||||
if [ -n "$old_id" ]; then
|
||||
curl -sf -X DELETE -H "$auth" "$api/releases/${rel_id}/assets/${old_id}"
|
||||
echo "Replaced existing latest.zip (asset $old_id)"
|
||||
fi
|
||||
|
||||
curl -sf -X POST -H "$auth" \
|
||||
-F "attachment=@${ZIP_PATH};filename=latest.zip" \
|
||||
"$api/releases/${rel_id}/assets?name=latest.zip" -o uploaded.json
|
||||
python3 -c 'import json; a=json.load(open("uploaded.json")); print("Attached", a["name"], a["size"], "bytes")'
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
name: Security
|
||||
|
||||
# Ruft den zentralen Scan-Workflow in security-workflows auf
|
||||
# (Semgrep SAST + Trivy filesystem scan).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
@@ -11,10 +15,6 @@ jobs:
|
||||
scan:
|
||||
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
||||
with:
|
||||
# MessageStore.cs uses string-interpolation in CommandText for table
|
||||
# names and clause-joins that come from internal code constants, not
|
||||
# user input. Values are bound via SqlParameter, the SQL surface is
|
||||
# local-only inside a Dalamud plugin. Semgrep matches the pattern
|
||||
# without dataflow, so it flags those eight call sites; CodeQL
|
||||
# would not. Suppressed for this repo only.
|
||||
# MessageStore.cs interpoliert SQL-Strings, die plugin-lokal sicher sind;
|
||||
# Semgrep matcht das Pattern, CodeQL mit Datenflussanalyse nicht.
|
||||
semgrep-exclude-rules: 'csharp.lang.security.sqli.csharp-sqli.csharp-sqli'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
subtitle: "First-Run Wizard — neu in 4 Steps, Roleplay-Profil neu"
|
||||
versionsnatur: "UX-Patch"
|
||||
---
|
||||
- **Vier Steps statt Single-Page.** Der First-Run-Wizard öffnet jetzt in vier Bühnen: Willkommen → Privacy-Profil → Power-Settings → Fertig. Pagination-Dots in Forge-Bronze oben rechts, Back/Skip/Next im Footer. Standardgröße 720×480 (Min 600×400) und der Fuchs-Banner sitzt als zugeklappter TreeNode oben in Step 1, damit die Einleitung im Fokus bleibt.
|
||||
- **Neues Privacy-Profil „Roleplay".** Datensparsamkeit plus Sagen und beide Emote-Typen für Story-Logs. Schreien und Rufen bleiben außen vor, Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage. Privacy-Picker wird zum 2×2-Grid, Casual bleibt mit ★-Marker als Empfehlung.
|
||||
- **Power-Settings sichtbar.** Bislang versteckte Defaults bekommen eine eigene Bühne: Vorherige Session laden, Filter inkl. alter Messages, N Tell-Messages vorladen, Compact-Density, Prettier-Timestamps und Theme-Picker für die 10 Built-in-Themes. Keine neuen Settings, nur das Bestehende sauber sichtbar.
|
||||
- **Staged-Commit und Test-Hint auf der Fertig-Bühne.** Auswahl wird erst beim Klick auf „Fertig ✓" geschrieben. „Später entscheiden" oder X-Close lässt die bestehende Config unangetastet, ein nicht angefasster Step behält die alten Werte. Direkt darunter sichtbar: „Tipp /tell <Spielername>", plus die aktuelle Preload-Zahl aus Step 3 als Hinweis auf den Auto-Tell-Tab-Spawn.
|
||||
- **Bestehende User sehen den neuen Wizard einmal.** Wer schon v1.5.1 hatte, bekommt den Multi-Step-Flow beim ersten v1.5.2-Boot aufgepoppt. Neues Config-Feld `WizardLastShownVersion` triggert das einmalig pro Wizard-Rework; Skip oder Finish reicht und danach öffnet er nicht mehr automatisch.
|
||||
- **Unter der Haube.** Pure-Helper-Tests für alle vier Profile-Sets in der Build-Suite (zwölf neue Facts), plus ein WizardStateSmokeStep für `/xlperf`. Migration v17 bleibt, nur ein optionales Config-Feld kommt dazu.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
subtitle: "24 Sprachen, Inter Light statt Exo 2, HITCH 74 → 20 ms"
|
||||
versionsnatur: "Localisation + Font-Stack"
|
||||
---
|
||||
- **24 wählbare UI-Sprachen.** Aus dem ursprünglich nur als FR-Lokalisierung geplanten Cycle ist eine breite Welle geworden: Catalan, Czech, Danish, Dutch, English, Finnish, French, German, Greek, Hungarian, Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese (BR), Portuguese (PT), Romanian, Russian, Spanish, Swedish, Turkish, Ukrainian, Simplified Chinese, Traditional Chinese. Dropdown sortiert alphabetisch nach Endonym, „None" oben angepinnt. Nicht-native Übersetzungen sind AI-assisted und für Community-Review im Forge-Discord markiert.
|
||||
- **Inter Light statt Exo 2 als bundled Schrift.** Plus NotoSansCjkRegular als dritte Merge-Schicht. Damit deckt der Stack Latin Extended-A/B, Greek polytonic, Cyrillic Supplement und CJK (inkl. Hangul, Simplified-Han nach Reform) ab — die nicht-vanilla-FFXIV-Sprachen waren mit Exo 2 nicht lesbar.
|
||||
- **HITCH 74 → ~20 ms als Side-Effect.** Der UiBuilder-First-Frame-Lag lag seit v1.4.x stabil bei 74 ms; v1.5.1 wollte ihn in Richtung 7 ms ziehen, fiel als „Hypothese zu optimistisch" durch. Echter Grund: `Plugin.cs:937` push'te `RegularFont` nur wenn `FontsEnabled` true war — die „Mitgelieferte Schrift verwenden"-Logik setzte `FontsEnabled = false` mit, der bundled-Pfad war die ganze v1.5.x-Reihe tot, FFXIVs Axis-Font übernahm und kostete ~50 ms extra. Fix routet `RegularFont` jetzt auch über `UseHellionFont`. Median ~20 ms im 5-Reload-Stresstest (17.9-23.6 ms, Linux/Wine; Windows-Baseline steht aus).
|
||||
- **Glyph-Ranges aktivieren sich automatisch beim Sprachwechsel** plus eine One-Shot-Migration für User die schon eine non-Latin-Sprache eingestellt hatten. Neue WarningText unter dem Sprach-Dropdown weist darauf hin, dass FFXIVs Chat-Engine offiziell nur EN/DE/FR/JA-Glyphen rendert — andere Schriften können in der Game-Eingabe Garbled-Output zeigen.
|
||||
- **Unter der Haube.** Drei-Layer-Font-Stack, zwei neue ExtraGlyphRanges-Flags (`LatinExtended`, `Greek`), `LanguageOverride`-Enum wächst um zehn Locales plus drei reaktivierte (Italian, Korean, Norwegian mit `nb`). Append-only damit User-Configs stabil bleiben. Migration v17 bleibt.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
subtitle: "Theme-Crossfade, Quick-Picker, Hover-Animationen"
|
||||
versionsnatur: "Polish & Motion"
|
||||
---
|
||||
- **Theme-Crossfade.** Theme-Wechsel blenden jetzt sanft über rund 300 ms ineinander, statt hart umzuschalten. Alle Hellion-Flächen gleiten mit: Sidebar, Titel, Buttons, Tabs, Scrollbar, Trennlinien. Der Fenster-Hintergrund snappt bewusst weiter, damit das Per-Window-Deckkraft-Setting aus Dalamuds Pinning-Menü unangetastet bleibt.
|
||||
- **Header-Quick-Picker.** Neuer Paletten-Button links vom Zahnrad im Chat-Header. Ein Klick öffnet ein kompaktes Popup mit zwei Sektionen: alle Built-in- und Custom-Themes sowie alle Tabs. Der aktive Eintrag trägt ein Häkchen, ein Klick wechselt ohne das Popup zu schließen. So lassen sich mehrere Wechsel hintereinander erledigen, ohne den Umweg über die Einstellungen.
|
||||
- **Sanfte Hover-Animationen.** Sidebar-Icons faden bei Hover sanft von gedimmt auf volle Deckkraft. Card-Mode-Trennlinien heben sich beim Überfahren einer Zeile für den ganzen Tab dezent ab. Beides framerate-unabhängig gerechnet, also auch bei Wine-Stall-Frames stabil.
|
||||
- **Bewegung reduzieren.** Neuer Toggle im Tab für Theme und Layout. Er deaktiviert Crossfade, Hover-Animationen und das Pulsieren ungelesener Tabs für alle, die eine statische Oberfläche bevorzugen.
|
||||
- Drei P3-Items plus der Accessibility-Toggle, kein Schema-Bump, keine Migration. Eine kleine Polish-Welle vor den größeren Cycles.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
subtitle: "Backlog-Sync Tab-Features"
|
||||
versionsnatur: "Bundle-Patch (Hälfte 1 von 2)"
|
||||
---
|
||||
- **Fehlgeschlagener Tell.** Geht ein gesendeter Tell nicht durch (Empfänger offline, in einer Instanz oder blockiert), erscheint jetzt ein Warn-Toast statt dass die Systemmeldung durchrauscht. Abschaltbar in den Einstellungen unter Chat.
|
||||
- **Ton pro Tab.** Jeder Chat-Tab kann einen Benachrichtigungston spielen, wenn eine Nachricht eintrifft, während ein anderer Tab aktiv ist. Zur Wahl stehen die 16 Spiel-Chat-Sounds oder drei mitgelieferte Hellion-Sounds, mit einem Vorhör-Knopf. Standardmäßig aus, hört auf den globalen Sound-Schalter.
|
||||
- **Tab umbenennen.** Das Umbenennen-Feld im Rechtsklick-Menü fokussiert sich beim Öffnen von selbst und nimmt jetzt bis zu 512 Zeichen.
|
||||
- **Sprung ans Ende.** In der Chat-Kopfleiste erscheint ein Knopf, sobald man vom aktuellen Ende weggescrollt ist. Ein Klick springt zurück zur jüngsten Nachricht.
|
||||
- **Karten- und Item-Links.** Kartenmarkierung und verlinktes Item lassen sich aus dem Rechtsklick-Menü der Chat-Eingabe einfügen.
|
||||
- **Fuchs-Banner.** Das Hellion-Forge-Fuchs-Motiv im Einrichtungs-Assistenten und im Informations-Tab ist jetzt ein echtes Bild statt ASCII-Kunst.
|
||||
- Schema-Bump auf v18, rein additiv.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
subtitle: "Settings Overhaul + Filter & Notification Polish"
|
||||
versionsnatur: "Settings-Overhaul-Release"
|
||||
---
|
||||
- **Settings komplett neu strukturiert** — die zehn alten Tabs sind auf sieben zusammengefasst (Allgemein, Aussehen, Chat, Fenster, Kanäle, Daten & Privatsphäre, Über). Jeder Tab gliedert sich jetzt in Sektionen, die beim Reingehen eingeklappt sind. Controls innerhalb einer Sektion sind nach Typ gruppiert. Tabs-Tab im Per-Tab-Panel ebenfalls in Sub-Sektionen aufgeteilt.
|
||||
- **Absender-Namen anpassbar** — neue Optionen in Chat → Nachrichten für das Namensformat (Voll / Vorname / Initialen) und das Welt-Suffix (Nie / Andere Welten / Immer).
|
||||
- **Pre-Send-Warnung für Plugin-Symbole** — beim Senden einer Nachricht mit Symbolen, die nur HellionChat-User sehen, kommt eine Warnung. Verhindert leere Kästchen bei anderen.
|
||||
- **Getrennte Fenster-Deckkraft** — Aktiv vs. Inaktiv. Aktiv wie bisher; Inaktiv über einen zweiten Slider unter Aussehen → Fenster-Stil.
|
||||
- **Lautstärke für eigene Notification-Sounds** — Slider in Allgemein → Sound, im Kanäle-Tab pro Tab nochmal angezeigt. Wirkt nur auf die drei mitgelieferten Custom-Sounds, die 16 Game-Sounds bleiben unverändert.
|
||||
- **Regex-Filter pro Tab gestrichen** — kurz dabei, dann verworfen: der eingebaute FFXIV-Blackword-Filter deckt das ab.
|
||||
- **Lokalisierung erweitert** — neue Section-Titel und v1.5.6-Controls in allen 24 Sprachen, maschinell übersetzt. Native-Review läuft weiter über den Hellion Forge Discord.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
subtitle: "Rebuilt, Repaired, Reset"
|
||||
versionsnatur: "Major-Release mit Config-Reset"
|
||||
---
|
||||
- **Deine Einstellungen werden zurückgesetzt.** Neun Zyklen Umbau haben gespeicherte Werte hinterlassen, die auf Oberflächen zeigen, die es nicht mehr gibt. Neu anzufangen ist der einzige Weg, dass alle dieselben Vorgaben haben. **Dein Nachrichtenverlauf bleibt unberührt**, der liegt in einer eigenen Datenbank. Die alten Einstellungen liegen als `HellionChat.json.pre-2.0.0.bak` daneben.
|
||||
- **Behoben, und mehrere davon haben Daten verloren oder versteckt:** Die rückwirkende Bereinigung ließ sich nie anwenden. Das Verdichten der Datenbank schlug fehl und meldete danach, es sei nichts gelöscht worden, während alles weg war. Gelöschte Nachrichten blieben im Suchindex. Angepinnte Flüster-Tabs kamen eine ganze Sitzung lang leer hoch. Der Export schrieb ungültiges JSON. Ein Auskoppelfenster mit Titelleiste ließ sich nicht schließen. Ein 403 eines Emote-Dienstes riss alle 65 funktionierenden Emotes mit.
|
||||
- **Was sich am Speichern ändert:** Das Kanalraster entscheidet jetzt allein. Bisher griff die Unbekannt-Absicherung auch bei bekannten Kanälen, ein abgewählter Kanal wurde also trotzdem geschrieben. Wen das betraf, der speichert ab jetzt weniger. Nichts in der Datenbank wird angefasst.
|
||||
- **Jedes Fenster zeichnet das Plugin selbst** und alle sprechen eine Sprache: Struktur trägt die Typografie, alles Drückbare bekommt eine Fläche, jede Farbe wird gegen ihren Untergrund gemessen. Dazu benannte Typo-Rollen, Zeitstempel in eigener Spalte, kursive Systemmeldungen.
|
||||
- **Wieder erreichbar:** Export, Tab-Editor, Datenbankpflege und Anpinnen hatten beim Umbau ihre Zugänge verloren. Der Screenshot-Modus erreichte eine von vier Flächen mit Tab-Namen, jetzt alle vier. Der DSGVO-Hinweis beim Profil "Volle Historie" war in 25 Sprachen übersetzt und seit Mai unsichtbar.
|
||||
- **Gestrichen:** sechs Einstellungen mit Regler, gespeichertem Wert und ohne jeden Leser. Ein Regex-Filter pro Tab, den der spieleigene Wortfilter abdeckt. Ein Assistenten-Häkchen, das abgefragt, als angewendet gemeldet und nie gelesen wurde.
|
||||
- **Neu:** Emote-Tab im Standard-Layout (Testerwunsch), Orts- und Serverzeit in der Statusleiste, Screenshot-Modus aus der Eingabezeile erreichbar, `/hellion wizard`, ein Stil-Labor unter `/hellion lab`, und ein Hinweis im Assistenten, dass Plugins in FFXIV eine Grauzone sind und nicht in öffentliche Kanäle gehören.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
subtitle: "Hotfix"
|
||||
versionsnatur: "Hotfix ohne sichtbare Änderungen"
|
||||
---
|
||||
- **Nichts Neues zu sehen.** Wer 2.0.0 in der ersten Stunde gezogen hat, sollte trotzdem updaten: das 2.0.0-Archiv wurde ohne eine Abhängigkeits-Aktualisierung gebaut, die hier drin ist.
|
||||
- **MessagePack von 3.1.4 auf 3.1.7.** Das Paket serialisiert die Nachrichten-Payloads in der lokalen Datenbank. Die Meldungen betreffen die Rekursionstiefe in `Skip` und einen Fehler in der LZ4-Dekomprimierung, beide nur über präparierte Eingaben erreichbar. Das Plugin schreibt und liest ausschließlich seine eigenen Bytes in einer lokalen Datei, praktisch bräuchte ein Angreifer also schon Schreibzugriff darauf. Trotzdem gehoben, weil es nichts kostet.
|
||||
- **Der Release-Workflow hängt sein Archiv wieder selbst an.** Bei 2.0.0 lief der Build sauber durch und scheiterte dann am Veröffentlichen, weshalb dieses Release von Hand fertiggestellt werden musste.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
subtitle: "Emotes raus, Platzhalter gefixt"
|
||||
versionsnatur: "Aufräum-Release"
|
||||
---
|
||||
- **BetterTTV-Emotes sind raus.** Der Endpunkt für geteilte Emotes liegt hinter einer Anmeldung, und von dort kam fast alles. Übrig blieben 54 überwiegend statische Bilder aus der Twitch-Frühzeit, davon genau eines animiert, und das wollte 492 Einzelbilder und 37 MB Grafikspeicher. Das Plugin macht jetzt **gar keine ausgehenden Netzwerkaufrufe** mehr. Gespeicherte Nachrichten mit Emotes bleiben lesbar und zeigen den getippten Code.
|
||||
- **Fünf Beschreibungen im Fenster-Tab zeigten `{0}` statt des Plugin-Namens.** Betroffen waren alle 25 Sprachen, im Deutschen fiel es zuerst auf, weil der Platzhalter dort am Satzanfang steht. Ein Test prüft das jetzt dauerhaft.
|
||||
- **Neue Vorschaubilder** im Plugin-Installer. Die alten waren vom 8. Mai und zeigten die Oberfläche vor dem Umbau.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
subtitle: "Kontextmenü in Pop-outs, Screenshot-Modus"
|
||||
versionsnatur: "Fehlerbehebung"
|
||||
---
|
||||
- **Das Kontextmenü ging in Pop-out-Fenstern nicht auf.** Ein Rechtsklick auf einen Namen oder ein Item hat dort schlicht nichts gemacht. Alle Chat-Flächen teilen sich einen Popup-Zustand, und das zuerst gezeichnete Fenster hat das Menü verworfen, bevor das Pop-out überhaupt an der Reihe war. Das Menü bleibt jetzt bei dem Fenster, aus dem der Klick kam.
|
||||
- **Der Screenshot-Modus ließ sich nicht ausschalten.** Er wird gespeichert, aber keiner der beiden Schalter hat die Änderung in die Konfiguration geschrieben. Einmal gespeichert kam er bei jedem Laden des Plugins wieder. Beide Schalter speichern jetzt. Wer ihn gerade festhängen hat, klickt ihn einmal aus.
|
||||
- **Vom Spiel eingefärbter Text konnte unsichtbar werden.** Manche dieser Farben kommen ohne Alpha-Byte an und wurden beim Umsortieren vollständig transparent.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
subtitle: "Aufgeräumt: Beschreibung und tote Strings"
|
||||
versionsnatur: "Aufräum-Release"
|
||||
---
|
||||
Am Verhalten des Plugins ändert sich nichts. Zwei Dinge, die überfällig waren.
|
||||
|
||||
- **Die Beschreibung im Plugin-Installer war zu lang und an einer Stelle schlicht falsch.** Sie lief über 908 Zeichen und fing damit an, eine Kategorie zu nennen, statt zu sagen was das Plugin tut. Ausserdem stand dort 24 Sprachen, es sind 25. Und die beiden Manifeste trugen unterschiedliche Kurzbeschreibungen, in der Plugin-Liste stand also ein anderer Satz als in den Details. Jetzt sind es 367 Zeichen, überall dieselben.
|
||||
- **30 Übersetzungsschlüssel für das Webinterface sind raus.** Das Feature selbst, seine HTTP-Routen und sein Frontend haben den Code im Mai verlassen, die Texte blieben in allen 25 Sprachen liegen, dazu je eine erzeugte Property. Vor dem Löschen wurde jeder Schlüssel einzeln gegen das gesamte Repository geprüft.
|
||||
@@ -0,0 +1,36 @@
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// Ported 1:1 from v1.5.6 ChatLogWindow.SetUpAllCommands. Provides a fast
|
||||
// lookup from slash-command string to the game's TextCommand row so the
|
||||
// InputBar callback can feed descriptions to CommandHelpWindow without
|
||||
// hitting the sheet on every keystroke.
|
||||
internal static class AllCommands
|
||||
{
|
||||
private static readonly Dictionary<string, TextCommand> Commands = BuildCommands();
|
||||
|
||||
private static Dictionary<string, TextCommand> BuildCommands()
|
||||
{
|
||||
var dict = new Dictionary<string, TextCommand>(StringComparer.Ordinal);
|
||||
foreach (var command in Sheets.TextCommandSheet)
|
||||
{
|
||||
if (!command.Command.IsEmpty)
|
||||
dict.TryAdd(command.Command.ToString(), command);
|
||||
|
||||
if (!command.ShortCommand.IsEmpty)
|
||||
dict.TryAdd(command.ShortCommand.ToString(), command);
|
||||
|
||||
if (!command.Alias.IsEmpty)
|
||||
dict.TryAdd(command.Alias.ToString(), command);
|
||||
|
||||
if (!command.ShortAlias.IsEmpty)
|
||||
dict.TryAdd(command.ShortAlias.ToString(), command);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public static bool TryGetValue(string command, out TextCommand textCommand) =>
|
||||
Commands.TryGetValue(command, out textCommand);
|
||||
}
|
||||
@@ -21,7 +21,15 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private readonly MessageManager _messageManager;
|
||||
private readonly MessageStore _store;
|
||||
private readonly ILogger<AutoTellTabsService> _logger;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
// Tabs-list structure lock now lives on Plugin (neutral owner) so the
|
||||
// MessageManager refilter can share it. See Plugin.TabsListLock.
|
||||
private object TabsListLock => _plugin.TabsListLock;
|
||||
|
||||
// Bumped whenever something wipes unpinned temp tabs wholesale (logout).
|
||||
// HandleTell reads it before releasing the lock and re-checks after, so a
|
||||
// tab built in between is discarded instead of outliving the wipe.
|
||||
private int _tabGeneration;
|
||||
|
||||
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||
@@ -31,6 +39,10 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
// Set when Initialize ran before a character was available; cleared once the
|
||||
// history has actually been loaded.
|
||||
private bool _rehydratePending;
|
||||
|
||||
internal AutoTellTabsService(
|
||||
Plugin plugin,
|
||||
MessageManager messageManager,
|
||||
@@ -46,8 +58,8 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||
// transitions are cold-path and don't need lock-free reads.
|
||||
// Replaces an Interlocked counter: the pin-state transitions are cold-path
|
||||
// and don't need lock-free reads.
|
||||
internal int ActiveTempTabCount =>
|
||||
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
@@ -68,12 +80,30 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
RehydratePinnedTabs();
|
||||
|
||||
_messageManager.MessageProcessed += HandleTell;
|
||||
Plugin.ClientState.Login += OnLogin;
|
||||
Plugin.ClientState.Logout += OnLogout;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// Deferred when the plugin starts before a character is logged in, which is
|
||||
// the normal case: the game loads plugins at boot. CurrentContentId is 0
|
||||
// until then, so the history query would look up tells for character zero,
|
||||
// find none, and leave every pinned tab blank for the whole session.
|
||||
//
|
||||
// Only visible to someone who actually pins a tell tab AND starts the game
|
||||
// with the plugin already installed. Reloading the plugin in a running
|
||||
// session -- what a developer does all day -- hides it completely.
|
||||
private void RehydratePinnedTabs()
|
||||
{
|
||||
if (_messageManager.CurrentContentId == 0)
|
||||
{
|
||||
_logger.LogDebug("[Pin] Rehydrate deferred: no character yet, waiting for login");
|
||||
_rehydratePending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_rehydratePending = false;
|
||||
|
||||
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
_logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
|
||||
|
||||
@@ -114,6 +144,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin.ClientState.Login -= OnLogin;
|
||||
Plugin.ClientState.Logout -= OnLogout;
|
||||
_messageManager.MessageProcessed -= HandleTell;
|
||||
_initialized = false;
|
||||
@@ -147,15 +178,19 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_tempTabsLock)
|
||||
// Three steps, because building the tab pulls history out of the store and
|
||||
// that must not happen under TabsListLock (the query sorts the whole
|
||||
// receiver history). Step 1 and 3 are locked, step 2 is not.
|
||||
int generation;
|
||||
lock (TabsListLock)
|
||||
{
|
||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
if (existing != null)
|
||||
{
|
||||
// Already routed via MessageManager pipeline. Repair the
|
||||
// tell-target if the fallback hit a pinned tab whose
|
||||
// TellTarget didn't survive a previous round-trip — keeps
|
||||
// FindTempTab fast on the next message.
|
||||
// Already routed via MessageManager pipeline — no AddMessage here,
|
||||
// HandleTell runs after the delivery loop. Repair the tell-target if
|
||||
// the fallback hit a pinned tab whose TellTarget didn't survive a
|
||||
// previous round-trip — keeps FindTempTab fast on the next message.
|
||||
if (
|
||||
existing.IsPinned
|
||||
&& (existing.TellTarget is null || !existing.TellTarget.IsSet())
|
||||
@@ -172,12 +207,29 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
generation = _tabGeneration;
|
||||
}
|
||||
|
||||
var tab = BuildTempTabWithHistory(partner.Value, message);
|
||||
|
||||
lock (TabsListLock)
|
||||
{
|
||||
// A logout in between wiped the unpinned pool; committing now would
|
||||
// resurrect a tab for a character we already left.
|
||||
if (generation != _tabGeneration)
|
||||
return;
|
||||
|
||||
// Someone else (self-test, UI) may have created the tab while we built
|
||||
// ours. Hand the message to theirs and drop what we built — unlike the
|
||||
// early return above, this tab appeared after the delivery loop ran.
|
||||
var raced = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
if (raced != null)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
raced.AddMessage(message, unread: true);
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnTempTab(partner.Value, message);
|
||||
CommitTempTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +270,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Tab? FindTempTab(string name, uint world)
|
||||
internal static Tab? FindTempTab(string name, uint world)
|
||||
{
|
||||
var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||
t.IsTempTab
|
||||
@@ -239,45 +291,60 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
// Lock-protected lookup for the framework-thread caller (TellRouterService).
|
||||
// Config.Tabs is mutated under the shared Plugin.TabsListLock on the worker thread,
|
||||
// so a framework-tick reader must take the same lock to avoid enumerating the list
|
||||
// mid-mutation.
|
||||
internal Tab? FindTempTabSafe(string name, uint world)
|
||||
{
|
||||
lock (TabsListLock)
|
||||
return FindTempTab(name, world);
|
||||
}
|
||||
|
||||
internal void DropOldestTempTab()
|
||||
{
|
||||
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||
// never drop candidates. They leave the bucket only via Unpin or
|
||||
// PromoteToPermanent.
|
||||
var victim = Plugin
|
||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (victim.Tab == null)
|
||||
// Lock the list-structure ops so the (currently caller-less) Unpin path
|
||||
// can't race the worker; re-entrant when HandleTell already holds the lock.
|
||||
lock (TabsListLock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||
// never drop candidates. They leave the bucket only via Unpin or
|
||||
// PromoteToPermanent.
|
||||
var victim = Plugin
|
||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
|
||||
// Clean up pop-out window if tab is popped out
|
||||
if (victim.Tab.PopOut)
|
||||
{
|
||||
var popout = _plugin.ChatLogWindow.ActivePopouts.FirstOrDefault(p =>
|
||||
p.TabIdentifier == victim.Tab.Identifier
|
||||
);
|
||||
if (popout != null)
|
||||
if (victim.Tab == null)
|
||||
{
|
||||
popout.IsOpen = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||
var dropped = victim.Tab;
|
||||
// By reference, not by index: the index came from a Select() earlier in
|
||||
// this block and would point at the wrong tab if anything shifted the list.
|
||||
Plugin.Config.Tabs.Remove(dropped);
|
||||
|
||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||
if (victim.Index <= _plugin.LastTab)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
// Re-anchor the UI selection if it pointed at the dropped tab, and close any
|
||||
// pop-out window the dropped tab owned. Both run on the PendingMessage worker
|
||||
// thread and touch window state the Draw path reads (OnTabActivated re-seed +
|
||||
// the pool's Unbind), so marshal onto the framework thread to serialize with
|
||||
// Draw (reference_dalamud_framework_thread). TryClose is idempotent: a tab that
|
||||
// was never popped is a silent no-op.
|
||||
Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_plugin.ChannelPopoutPool.TryClose(dropped.Identifier);
|
||||
_plugin.MainWindow?.ResetActiveTabIfRemoved(dropped);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnTempTab((string Name, uint World) partner, Message currentMessage)
|
||||
// Runs WITHOUT TabsListLock: PreloadHistory hits the store, which used to hold
|
||||
// the lock across a query that sorted the whole receiver history. The tab is not
|
||||
// public until CommitTempTab adds it, so building it unlocked is safe.
|
||||
private Tab BuildTempTabWithHistory((string Name, uint World) partner, Message currentMessage)
|
||||
{
|
||||
var tab = BuildTempTab(partner.Name, partner.World);
|
||||
|
||||
@@ -286,13 +353,40 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
tab.AddMessage(currentMessage, unread: true);
|
||||
|
||||
// Open as pop-out if configured (set before Tabs.Add for next render-tick)
|
||||
// Flag the tab as a pop-out if configured; the marshalled TryOpen below reads
|
||||
// that flag to open the real window.
|
||||
if (Plugin.Config.AutoTellTabsOpenAsPopout)
|
||||
{
|
||||
tab.PopOut = true;
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
// Caller MUST hold TabsListLock.
|
||||
private void CommitTempTab(Tab tab)
|
||||
{
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.Add(tab);
|
||||
|
||||
// Actually open the pop-out window for the flagged tab — without this the
|
||||
// flag was dead (a PopOut tab with no window). CommitTempTab runs on the
|
||||
// PendingMessage worker thread under Plugin.TabsListLock; TryOpen does
|
||||
// OnTabActivated + Bind (window state Draw reads), so marshal onto the
|
||||
// framework thread. If the pool is full, drop the flag so it never claims a
|
||||
// window it didn't get (flag/window parity).
|
||||
if (tab.PopOut)
|
||||
{
|
||||
Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (!_plugin.ChannelPopoutPool.TryOpen(tab))
|
||||
tab.PopOut = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||
@@ -300,6 +394,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return new Tab
|
||||
{
|
||||
Name = FormatTabName(playerName, worldRowId),
|
||||
NameCameFromPartner = true,
|
||||
IsTempTab = true,
|
||||
AllSenderMessages = true,
|
||||
TellTarget = new TellTarget(playerName, worldRowId, 0, TellReason.Direct),
|
||||
@@ -407,7 +502,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_tempTabsLock)
|
||||
lock (TabsListLock)
|
||||
{
|
||||
// Guard against frame-race: sidebar might render a tab already removed by LRU or logout
|
||||
if (!Plugin.Config.Tabs.Contains(tab))
|
||||
@@ -419,43 +514,53 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Fires on the login that follows a boot-time start, and on every character
|
||||
// switch after one. Guarded by the pending flag so a switch does not append
|
||||
// a second copy of the history to tabs that already have it.
|
||||
private void OnLogin()
|
||||
{
|
||||
if (!_rehydratePending)
|
||||
return;
|
||||
|
||||
RehydratePinnedTabs();
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
lock (TabsListLock)
|
||||
{
|
||||
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||
// of pinning. Only unpinned ones get stripped.
|
||||
var lastIndex = _plugin.LastTab;
|
||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
var currentWasUnpinnedTempTab =
|
||||
lastIndexValid
|
||||
&& TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);
|
||||
var active = _plugin.MainWindow?.ActiveTab;
|
||||
|
||||
var poppedTempTabIds = Plugin
|
||||
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||
.Config.Tabs.Where(t =>
|
||||
TabLifecycleHelpers.IsInUnpinnedPool(t)
|
||||
&& _plugin.ChannelPopoutPool.IsOpen(t.Identifier)
|
||||
)
|
||||
.Select(t => t.Identifier)
|
||||
.ToList();
|
||||
if (poppedTempTabIds.Count > 0)
|
||||
{
|
||||
var poppedSet = poppedTempTabIds.ToHashSet();
|
||||
foreach (
|
||||
var popout in _plugin
|
||||
.ChatLogWindow.ActivePopouts.Where(p => poppedSet.Contains(p.TabIdentifier))
|
||||
.ToList()
|
||||
)
|
||||
{
|
||||
popout.IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close any pop-out window an unpinned temp tab owns before the tabs leave
|
||||
// the list. Filtering on the live pool (not the PopOut flag) also catches
|
||||
// manually right-clicked pop-outs, which never set the flag.
|
||||
foreach (var id in poppedTempTabIds)
|
||||
_plugin.ChannelPopoutPool.TryClose(id);
|
||||
|
||||
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
if (currentWasUnpinnedTempTab || !stillValid)
|
||||
// HandleTell builds a tab outside the lock; bumping here lets it detect
|
||||
// that the world moved on and drop what it built. Read and compared under
|
||||
// the same lock, so no volatile needed.
|
||||
_tabGeneration++;
|
||||
|
||||
// Re-anchor the UI selection if the active tab was one of the stripped
|
||||
// unpinned temp tabs (reference predicate, not an index). Logout is a
|
||||
// framework-thread event, so this is already serialized with Draw — no
|
||||
// marshalling needed here, unlike the worker-thread eviction path.
|
||||
if (active is { } a && TabLifecycleHelpers.IsInUnpinnedPool(a))
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
_plugin.MainWindow?.ResetActiveTabIfRemoved(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,16 +575,23 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||
// Count and flag under one lock so the cap can't be raced. SaveConfig stays
|
||||
// OUTSIDE -- holding TabsListLock across a save would put an fsync on the
|
||||
// click path, which is what a later cycle just removed elsewhere.
|
||||
lock (TabsListLock)
|
||||
{
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||
NotificationType.Warning
|
||||
);
|
||||
return false;
|
||||
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||
{
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||
NotificationType.Warning
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
tab.IsPinned = true;
|
||||
}
|
||||
|
||||
tab.IsPinned = true;
|
||||
_logger.LogDebug(
|
||||
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||
);
|
||||
@@ -496,13 +608,18 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
// If the unpinned pool is already full, dropping the oldest before
|
||||
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||
// candidate.
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
// candidate. Under lock, since DropOldestTempTab mutates the list.
|
||||
// SaveConfig stays outside, see TryPin.
|
||||
lock (TabsListLock)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
}
|
||||
|
||||
tab.IsPinned = false;
|
||||
}
|
||||
|
||||
tab.IsPinned = false;
|
||||
_logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
@@ -514,9 +631,17 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
tab.IsTempTab = false;
|
||||
tab.IsPinned = false;
|
||||
tab.TellTarget = TellTarget.Empty();
|
||||
// Drops the temp/pin flags, the persisted tell target AND the runtime
|
||||
// channel's tell state. The runtime-channel clear is the CORR-1 guard —
|
||||
// see StripTellBindingOnPromote; clearing Tab.TellTarget alone would leave
|
||||
// CurrentChannel.Channel == Tell + a stale target and route a typed line
|
||||
// silently as /tell to the old partner.
|
||||
// Flips IsTempTab/IsPinned, which decide pool membership and whether a save
|
||||
// strips the tab. Under lock so a concurrent save sees one or the other, never
|
||||
// half. SaveConfig stays outside, see TryPin.
|
||||
lock (TabsListLock)
|
||||
TabLifecycleHelpers.StripTellBindingOnPromote(tab);
|
||||
|
||||
_logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ internal static class BrandingLinks
|
||||
public const string HellionForgeGitea = "https://gitea.hellion-forge.cloud/Hellion-Forge";
|
||||
public const string HellionChatRepo =
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||
public const string HellionChatCustomRepoManifest =
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json";
|
||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
||||
|
||||
@@ -26,6 +28,7 @@ internal static class BrandingLinks
|
||||
HellionForgeDiscordInvite,
|
||||
HellionForgeGitea,
|
||||
HellionChatRepo,
|
||||
HellionChatCustomRepoManifest,
|
||||
HellionForgeWebsite,
|
||||
HellionMediaWebsite
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Dalamud.Interface.Textures;
|
||||
|
||||
namespace HellionChat.Branding;
|
||||
|
||||
// UI sibling of HellionForgeAscii.FoxMini: the embedded Hellion Forge fox
|
||||
// banner PNG. Uses ITextureProvider.GetFromManifestResource, a "Get" shared
|
||||
// texture, so Dalamud owns the cache and lifetime. No manual dispose, no async
|
||||
// handling in the plugin. Static to mirror HellionForgeAscii (zero injectable
|
||||
// deps; Plugin.TextureProvider is a static [PluginService]).
|
||||
internal static class FoxBannerTexture
|
||||
{
|
||||
private const string ResourceName = "HellionChat.Branding.fox-banner.png";
|
||||
|
||||
// Resolved fresh on every access. Dalamud keeps the shared texture cached
|
||||
// internally and decodes it asynchronously, so GetWrapOrDefault() returns
|
||||
// null for the first few frames until the decode finishes.
|
||||
public static ISharedImmediateTexture Shared =>
|
||||
Plugin.TextureProvider.GetFromManifestResource(
|
||||
typeof(FoxBannerTexture).Assembly,
|
||||
ResourceName
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
namespace HellionChat.Branding;
|
||||
|
||||
// Lazy-loaded provenance art that ships embedded with the DLL. Two
|
||||
// variants:
|
||||
// Lazy-loaded ASCII art that ships embedded with the DLL.
|
||||
//
|
||||
// - FoxBanner: the full-size silhouette with "Hellion Forge" inside
|
||||
// the body — rendered in the first-run wizard and the Information
|
||||
// tab as a small "about the makers" anchor.
|
||||
// - FoxMini: the four-line fox-head + curly-tail that gets stitched
|
||||
// into the DI-logger bootstrap line so an xllog reader sees the
|
||||
// same signature on every plugin load.
|
||||
//
|
||||
// Both files live as embedded resources under HellionChat.Branding.* so
|
||||
// the plugin DLL is self-contained — no on-disk asset lookup that could
|
||||
// The file lives as an embedded resource under HellionChat.Branding.* so
|
||||
// the plugin DLL is self-contained; no on-disk asset lookup that could
|
||||
// silently miss after a partial deploy.
|
||||
internal static class HellionForgeAscii
|
||||
{
|
||||
private static string? _foxBanner;
|
||||
private static string? _foxMini;
|
||||
|
||||
public static string FoxBanner => _foxBanner ??= Load("HellionChat.Branding.fox-banner.txt");
|
||||
|
||||
public static string FoxMini => _foxMini ??= Load("HellionChat.Branding.fox-mini.txt");
|
||||
|
||||
private static string Load(string resourceName)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace HellionChat;
|
||||
|
||||
// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge.
|
||||
// Before that the fallback merged over the full `Ranges` array (Default + endonyms),
|
||||
// duplicating the Latin/Default work already done by the global/Japanese fonts.
|
||||
// This is the trimmed remainder the fallback is actually the sole source for:
|
||||
// - Hangul Syllables (AC00-D7A3): no other merged font ships Korean glyphs.
|
||||
// - The full CJK Unified Ideographs (Han) block: at UseHellionFont=true the global
|
||||
// font is Inter-Light (no CJK), so the fallback is the SOLE Han source. The JpRange
|
||||
// overlap is harmless (MergeMode: the Japanese font wins for shared kanji).
|
||||
// Deliberately excluded: ONLY the ASCII/Latin Default block (0x20-0xFF), which the
|
||||
// global font already owns -- that doubled Latin merge is the waste being removed.
|
||||
// Kept as plain start/end pairs so it is unit-testable without the unsafe ImGui
|
||||
// glyph-range builder (mirrors FontSizeResolver's split-for-test rationale).
|
||||
internal static class CjkFallbackRange
|
||||
{
|
||||
// Hangul Syllables + the full CJK Unified Ideographs (Han) block. Rationale: see
|
||||
// class comment. Plain start/end pairs so it stays unit-testable.
|
||||
internal static readonly ushort[] Pairs =
|
||||
[
|
||||
0xAC00,
|
||||
0xD7A3, // Hangul Syllables
|
||||
0x4E00,
|
||||
0x9FFF, // CJK Unified Ideographs (full Han) -- sole source at UseHellionFont
|
||||
];
|
||||
}
|
||||
+334
-200
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using Dalamud;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Configuration;
|
||||
@@ -34,7 +35,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 17;
|
||||
internal const int LatestVersion = 27;
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
@@ -44,17 +45,29 @@ public class Configuration : IPluginConfiguration
|
||||
// Global window opacity, applied across all themes.
|
||||
public float WindowOpacity = 0.85f;
|
||||
|
||||
// Background opacity of the main chat window while unfocused.
|
||||
// WindowOpacity above stays the focused value.
|
||||
public float WindowOpacityInactive = 0.75f;
|
||||
|
||||
// Reserved for future UI toggles; pre-declared to avoid a migration later.
|
||||
public bool ReduceMotion;
|
||||
|
||||
// v1.2.1: default flipped false → true. Compact single-line layout is
|
||||
// more readable than the card-rows layout introduced in v1.2.0.
|
||||
public bool UseCompactDensity = true;
|
||||
public bool UseCompactDensity;
|
||||
|
||||
// Privacy by Default master switch. Set false to restore upstream behaviour.
|
||||
public bool PrivacyFilterEnabled = true;
|
||||
|
||||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
||||
// Stays empty here. Dalamud deserialises with Json.NET's default settings,
|
||||
// which means ObjectCreationHandling.Auto: a collection field that already
|
||||
// holds items is *populated*, not replaced. A non-empty initializer would
|
||||
// therefore union itself into every config on load and switch channels the
|
||||
// user had unticked back on. Verified against Newtonsoft 13.0.3:
|
||||
// saved [] loads as the initializer, saved [Say] loads as initializer + Say.
|
||||
//
|
||||
// Privacy by Default (DSGVO Art. 25) is seeded in CreateFresh instead, which
|
||||
// only runs when there is no config file at all.
|
||||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
||||
|
||||
// Failsafe for ChatTypes added by future FFXIV patches. New configs default
|
||||
@@ -64,24 +77,39 @@ public class Configuration : IPluginConfiguration
|
||||
.PrivacyDefaults
|
||||
.DefaultPersistUnknownChannels;
|
||||
|
||||
// F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||
// Dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||
// the log every frame. NonSerialized so the warning fires once per
|
||||
// runtime, not once-ever-per-install.
|
||||
[NonSerialized]
|
||||
private readonly HashSet<ChatType> _warnedUnknownChannels = new();
|
||||
|
||||
// A first-ever start records the player's own conversations and nothing
|
||||
// else. Deliberately not a field initializer -- see PrivacyPersistChannels.
|
||||
internal static Configuration CreateFresh()
|
||||
{
|
||||
var config = new Configuration();
|
||||
config.PrivacyPersistChannels = [.. Privacy.PrivacyDefaults.PrivacyFirstWhitelist];
|
||||
return config;
|
||||
}
|
||||
|
||||
public bool IsAllowedForStorage(ChatType type)
|
||||
{
|
||||
if (!PrivacyFilterEnabled)
|
||||
return true;
|
||||
if (PrivacyPersistChannels.Contains(type))
|
||||
return true;
|
||||
|
||||
// F3.2: log first occurrence of a ChatType the running build doesn't
|
||||
// recognise — i.e. one a future FFXIV patch may have added. Known
|
||||
// types the user opted out of are routed through the failsafe
|
||||
// silently, like before.
|
||||
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||
// Runs per message on the worker thread while the settings UI can Add to the
|
||||
// same set from the draw thread. A HashSet.Contains racing an Add that
|
||||
// resizes buckets can return the wrong answer -- and this answer decides
|
||||
// whether a message is persisted. Lock kept tight, this is a hot path.
|
||||
bool listed;
|
||||
lock (Plugin.Instance.ConfigMapsLock)
|
||||
listed = PrivacyPersistChannels.Contains(type);
|
||||
|
||||
var known = Enum.IsDefined(typeof(ChatType), type);
|
||||
|
||||
// Log the first occurrence of a ChatType the running build doesn't
|
||||
// recognise — i.e. one a future FFXIV patch may have added.
|
||||
if (!known && !listed && _warnedUnknownChannels.Add(type))
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||
@@ -90,7 +118,7 @@ public class Configuration : IPluginConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
return PrivacyPersistUnknownChannels;
|
||||
return Privacy.StorageRule.Allows(listed, known, PrivacyPersistUnknownChannels);
|
||||
}
|
||||
|
||||
// Retention master switch defaults to false — plugin will not delete
|
||||
@@ -100,6 +128,15 @@ public class Configuration : IPluginConfiguration
|
||||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
||||
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
||||
public bool FirstRunCompleted;
|
||||
|
||||
// Tracks which plugin version last surfaced the first-run wizard.
|
||||
// When the running version is newer than this, Plugin.LoadAsync
|
||||
// re-opens the wizard once so existing users see major UX reworks
|
||||
// (e.g. the v1.5.2 multi-step rewrite). Skip path and Finish both
|
||||
// set FirstRunCompleted = true on close, so the wizard only fires
|
||||
// once per version bump even if the user dismisses it.
|
||||
public string WizardLastShownVersion = string.Empty;
|
||||
|
||||
public bool UseHellionFont = true;
|
||||
public bool ShowHonorificTitleInHeader = true;
|
||||
|
||||
@@ -108,21 +145,33 @@ public class Configuration : IPluginConfiguration
|
||||
// who don't care, and dodges the per-frame DrawList overhead on low-end
|
||||
// hardware. Gradient (Color3 / GradientColourSet) is parsed but rendered
|
||||
// as the primary Color until a later cycle ports the animation.
|
||||
public bool ShowHonorificGlow;
|
||||
public bool ShowHonorificGlow = true;
|
||||
public bool EnableAutoTellTabs = true;
|
||||
public int AutoTellTabsLimit = 15;
|
||||
public bool AutoTellTabsCompactDisplay;
|
||||
public int AutoTellTabsHistoryPreload = 20;
|
||||
public bool AutoTellTabsCompactDisplay = true;
|
||||
public int AutoTellTabsHistoryPreload = 100;
|
||||
|
||||
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
|
||||
// v1.2.0; users can widen up to 160 to fit a section-header line like
|
||||
// "Active Tells (3)" without truncation.
|
||||
public int SidebarWidth = 44;
|
||||
// Expanded sidebar width in pixels. 44 was carried over from the v1.2.0
|
||||
// icon-only layout and stayed the default long after the sidebar started
|
||||
// drawing labels beside those icons, so every tab name came out clipped --
|
||||
// it only went unnoticed because everyone had widened it by hand. 160 fits
|
||||
// the German tab names, which are the longest of the 25 languages, and the
|
||||
// floor below is set where they stop being readable rather than where the
|
||||
// icons stop fitting.
|
||||
public int SidebarWidth = 160;
|
||||
public bool AutoTellTabsShowGreetedToggle;
|
||||
public bool SeenPopOutInputHint;
|
||||
public bool PopOutInputEnabled = true;
|
||||
public bool SeenPopOutHeaderHint;
|
||||
public bool AutoTellTabsOpenAsPopout;
|
||||
|
||||
// On by default: the wizard's closing step tells the user to try /tell and
|
||||
// watch a conversation open on its own, so the behaviour it describes has to
|
||||
// be the behaviour they get.
|
||||
public bool AutoTellTabsOpenAsPopout = true;
|
||||
|
||||
// How sender names are rendered in the chat log.
|
||||
public WorldSuffixMode WorldSuffixMode = WorldSuffixMode.OtherWorldOnly;
|
||||
public NameFormMode NameFormMode = NameFormMode.Full;
|
||||
|
||||
public int GetRetentionDays(ChatType type)
|
||||
{
|
||||
@@ -143,46 +192,58 @@ public class Configuration : IPluginConfiguration
|
||||
// v1.2.1: default flipped false → true for consistency with other hide defaults.
|
||||
public bool HideInNewGamePlusMenu = true;
|
||||
public bool HideWhenInactive;
|
||||
public int InactivityHideTimeout = 10;
|
||||
public bool InactivityHideActiveDuringBattle = true;
|
||||
|
||||
[Obsolete("Use InactivityHideChannelsV2 instead")]
|
||||
public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];
|
||||
|
||||
public Dictionary<ChatType, (ChatSource, ChatSource)> InactivityHideChannelsV2 = [];
|
||||
public bool InactivityHideExtraChatAll = true;
|
||||
public HashSet<Guid> InactivityHideExtraChatChannels = [];
|
||||
public bool ShowHideButton = true;
|
||||
public bool NativeItemTooltips = true;
|
||||
public bool ScreenshotMode;
|
||||
|
||||
// No control and no reader. Kept so a stored value survives until the
|
||||
// rendering they describe exists; see the reconnect backlog. Note the two
|
||||
// resource sets disagree on what PrettierTimestamps even means -- the wizard
|
||||
// called it "relative time", the settings tab "modern layout".
|
||||
public bool PrettierTimestamps = true;
|
||||
public bool MoreCompactPretty;
|
||||
public bool MoreCompactPretty = true;
|
||||
public bool HideSameTimestamps = true;
|
||||
|
||||
// No reader; see the reconnect backlog.
|
||||
public bool ShowNoviceNetwork;
|
||||
|
||||
// Migration-only since v23: the 1.5.6 sidebar↔top-tabs switch, superseded by
|
||||
// MainWindowLayoutMode in the v1.6.0 rewrite. No UI control anymore; read by
|
||||
// the v23 migration in Plugin.cs and kept deserializable so a 1.5.6 user's
|
||||
// false value survives one load. Remove in a later schema bump.
|
||||
public bool SidebarTabView = true;
|
||||
|
||||
// No reader; see the reconnect backlog.
|
||||
public bool PrintChangelog = true;
|
||||
public bool OnlyPreviewIf;
|
||||
public int PreviewMinimum = 1;
|
||||
public PreviewPosition PreviewPosition = PreviewPosition.Inside;
|
||||
public CommandHelpSide CommandHelpSide = CommandHelpSide.None;
|
||||
public CommandHelpSide CommandHelpSide = CommandHelpSide.Right;
|
||||
public KeybindMode KeybindMode = KeybindMode.Strict;
|
||||
public LanguageOverride LanguageOverride = LanguageOverride.None;
|
||||
public bool CanMove = true;
|
||||
public bool CanResize = true;
|
||||
public bool ShowTitleBar = true;
|
||||
public bool ShowTitleBar;
|
||||
public bool ShowPopOutTitleBar = true;
|
||||
public bool DatabaseBattleMessages;
|
||||
public bool LoadPreviousSession;
|
||||
public bool FilterIncludePreviousSessions;
|
||||
public bool SortAutoTranslate;
|
||||
public bool CollapseDuplicateMessages;
|
||||
public bool CollapseKeepUniqueLinks;
|
||||
public bool SymbolPickerEnabled = true;
|
||||
public bool PlaySounds = true;
|
||||
|
||||
// AUDIO-1: playback volume (0-1) for the three bundled custom sounds.
|
||||
public float CustomSoundVolume = 0.5f;
|
||||
|
||||
// Toast when a tell the user sent could not be delivered.
|
||||
public bool NotifyFailedTell = true;
|
||||
|
||||
// Warn before sending a message that carries plugin-only glyphs.
|
||||
public bool NotifyPluginDisclosure = true;
|
||||
public bool KeepInputFocus = true;
|
||||
public int MaxLinesToRender = 2_500; // 1-10000
|
||||
public bool Use24HourClock = true;
|
||||
public bool ShowEmotes = true;
|
||||
public HashSet<string> BlockedEmotes = [];
|
||||
public bool FontsEnabled = true;
|
||||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
||||
public float FontSizeV2 = 12.75f;
|
||||
@@ -219,145 +280,66 @@ public class Configuration : IPluginConfiguration
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// No reader; see the reconnect backlog.
|
||||
public bool ColorSelectedInputChannelButton = true;
|
||||
public List<Tab> Tabs = [];
|
||||
|
||||
public ConfigKeyBind? ChatTabForward;
|
||||
public ConfigKeyBind? ChatTabBackward;
|
||||
|
||||
public void UpdateFrom(Configuration other, bool backToOriginal)
|
||||
{
|
||||
if (backToOriginal)
|
||||
foreach (var tab in Tabs.Where(t => t.PopOut))
|
||||
tab.PopOut = false;
|
||||
// v20 fields: window visibility state, channel popout pool size and
|
||||
// sidebar auto-switch threshold. All initializers double as the
|
||||
// migration defaults for configs loaded at v19 or earlier.
|
||||
// Still written on open/close, but no longer read for the start state: the
|
||||
// window always shows on login (1.5.6 parity, MainWindow ctor). Kept for the
|
||||
// migration round-trip and a possible future "remember session state" opt-in.
|
||||
public bool MainWindowOpen = true;
|
||||
public bool SettingsWindowOpen;
|
||||
public int MaxParallelPopouts = 8;
|
||||
public TellAutoOpenMode TellAutoOpenMode = TellAutoOpenMode.Sidebar;
|
||||
|
||||
HideChat = other.HideChat;
|
||||
HideDuringCutscenes = other.HideDuringCutscenes;
|
||||
HideWhenNotLoggedIn = other.HideWhenNotLoggedIn;
|
||||
HideWhenUiHidden = other.HideWhenUiHidden;
|
||||
HideInLoadingScreens = other.HideInLoadingScreens;
|
||||
HideInBattle = other.HideInBattle;
|
||||
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
|
||||
HideWhenInactive = other.HideWhenInactive;
|
||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value
|
||||
);
|
||||
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
||||
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
||||
ShowHideButton = other.ShowHideButton;
|
||||
NativeItemTooltips = other.NativeItemTooltips;
|
||||
PrettierTimestamps = other.PrettierTimestamps;
|
||||
MoreCompactPretty = other.MoreCompactPretty;
|
||||
HideSameTimestamps = other.HideSameTimestamps;
|
||||
ShowNoviceNetwork = other.ShowNoviceNetwork;
|
||||
SidebarTabView = other.SidebarTabView;
|
||||
PrintChangelog = other.PrintChangelog;
|
||||
OnlyPreviewIf = other.OnlyPreviewIf;
|
||||
PreviewMinimum = other.PreviewMinimum;
|
||||
PreviewPosition = other.PreviewPosition;
|
||||
CommandHelpSide = other.CommandHelpSide;
|
||||
KeybindMode = other.KeybindMode;
|
||||
LanguageOverride = other.LanguageOverride;
|
||||
CanMove = other.CanMove;
|
||||
CanResize = other.CanResize;
|
||||
ShowTitleBar = other.ShowTitleBar;
|
||||
ShowPopOutTitleBar = other.ShowPopOutTitleBar;
|
||||
DatabaseBattleMessages = other.DatabaseBattleMessages;
|
||||
LoadPreviousSession = other.LoadPreviousSession;
|
||||
FilterIncludePreviousSessions = other.FilterIncludePreviousSessions;
|
||||
SortAutoTranslate = other.SortAutoTranslate;
|
||||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||||
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
||||
PlaySounds = other.PlaySounds;
|
||||
KeepInputFocus = other.KeepInputFocus;
|
||||
MaxLinesToRender = other.MaxLinesToRender;
|
||||
Use24HourClock = other.Use24HourClock;
|
||||
ShowEmotes = other.ShowEmotes;
|
||||
// Deep-copy so settings window edits don't leak into live config before Save.
|
||||
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||||
FontsEnabled = other.FontsEnabled;
|
||||
ItalicEnabled = other.ItalicEnabled;
|
||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||||
FontSizeV2 = other.FontSizeV2;
|
||||
GlobalFontV2 = other.GlobalFontV2;
|
||||
JapaneseFontV2 = other.JapaneseFontV2;
|
||||
ItalicFontV2 = other.ItalicFontV2;
|
||||
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
||||
TooltipOffset = other.TooltipOffset;
|
||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||
// When true (default) the tell-auto-open router switches the active tab to the
|
||||
// incoming tell on every message; when false the tab is still created/revealed
|
||||
// with its unread badge but the active tab is left where the user is reading.
|
||||
public bool TellAutoOpenSwitchAlways = true;
|
||||
public int SidebarAutoSwitchThresholdPx = 800;
|
||||
|
||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||
// session-only and held from the local state. For persistent tabs
|
||||
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||
// the user may have switched channel in-game between settings-open
|
||||
// and settings-save, and we'd otherwise overwrite that with the
|
||||
// settings-time snapshot.
|
||||
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel));
|
||||
// v22 field: MainWindow layout mode (sidebar vs. horizontal top tabs).
|
||||
// Initializer doubles as the migration default for configs loaded at v21.
|
||||
public MainWindowLayoutMode MainWindowLayoutMode = MainWindowLayoutMode.Sidebar;
|
||||
}
|
||||
|
||||
Tabs = other
|
||||
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||
.Select(t =>
|
||||
{
|
||||
var clone = t.Clone();
|
||||
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
|
||||
{
|
||||
clone.Messages = live.Messages;
|
||||
clone.LastSendUnread = live.LastSendUnread;
|
||||
clone.CurrentChannel = live.CurrentChannel;
|
||||
}
|
||||
return clone;
|
||||
})
|
||||
.ToList();
|
||||
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||
[Serializable]
|
||||
public enum TellAutoOpenMode
|
||||
{
|
||||
Off,
|
||||
Sidebar,
|
||||
TopTab,
|
||||
Popout,
|
||||
}
|
||||
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
public static class TellAutoOpenModeExt
|
||||
{
|
||||
// The only display name set still in English. It sat inline in ChannelsTab
|
||||
// as a literal array, which is why it was missed when the rest moved into
|
||||
// resources; here it is at least in the same place as its peers for the
|
||||
// localisation pass to pick up.
|
||||
public static string Name(this TellAutoOpenMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
TellAutoOpenMode.Off => "Off",
|
||||
TellAutoOpenMode.Sidebar => "Sidebar",
|
||||
TellAutoOpenMode.TopTab => "Top tab",
|
||||
TellAutoOpenMode.Popout => "Popout",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||
};
|
||||
}
|
||||
|
||||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
||||
PrivacyPersistChannels = [.. other.PrivacyPersistChannels];
|
||||
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
||||
|
||||
RetentionEnabled = other.RetentionEnabled;
|
||||
RetentionDefaultDays = other.RetentionDefaultDays;
|
||||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(
|
||||
p => p.Key,
|
||||
p => p.Value
|
||||
);
|
||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||
|
||||
// v1.1.0 theme engine fields
|
||||
Theme = other.Theme;
|
||||
WindowOpacity = other.WindowOpacity;
|
||||
ReduceMotion = other.ReduceMotion;
|
||||
UseCompactDensity = other.UseCompactDensity;
|
||||
|
||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||
SidebarWidth = other.SidebarWidth;
|
||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||
|
||||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||
PopOutInputEnabled = other.PopOutInputEnabled;
|
||||
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
|
||||
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
|
||||
}
|
||||
[Serializable]
|
||||
public enum MainWindowLayoutMode
|
||||
{
|
||||
Sidebar,
|
||||
TopTabs,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -397,9 +379,6 @@ public class Tab
|
||||
// Optional FontAwesome glyph name; null falls back to TabIconMapping default.
|
||||
public string? Icon = null;
|
||||
|
||||
[Obsolete("Removed in favor of SelectedChannels")]
|
||||
public Dictionary<ChatType, ChatSource> ChatCodes = new();
|
||||
|
||||
public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
|
||||
public bool ExtraChatAll;
|
||||
public HashSet<Guid> ExtraChatChannels = [];
|
||||
@@ -416,23 +395,37 @@ public class Tab
|
||||
public bool CanMove = true;
|
||||
public bool CanResize = true;
|
||||
|
||||
public bool IndependentHide;
|
||||
public bool HideDuringCutscenes = true;
|
||||
public bool HideWhenNotLoggedIn = true;
|
||||
public bool HideWhenUiHidden = true;
|
||||
public bool HideInLoadingScreens;
|
||||
public bool HideInBattle;
|
||||
// Six per-tab hide conditions used to live here. Their reader was the
|
||||
// pop-out window, which stopped consulting them in cf4705e; the equivalents
|
||||
// that survive are the window-level fields of the same name further up this
|
||||
// file, and v1.12.0 gave every one of those a control.
|
||||
//
|
||||
// Per-tab was the wrong unit anyway: "hide during cutscenes" is a statement
|
||||
// about the screen, not about one conversation.
|
||||
//
|
||||
// HideWhenInactive stays -- the auto-tell service writes it.
|
||||
public bool HideWhenInactive;
|
||||
|
||||
public bool IsTempTab;
|
||||
|
||||
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||
// separate from the AutoTellTabsLimit bucket.
|
||||
// Pinned TempTabs survive plugin reload and logout -- tester feedback in
|
||||
// v1.4.7. Pinned tabs live in their own pool (MaxPinnedTempTabs) separate
|
||||
// from the AutoTellTabsLimit bucket.
|
||||
public bool IsPinned;
|
||||
public bool AllSenderMessages;
|
||||
public TellTarget TellTarget = TellTarget.Empty();
|
||||
|
||||
// Set once, where the name is built from a conversation partner. Never
|
||||
// cleared by promotion, unlike IsTempTab and TellTarget -- both of those are
|
||||
// routing state and are deliberately wiped when a tab is promoted, while the
|
||||
// name they produced stays. Screenshot mode reads this, so it has to outlive
|
||||
// every path that keeps the name but drops the binding.
|
||||
public bool NameCameFromPartner;
|
||||
|
||||
// Per-tab notification sound for messages arriving in an inactive tab.
|
||||
public bool EnableNotificationSound;
|
||||
public uint NotificationSoundId = 1;
|
||||
|
||||
[NonSerialized]
|
||||
public uint Unread;
|
||||
|
||||
@@ -475,6 +468,45 @@ public class Tab
|
||||
[NonSerialized]
|
||||
internal string? _cachedTellIcon;
|
||||
|
||||
// hover-lerp state. Default 0f means "not hovered". Sidebar
|
||||
// path animates per tab; card-mode-border path is tab-aggregate
|
||||
// (any card-row hover ramps the alpha for all cards in this tab).
|
||||
// Lerp speed lives in the render loop, not here, so the same field
|
||||
// serves both sites at the same animation curve.
|
||||
[NonSerialized]
|
||||
internal float _hoverAlpha;
|
||||
|
||||
[NonSerialized]
|
||||
internal float _cardHoverAlpha;
|
||||
|
||||
// Copy-on-write for the three channel-filter fields. They are read without
|
||||
// any lock from the pending-message thread, the filter worker and the draw
|
||||
// thread, and until v1.12.0 nothing ever wrote them after load -- so the
|
||||
// tab editor is their first writer, and mutating a live Dictionary while
|
||||
// Matches enumerates it is the classic way to get a wrong answer or an
|
||||
// exception on somebody else's thread.
|
||||
//
|
||||
// Building the replacements and swapping the references means a reader sees
|
||||
// either the old set or the new one, never half of either.
|
||||
//
|
||||
// What this deliberately does not do is make the three writes one atomic
|
||||
// step. A reader can catch the new dictionary with the old ExtraChat flag
|
||||
// for a single message. That is harmless: the editor finishes by clearing
|
||||
// and refiltering every tab, so any message placed by a mixed view is
|
||||
// reconsidered a moment later. Making it truly atomic would mean one
|
||||
// reference for all three, and these three are serialized fields with a
|
||||
// shape the config file already has.
|
||||
internal void ReplaceChannelFilter(
|
||||
Dictionary<ChatType, (ChatSource, ChatSource)> selected,
|
||||
bool extraChatAll,
|
||||
HashSet<Guid> extraChatChannels
|
||||
)
|
||||
{
|
||||
Volatile.Write(ref SelectedChannels, selected);
|
||||
Volatile.Write(ref ExtraChatChannels, extraChatChannels);
|
||||
Volatile.Write(ref ExtraChatAll, extraChatAll);
|
||||
}
|
||||
|
||||
public bool Matches(Message message)
|
||||
{
|
||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||
@@ -495,14 +527,14 @@ public class Tab
|
||||
return;
|
||||
|
||||
Unread += 1;
|
||||
if (
|
||||
message.Matches(
|
||||
Plugin.Config.InactivityHideChannelsV2,
|
||||
Plugin.Config.InactivityHideExtraChatAll,
|
||||
Plugin.Config.InactivityHideExtraChatChannels
|
||||
)
|
||||
)
|
||||
LastActivity = Environment.TickCount64;
|
||||
|
||||
// Stamped for every message now. The condition that used to sit here
|
||||
// filtered on InactivityHideChannels, a setting for the hide-when-
|
||||
// inactive feature -- and that feature lost its reader in cf4705e. So
|
||||
// which tell tab the auto-tell pool drops first, which is the only
|
||||
// thing that reads this stamp, hung on a setting for something that
|
||||
// does not happen.
|
||||
LastActivity = Environment.TickCount64;
|
||||
}
|
||||
|
||||
public void Clear() => Messages.Clear();
|
||||
@@ -512,6 +544,11 @@ public class Tab
|
||||
return new Tab
|
||||
{
|
||||
Name = Name,
|
||||
// Icon feeds the sidebar glyph and a clone round-trip used to drop
|
||||
// it silently. ChatCodes sat beside it until v1.12.0, carrying data
|
||||
// for a migration that the v16 schema gate had already made
|
||||
// unreachable.
|
||||
Icon = Icon,
|
||||
SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||||
ExtraChatAll = ExtraChatAll,
|
||||
ExtraChatChannels = ExtraChatChannels.ToHashSet(),
|
||||
@@ -529,17 +566,13 @@ public class Tab
|
||||
CurrentChannel = CurrentChannel.Clone(),
|
||||
CanMove = CanMove,
|
||||
CanResize = CanResize,
|
||||
IndependentHide = IndependentHide,
|
||||
HideDuringCutscenes = HideDuringCutscenes,
|
||||
HideWhenNotLoggedIn = HideWhenNotLoggedIn,
|
||||
HideWhenUiHidden = HideWhenUiHidden,
|
||||
HideInLoadingScreens = HideInLoadingScreens,
|
||||
HideInBattle = HideInBattle,
|
||||
HideWhenInactive = HideWhenInactive,
|
||||
IsTempTab = IsTempTab,
|
||||
IsPinned = IsPinned,
|
||||
AllSenderMessages = AllSenderMessages,
|
||||
TellTarget = TellTarget.Clone(),
|
||||
EnableNotificationSound = EnableNotificationSound,
|
||||
NotificationSoundId = NotificationSoundId,
|
||||
IsGreeted = IsGreeted,
|
||||
};
|
||||
}
|
||||
@@ -823,17 +856,27 @@ public enum LanguageOverride
|
||||
French,
|
||||
German,
|
||||
Greek,
|
||||
|
||||
// Italian,
|
||||
Japanese,
|
||||
|
||||
// Korean,
|
||||
// Norwegian,
|
||||
PortugueseBrazil,
|
||||
Romanian,
|
||||
Russian,
|
||||
Spanish,
|
||||
Swedish,
|
||||
|
||||
// v1.5.3: Crowdin-heritage activated and Forge-maintained additions.
|
||||
// Append-only to preserve serialized integer values of existing user configs.
|
||||
Italian,
|
||||
Korean,
|
||||
Norwegian,
|
||||
Catalan,
|
||||
Czech,
|
||||
Danish,
|
||||
Finnish,
|
||||
Hungarian,
|
||||
Polish,
|
||||
PortuguesePortugal,
|
||||
Turkish,
|
||||
Ukrainian,
|
||||
}
|
||||
|
||||
public static class LanguageOverrideExt
|
||||
@@ -849,15 +892,24 @@ public static class LanguageOverrideExt
|
||||
LanguageOverride.French => "Français",
|
||||
LanguageOverride.German => "Deutsch",
|
||||
LanguageOverride.Greek => "Ελληνικά",
|
||||
// LanguageOverride.Italian => "Italiano",
|
||||
LanguageOverride.Italian => "Italiano",
|
||||
LanguageOverride.Japanese => "日本語",
|
||||
// LanguageOverride.Korean => "한국어 (Korean)",
|
||||
// LanguageOverride.Norwegian => "Norsk",
|
||||
LanguageOverride.Korean => "한국어",
|
||||
LanguageOverride.Norwegian => "Norsk bokmål",
|
||||
LanguageOverride.PortugueseBrazil => "Português do Brasil",
|
||||
LanguageOverride.Romanian => "Română",
|
||||
LanguageOverride.Russian => "Русский",
|
||||
LanguageOverride.Spanish => "Español",
|
||||
LanguageOverride.Swedish => "Svenska",
|
||||
LanguageOverride.Catalan => "Català",
|
||||
LanguageOverride.Czech => "Čeština",
|
||||
LanguageOverride.Danish => "Dansk",
|
||||
LanguageOverride.Finnish => "Suomi",
|
||||
LanguageOverride.Hungarian => "Magyar",
|
||||
LanguageOverride.Polish => "Polski",
|
||||
LanguageOverride.PortuguesePortugal => "Português (Portugal)",
|
||||
LanguageOverride.Turkish => "Türkçe",
|
||||
LanguageOverride.Ukrainian => "Українська",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||
};
|
||||
|
||||
@@ -872,17 +924,80 @@ public static class LanguageOverrideExt
|
||||
LanguageOverride.French => "fr",
|
||||
LanguageOverride.German => "de",
|
||||
LanguageOverride.Greek => "el",
|
||||
// LanguageOverride.Italian => "it",
|
||||
LanguageOverride.Italian => "it",
|
||||
LanguageOverride.Japanese => "ja",
|
||||
// LanguageOverride.Korean => "ko",
|
||||
// LanguageOverride.Norwegian => "no",
|
||||
LanguageOverride.Korean => "ko",
|
||||
LanguageOverride.Norwegian => "nb",
|
||||
LanguageOverride.PortugueseBrazil => "pt-br",
|
||||
LanguageOverride.Romanian => "ro",
|
||||
LanguageOverride.Russian => "ru",
|
||||
LanguageOverride.Spanish => "es",
|
||||
LanguageOverride.Swedish => "sv",
|
||||
LanguageOverride.Catalan => "ca",
|
||||
LanguageOverride.Czech => "cs",
|
||||
LanguageOverride.Danish => "da",
|
||||
LanguageOverride.Finnish => "fi",
|
||||
LanguageOverride.Hungarian => "hu",
|
||||
LanguageOverride.Polish => "pl",
|
||||
LanguageOverride.PortuguesePortugal => "pt-pt",
|
||||
LanguageOverride.Turkish => "tr",
|
||||
LanguageOverride.Ukrainian => "uk",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||
};
|
||||
|
||||
// Maps a language to the ExtraGlyphRanges flag required for full UI
|
||||
// rendering in that locale. The settings save path ORs this into
|
||||
// Mutable.ExtraGlyphRanges so users do not need to know which range
|
||||
// to tick manually. Returns 0 for locales fully covered by the default
|
||||
// ImGui glyph range (Latin-1) or by the separate Japanese font handle.
|
||||
// The same mapping keyed by culture code, for when the language override is
|
||||
// None and the UI follows Dalamud. Without this the ranges only ever get
|
||||
// filled by an explicit language pick -- installs that never touched the
|
||||
// setting rendered their own locale in whatever the default range covers,
|
||||
// which for Korean, Chinese, Cyrillic and Greek is boxes. It went unnoticed
|
||||
// while configs accumulated ranges over time; a fresh config has none.
|
||||
public static ExtraGlyphRanges RequiredGlyphRangesForCulture(string? cultureCode)
|
||||
{
|
||||
var code = (cultureCode ?? string.Empty).ToLowerInvariant();
|
||||
|
||||
// Longest first: zh-hant has to win over the zh prefix.
|
||||
if (code.StartsWith("zh-hant") || code.StartsWith("zh-tw") || code.StartsWith("zh-hk"))
|
||||
return ExtraGlyphRanges.ChineseFull;
|
||||
if (code.StartsWith("zh"))
|
||||
return ExtraGlyphRanges.ChineseSimplifiedCommon;
|
||||
if (code.StartsWith("ko"))
|
||||
return ExtraGlyphRanges.Korean;
|
||||
if (code.StartsWith("uk") || code.StartsWith("ru") || code.StartsWith("be"))
|
||||
return ExtraGlyphRanges.Cyrillic;
|
||||
if (code.StartsWith("el"))
|
||||
return ExtraGlyphRanges.Greek;
|
||||
if (
|
||||
code.StartsWith("cs")
|
||||
|| code.StartsWith("pl")
|
||||
|| code.StartsWith("ro")
|
||||
|| code.StartsWith("hu")
|
||||
|| code.StartsWith("tr")
|
||||
)
|
||||
return ExtraGlyphRanges.LatinExtended;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static ExtraGlyphRanges RequiredGlyphRanges(this LanguageOverride mode) =>
|
||||
mode switch
|
||||
{
|
||||
LanguageOverride.Korean => ExtraGlyphRanges.Korean,
|
||||
LanguageOverride.ChineseSimplified => ExtraGlyphRanges.ChineseSimplifiedCommon,
|
||||
LanguageOverride.ChineseTraditional => ExtraGlyphRanges.ChineseFull,
|
||||
LanguageOverride.Ukrainian => ExtraGlyphRanges.Cyrillic,
|
||||
LanguageOverride.Greek => ExtraGlyphRanges.Greek,
|
||||
LanguageOverride.Czech
|
||||
or LanguageOverride.Polish
|
||||
or LanguageOverride.Romanian
|
||||
or LanguageOverride.Hungarian
|
||||
or LanguageOverride.Turkish => ExtraGlyphRanges.LatinExtended,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -896,10 +1011,23 @@ public enum ExtraGlyphRanges
|
||||
Korean = 1 << 4,
|
||||
Thai = 1 << 5,
|
||||
Vietnamese = 1 << 6,
|
||||
|
||||
// v1.5.3: Custom ranges for languages with Latin Extended-A glyphs (Czech,
|
||||
// Polish, Romanian, Turkish, Hungarian) and Greek polytonic accents.
|
||||
LatinExtended = 1 << 7,
|
||||
Greek = 1 << 8,
|
||||
}
|
||||
|
||||
public static class ExtraGlyphRangesExt
|
||||
{
|
||||
// Custom (start, end) inclusive pair lists for ranges that ImGui does
|
||||
// not ship a built-in helper for. SetUpRanges() feeds these into
|
||||
// ImFontGlyphRangesBuilder.AddChar via the `chars` parameter of
|
||||
// BuildRange so we avoid the lifetime/pinning question that the native
|
||||
// GetGlyphRanges*-pointer pathway papers over.
|
||||
internal static readonly ushort[] LatinExtendedPairs = { 0x0100, 0x024F };
|
||||
internal static readonly ushort[] GreekPairs = { 0x0370, 0x03FF, 0x1F00, 0x1FFF };
|
||||
|
||||
public static string Name(this ExtraGlyphRanges ranges) =>
|
||||
ranges switch
|
||||
{
|
||||
@@ -911,6 +1039,8 @@ public static class ExtraGlyphRangesExt
|
||||
ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
|
||||
ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name,
|
||||
ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name,
|
||||
ExtraGlyphRanges.LatinExtended => Language.ExtraGlyphRanges_LatinExtended_Name,
|
||||
ExtraGlyphRanges.Greek => Language.ExtraGlyphRanges_Greek_Name,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||||
};
|
||||
|
||||
@@ -925,6 +1055,10 @@ public static class ExtraGlyphRangesExt
|
||||
ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
|
||||
ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
|
||||
ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
|
||||
// LatinExtended and Greek are applied via builder.AddChar in
|
||||
// FontManager.SetUpRanges, not through a native pointer range.
|
||||
ExtraGlyphRanges.LatinExtended => 0,
|
||||
ExtraGlyphRanges.Greek => 0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
public static class EmoteCache
|
||||
{
|
||||
private static readonly string[] NotWorking =
|
||||
[
|
||||
":tf:",
|
||||
"(ditto)",
|
||||
"c!",
|
||||
"h!",
|
||||
"l!",
|
||||
"M&Mjc",
|
||||
"LUL3D",
|
||||
"p!",
|
||||
"POLICE2",
|
||||
"r!",
|
||||
"Pussy",
|
||||
"s!",
|
||||
"v!",
|
||||
"w!",
|
||||
"x0r6ztGiggle",
|
||||
"z!",
|
||||
"xar2EDM",
|
||||
"iron95Pls",
|
||||
"Clap2",
|
||||
"AlienPls3",
|
||||
"Life",
|
||||
"peepoPogClimbingTreeHard4House",
|
||||
"monkaGIGAftRobertDowneyJr",
|
||||
"DogLookingSussyAndCold",
|
||||
"DICKS",
|
||||
];
|
||||
|
||||
private static readonly HttpClient Client = new();
|
||||
|
||||
private const string BetterTTV = "https://api.betterttv.net/3";
|
||||
private const string GlobalEmotes = $"{BetterTTV}/cached/emotes/global";
|
||||
private const string Top100Emotes = "{0}/emotes/shared/top?before={1}&limit=100";
|
||||
private const string EmotePath = "https://cdn.betterttv.net/emote/{0}/3x";
|
||||
|
||||
[Serializable]
|
||||
private struct Top100()
|
||||
{
|
||||
[JsonPropertyName("emote")]
|
||||
public Emote Emote { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct Emote()
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public required string Code { get; set; }
|
||||
|
||||
[JsonPropertyName("imageType")]
|
||||
public required string ImageType { get; set; }
|
||||
}
|
||||
|
||||
public enum LoadingState
|
||||
{
|
||||
Unloaded,
|
||||
Loading,
|
||||
Done,
|
||||
}
|
||||
|
||||
// All fields below are uninitialised while State != Done.
|
||||
public static LoadingState State = LoadingState.Unloaded;
|
||||
|
||||
private static readonly Dictionary<string, Emote> Cache = new();
|
||||
private static readonly Dictionary<string, EmoteBase> EmoteImages = new();
|
||||
|
||||
public static string[] SortedCodeArray = [];
|
||||
|
||||
// Cancelled on Dispose to stop in-flight downloads; replaced on re-enable.
|
||||
private static CancellationTokenSource Cts = new();
|
||||
internal static CancellationToken Token => Cts.Token;
|
||||
|
||||
// Tracks in-flight loads so Dispose can drain them before teardown.
|
||||
private static readonly ConcurrentBag<Task> PendingLoads = new();
|
||||
|
||||
internal static void TrackLoad(Task loadTask, string emoteCode)
|
||||
{
|
||||
PendingLoads.Add(
|
||||
loadTask.ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Plugin.LogProxy.Error(
|
||||
t.Exception!,
|
||||
$"EmoteCache load failed for {emoteCode}"
|
||||
);
|
||||
},
|
||||
TaskScheduler.Default
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task LoadData()
|
||||
{
|
||||
if (State is not LoadingState.Unloaded)
|
||||
return;
|
||||
|
||||
// Reset CTS if Dispose was called and the plugin is being re-enabled.
|
||||
if (Cts.IsCancellationRequested)
|
||||
Cts = new CancellationTokenSource();
|
||||
|
||||
State = LoadingState.Loading;
|
||||
var ct = Cts.Token;
|
||||
try
|
||||
{
|
||||
var global = await Client.GetAsync(GlobalEmotes, ct);
|
||||
var globalList = await global.Content.ReadAsStringAsync(ct);
|
||||
|
||||
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
|
||||
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
|
||||
Cache.TryAdd(emote.Code, emote);
|
||||
|
||||
var lastId = string.Empty;
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId), ct);
|
||||
var topList = await top.Content.ReadAsStringAsync(ct);
|
||||
|
||||
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
||||
// BetterTTV occasionally returns entries with a null Code;
|
||||
// skip them so a single bad row doesn't break the whole cache.
|
||||
foreach (var emote in jsonList)
|
||||
if (
|
||||
!string.IsNullOrEmpty(emote.Emote.Code)
|
||||
&& !NotWorking.Contains(emote.Emote.Code)
|
||||
)
|
||||
Cache.TryAdd(emote.Emote.Code, emote.Emote);
|
||||
|
||||
lastId = jsonList.Last().Id;
|
||||
}
|
||||
|
||||
SortedCodeArray = Cache.Keys.Order().ToArray();
|
||||
State = LoadingState.Done;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Plugin disposed mid-load; State stays on Loading so re-enable can retry.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||
State = LoadingState.Unloaded;
|
||||
Plugin.LogProxy.Error(ex, "BetterTTV cache wasn't initialized");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
Cts.Cancel();
|
||||
|
||||
// 5s upper bound; anything still running gets abandoned.
|
||||
try
|
||||
{
|
||||
Task.WaitAll(PendingLoads.ToArray(), TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
// Faults already logged in TrackLoad.
|
||||
}
|
||||
|
||||
while (PendingLoads.TryTake(out _)) { }
|
||||
|
||||
foreach (var emote in EmoteImages.Values)
|
||||
emote.InnerDispose();
|
||||
}
|
||||
|
||||
internal static bool Exists(string code)
|
||||
{
|
||||
return State is LoadingState.Done && SortedCodeArray.Contains(code);
|
||||
}
|
||||
|
||||
internal static EmoteBase? GetEmote(string code)
|
||||
{
|
||||
if (State is not LoadingState.Done)
|
||||
return null;
|
||||
|
||||
if (!Cache.TryGetValue(code, out var emoteDetail))
|
||||
return null;
|
||||
|
||||
if (EmoteImages.TryGetValue(emoteDetail.Id, out var emote))
|
||||
return emote;
|
||||
|
||||
try
|
||||
{
|
||||
if (emoteDetail.ImageType == "gif")
|
||||
{
|
||||
var animatedEmote = new ImGuiGif().Prepare(emoteDetail);
|
||||
EmoteImages.Add(emoteDetail.Id, animatedEmote);
|
||||
return animatedEmote;
|
||||
}
|
||||
|
||||
var staticEmote = new ImGuiEmote().Prepare(emoteDetail);
|
||||
EmoteImages.Add(emoteDetail.Id, staticEmote);
|
||||
|
||||
return staticEmote;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Plugin.LogProxy.Error("Failed to convert");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class EmoteBase
|
||||
{
|
||||
public bool Failed;
|
||||
public bool IsLoaded;
|
||||
|
||||
public byte[] RawData = [];
|
||||
|
||||
protected IDalamudTextureWrap? Texture;
|
||||
|
||||
public virtual void Draw(Vector2 size)
|
||||
{
|
||||
ImGui.Image(Texture!.Handle, size);
|
||||
}
|
||||
|
||||
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
||||
{
|
||||
// Path-traversal guard: resolve and verify the candidate path stays
|
||||
// inside the cache directory before reading or writing.
|
||||
var dir = Path.GetFullPath(
|
||||
Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1")
|
||||
);
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
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, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
||||
RawData = await content.Content.ReadAsByteArrayAsync(ct);
|
||||
|
||||
await using var stream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read
|
||||
);
|
||||
await stream.WriteAsync(RawData, ct);
|
||||
}
|
||||
|
||||
return RawData;
|
||||
}
|
||||
|
||||
public abstract void InnerDispose();
|
||||
}
|
||||
|
||||
public sealed class ImGuiEmote : EmoteBase
|
||||
{
|
||||
public ImGuiEmote Prepare(Emote emote)
|
||||
{
|
||||
var ct = EmoteCache.Token;
|
||||
// Task.Run keeps the sync prefix off the ImGui render thread.
|
||||
EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code);
|
||||
return this;
|
||||
}
|
||||
|
||||
private async Task LoadAsyncTracked(Emote emote, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var image = await LoadAsync(emote, ct);
|
||||
if (image.Length <= 0)
|
||||
return;
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(
|
||||
image,
|
||||
cancellationToken: ct
|
||||
);
|
||||
IsLoaded = true;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Failed = true;
|
||||
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void InnerDispose()
|
||||
{
|
||||
Texture?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImGuiGif : EmoteBase
|
||||
{
|
||||
private List<(IDalamudTextureWrap Texture, float Delay)> Frames = [];
|
||||
private float FrameTimer;
|
||||
private int CurrentFrame;
|
||||
private ulong GlobalFrameCount;
|
||||
|
||||
public override void Draw(Vector2 size)
|
||||
{
|
||||
if (Frames.Count == 0)
|
||||
return;
|
||||
|
||||
if (CurrentFrame >= Frames.Count)
|
||||
{
|
||||
CurrentFrame = 0;
|
||||
FrameTimer = -1f;
|
||||
}
|
||||
|
||||
var frame = Frames[CurrentFrame];
|
||||
if (FrameTimer <= 0.0f)
|
||||
FrameTimer = frame.Delay;
|
||||
|
||||
ImGui.Image(frame.Texture.Handle, size);
|
||||
|
||||
if (GlobalFrameCount == Plugin.Interface.UiBuilder.FrameCount)
|
||||
return;
|
||||
|
||||
GlobalFrameCount = Plugin.Interface.UiBuilder.FrameCount;
|
||||
|
||||
FrameTimer -= ImGui.GetIO().DeltaTime;
|
||||
if (FrameTimer <= 0f)
|
||||
CurrentFrame++;
|
||||
}
|
||||
|
||||
public override void InnerDispose()
|
||||
{
|
||||
Frames.ForEach(f => f.Texture.Dispose());
|
||||
Frames.Clear();
|
||||
}
|
||||
|
||||
public ImGuiGif Prepare(Emote emote)
|
||||
{
|
||||
var ct = EmoteCache.Token;
|
||||
EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code);
|
||||
return this;
|
||||
}
|
||||
|
||||
private async Task LoadAsyncTracked(Emote emote, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var image = await LoadAsync(emote, ct);
|
||||
if (image.Length <= 0)
|
||||
return;
|
||||
|
||||
using var ms = new MemoryStream(image);
|
||||
using var img = Image.Load<Rgba32>(ms);
|
||||
if (img.Frames.Count == 0)
|
||||
return;
|
||||
|
||||
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
||||
foreach (var frame in img.Frames)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||
|
||||
// Match browser behaviour: anything under 20ms rounds up to 100ms.
|
||||
if (delay < 0.02f)
|
||||
delay = 0.1f;
|
||||
|
||||
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||
frame.CopyPixelDataTo(buffer);
|
||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(
|
||||
RawImageSpecification.Rgba32(frame.Width, frame.Height),
|
||||
buffer,
|
||||
cancellationToken: ct
|
||||
);
|
||||
frames.Add((tex, delay));
|
||||
}
|
||||
|
||||
Frames = frames;
|
||||
IsLoaded = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Plugin disposed mid-load; release any partial frames.
|
||||
foreach (var f in Frames)
|
||||
f.Texture.Dispose();
|
||||
Frames = [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Failed = true;
|
||||
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Export;
|
||||
|
||||
@@ -33,9 +34,23 @@ internal static class ExportFormatExt
|
||||
}
|
||||
|
||||
// Serializes message snapshots to Markdown, JSON, or CSV.
|
||||
// Caller handles pre-filtering except sender substring, which requires deserialized SeString.TextValue.
|
||||
//
|
||||
// Text comes from the chunk lists, never from SenderSource/ContentSource. Those
|
||||
// are raw SeStrings, and reading TextValue on one containing an auto-translate
|
||||
// phrase reaches SeStringEvaluator, which asserts it is on the main thread and
|
||||
// throws unconditionally when a macro resolves a global number. An export runs on
|
||||
// a worker, so that would abort it partway and leave half a file behind.
|
||||
//
|
||||
// The chunks are already resolved: ChunkUtil turns auto-translate into text at
|
||||
// ingest, and the full-text index reads them exactly this way. Same strings, no
|
||||
// evaluator, no thread affinity.
|
||||
//
|
||||
// The caller pre-filters by channel and date via StreamForExport; only the sender
|
||||
// substring is applied here.
|
||||
internal static class MessageExporter
|
||||
{
|
||||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
internal record FilterDescription(
|
||||
IReadOnlyCollection<int>? ChatTypes,
|
||||
DateTimeOffset? From,
|
||||
@@ -50,22 +65,83 @@ internal static class MessageExporter
|
||||
FilterDescription filter
|
||||
)
|
||||
{
|
||||
// Rejected before the file is touched. The old order opened the stream
|
||||
// first, so an unknown format left a zero-byte file where the user's
|
||||
// previous export had been.
|
||||
if (!Enum.IsDefined(format))
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, null);
|
||||
|
||||
var matching = filter.SenderSubstring is { Length: > 0 } needle
|
||||
? messages.Where(m => MatchesSender(m, needle))
|
||||
: messages;
|
||||
|
||||
using var writer = new StreamWriter(path, append: false, encoding: Encoding.UTF8);
|
||||
return format switch
|
||||
// Written beside the target and moved into place at the end. A crash or
|
||||
// an unplugged drive halfway through would otherwise leave a file that
|
||||
// opens fine and is quietly incomplete -- and this is the path a GDPR
|
||||
// access request goes out on, where "looks complete" is the dangerous
|
||||
// failure.
|
||||
var temp = path + ".part";
|
||||
int written;
|
||||
try
|
||||
{
|
||||
ExportFormat.Markdown => WriteMarkdown(writer, matching, filter),
|
||||
ExportFormat.Json => WriteJson(writer, matching, filter),
|
||||
ExportFormat.Csv => WriteCsv(writer, matching, filter),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
|
||||
};
|
||||
// Encoding.UTF8 writes a byte order mark, and that is not a
|
||||
// cosmetic detail here: a leading U+FEFF makes the JSON invalid for
|
||||
// every strict parser, Python's json.load included. CSV is the one
|
||||
// format that wants it -- without a BOM Excel guesses the codepage
|
||||
// and mangles every non-ASCII name in the file.
|
||||
var encoding = format == ExportFormat.Csv ? Encoding.UTF8 : Utf8NoBom;
|
||||
|
||||
using (var writer = new StreamWriter(temp, append: false, encoding))
|
||||
{
|
||||
written = format switch
|
||||
{
|
||||
ExportFormat.Markdown => WriteMarkdown(writer, matching, filter),
|
||||
ExportFormat.Json => WriteJson(writer, matching, filter),
|
||||
_ => WriteCsv(writer, matching, filter),
|
||||
};
|
||||
}
|
||||
|
||||
// An export that matched nothing does not replace anything. The
|
||||
// file still has a header and a footer, so moving it would put a
|
||||
// near-empty file where the user's previous export was -- and then
|
||||
// report "no message matched the filter", which reads as "nothing
|
||||
// happened". Dalamud's save dialog has no overwrite confirmation to
|
||||
// fall back on.
|
||||
if (written == 0)
|
||||
{
|
||||
TryDeleteTemp(temp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
File.Move(temp, path, overwrite: true);
|
||||
return written;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TryDeleteTemp(temp);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Best effort: the export already failed, and a leftover .part file is a
|
||||
// smaller problem than masking the original exception with an IO one.
|
||||
private static void TryDeleteTemp(string temp)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(temp))
|
||||
File.Delete(temp);
|
||||
}
|
||||
catch (IOException) { }
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
|
||||
private static bool MatchesSender(Message m, string needle) =>
|
||||
m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
SenderText(m).Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string SenderText(Message m) => ChunkUtil.ToRawString(m.Sender);
|
||||
|
||||
private static string ContentText(Message m) => ChunkUtil.ToRawString(m.Content);
|
||||
|
||||
private static int WriteMarkdown(
|
||||
StreamWriter w,
|
||||
@@ -94,8 +170,8 @@ internal static class MessageExporter
|
||||
}
|
||||
|
||||
var chatType = (ChatType)(ushort)m.Code.Type;
|
||||
var sender = m.SenderSource.TextValue.Trim().Trim('<', '>', '[', ']', ':').Trim();
|
||||
var content = m.ContentSource.TextValue;
|
||||
var sender = SenderText(m).Trim().Trim('<', '>', '[', ']', ':').Trim();
|
||||
var content = ContentText(m);
|
||||
|
||||
if (string.IsNullOrEmpty(sender))
|
||||
w.WriteLine($"**[{localDate:HH:mm}] {chatType}:** {content}");
|
||||
@@ -170,12 +246,17 @@ internal static class MessageExporter
|
||||
w.Write($",\"date\":\"{m.Date.ToString("O", CultureInfo.InvariantCulture)}\"");
|
||||
w.Write($",\"chat_type\":{(int)m.Code.Type}");
|
||||
w.Write($",\"chat_type_name\":\"{chatType}\"");
|
||||
w.Write($",\"source_kind\":{m.Code.Source}");
|
||||
w.Write($",\"target_kind\":{m.Code.Target}");
|
||||
// Cast, not interpolate. These are XivChatRelationKind, and string
|
||||
// interpolation of an enum writes the member name -- so every
|
||||
// message with a recognised relation produced
|
||||
// "source_kind":LocalPlayer, which no parser accepts. This is the
|
||||
// file an access request goes out on.
|
||||
w.Write($",\"source_kind\":{(int)m.Code.Source}");
|
||||
w.Write($",\"target_kind\":{(int)m.Code.Target}");
|
||||
w.Write($",\"receiver\":{m.Receiver}");
|
||||
w.Write($",\"content_id\":{m.ContentId}");
|
||||
w.Write($",\"sender\":{JsonString(m.SenderSource.TextValue)}");
|
||||
w.Write($",\"content\":{JsonString(m.ContentSource.TextValue)}");
|
||||
w.Write($",\"sender\":{JsonString(SenderText(m))}");
|
||||
w.Write($",\"content\":{JsonString(ContentText(m))}");
|
||||
w.Write("}");
|
||||
}
|
||||
|
||||
@@ -203,9 +284,9 @@ internal static class MessageExporter
|
||||
w.Write(',');
|
||||
w.Write(CsvString(chatType.ToString()));
|
||||
w.Write(',');
|
||||
w.Write(CsvString(m.SenderSource.TextValue));
|
||||
w.Write(CsvString(SenderText(m)));
|
||||
w.Write(',');
|
||||
w.Write(CsvString(m.ContentSource.TextValue));
|
||||
w.Write(CsvString(ContentText(m)));
|
||||
w.Write(',');
|
||||
w.Write(m.Receiver);
|
||||
w.Write(',');
|
||||
@@ -258,8 +339,17 @@ internal static class MessageExporter
|
||||
|
||||
private static string CsvString(string s)
|
||||
{
|
||||
// Leading =, +, - and @ make a spreadsheet treat the cell as a formula.
|
||||
// Every value here is text somebody else typed into a chat channel, and
|
||||
// this file exists to be opened in Excel, so a prefixed apostrophe goes
|
||||
// in front. It is the standard defence and it costs one character that
|
||||
// spreadsheets hide.
|
||||
if (s.Length > 0 && s[0] is '=' or '+' or '-' or '@' or '\t' or '\r')
|
||||
s = "'" + s;
|
||||
|
||||
if (s.IndexOfAny(['"', ',', '\n', '\r']) < 0)
|
||||
return s;
|
||||
|
||||
return "\"" + s.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
+299
-51
@@ -6,10 +6,12 @@ using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui.StyleEngine;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// Two LogProxy sites live in static methods (TryGetHellionFontBytes,
|
||||
// Two LogProxy sites live in static methods (TryGetBundledFontBytes,
|
||||
// AddFontWithFallback); a ctor-injected ILogger would not be reachable
|
||||
// from those scopes, so the class stays on Plugin.LogProxy.
|
||||
//
|
||||
@@ -39,9 +41,70 @@ public sealed class FontManager : IDisposable
|
||||
internal IFontHandle? RegularFont;
|
||||
internal IFontHandle? ItalicFont;
|
||||
|
||||
// v1.13.0: one handle per type role that needs a face of its own. Sender
|
||||
// carries extra weight, Meta a smaller size on a tiny glyph range.
|
||||
internal IFontHandle? SenderFont;
|
||||
internal IFontHandle? MetaFont;
|
||||
|
||||
// The mockup asks for weight 600 on the sender. There is no bold face in the
|
||||
// plugin and none in the bundled file, so the weight comes from a denser
|
||||
// rasterisation of the same outline. 1.0 is the SafeFontConfig default;
|
||||
// below ~1.2 the difference is not visible, above ~1.4 the glyphs smear.
|
||||
//
|
||||
// Note on cost: all three delegate handles now carry the full glyph range.
|
||||
// The meta face started with an ASCII-sized one, on the assumption it would
|
||||
// only ever draw clocks and world names -- then the channel header wanted
|
||||
// to use it, and tab names are free user input. An umlaut would have been
|
||||
// enough to break it.
|
||||
//
|
||||
// Not const: the smoke test compares three values side by side, and the
|
||||
// widget gallery exposes it.
|
||||
internal static float SenderWeight = 1.3f;
|
||||
|
||||
// Wired post-build; a Func keeps FontManager off the theme layer.
|
||||
private Func<ThemeTypography?>? _typographySource;
|
||||
|
||||
// Lets RebuildDelegateFontsIfChanged skip rebuilds when the size is unchanged.
|
||||
private (
|
||||
float Global,
|
||||
float Symbols,
|
||||
float Sender,
|
||||
float Meta,
|
||||
float Italic
|
||||
) _lastBuiltFingerprint;
|
||||
|
||||
// True once every required atlas-owned handle reports Available. Components
|
||||
// gate their first-frame draw on this — without it the layout math would
|
||||
// run against placeholder font metrics and snap when the real atlas
|
||||
// finishes building. ItalicFont being null means italics are disabled in
|
||||
// config, which is a ready state, not a pending one.
|
||||
public bool FontsReady =>
|
||||
Axis.Available
|
||||
&& AxisItalic.Available
|
||||
&& FontAwesome.Available
|
||||
&& RegularFont is { Available: true }
|
||||
&& (ItalicFont is null || ItalicFont.Available)
|
||||
// Unconditional, unlike ItalicFont: these two are always built. A handle
|
||||
// that is not ready yet makes SimplePushedFont push nothing at all --
|
||||
// silently -- so the first frame after a rebuild would measure the wrong
|
||||
// face and write those heights into the row cache.
|
||||
&& SenderFont is { Available: true }
|
||||
&& MetaFont is { Available: true };
|
||||
|
||||
private ushort[] Ranges = [];
|
||||
private ushort[] JpRange = [];
|
||||
|
||||
// Trimmed remainder the NotoSansCjk fallback is the sole source for
|
||||
// (Hangul + full Han); excludes the Default/Latin block already merged
|
||||
// by the global font, so the fallback no longer re-merges the full Ranges array.
|
||||
private ushort[] CjkFallbackGlyphRange = [];
|
||||
|
||||
// Report accessor for the ctor self-test: built glyph-range array lengths so
|
||||
// the step can show the dedup effect (a small trimmed fallback vs the large
|
||||
// primary range) in its on-disk report instead of a bare Pass.
|
||||
internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths =>
|
||||
(Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length);
|
||||
|
||||
public static readonly HashSet<float> AxisFontSizeList =
|
||||
[
|
||||
9.6f,
|
||||
@@ -62,8 +125,8 @@ public sealed class FontManager : IDisposable
|
||||
90f,
|
||||
];
|
||||
|
||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||
private static byte[]? HellionFontBytes;
|
||||
// Bundled UI font bytes (Inter Light, OFL-1.1); lazily loaded from manifest resources
|
||||
private static byte[]? BundledFontBytes;
|
||||
|
||||
public FontManager(IDalamudPluginInterface pluginInterface)
|
||||
{
|
||||
@@ -91,7 +154,13 @@ public sealed class FontManager : IDisposable
|
||||
|
||||
if (Plugin.Config.ItalicEnabled)
|
||||
ItalicFont = BuildItalicFontHandle(atlas);
|
||||
|
||||
SenderFont = BuildSenderFontHandle(atlas);
|
||||
MetaFont = BuildMetaFontHandle(atlas);
|
||||
}
|
||||
|
||||
// Source is still null here, so this is the config-only baseline.
|
||||
_lastBuiltFingerprint = EffectiveFontFingerprint();
|
||||
}
|
||||
|
||||
// Called from the settings save path when one of the font-related
|
||||
@@ -108,39 +177,165 @@ public sealed class FontManager : IDisposable
|
||||
|
||||
var atlas = _pluginInterface.UiBuilder.FontAtlas;
|
||||
|
||||
RegularFont?.Dispose();
|
||||
RegularFont = BuildRegularFontHandle(atlas);
|
||||
// Without the suppression each handle triggers its own atlas rebuild.
|
||||
// With two handles that was tolerable; with four it is four rebuilds for
|
||||
// one size change.
|
||||
using (atlas.SuppressAutoRebuild())
|
||||
{
|
||||
RegularFont?.Dispose();
|
||||
RegularFont = BuildRegularFontHandle(atlas);
|
||||
|
||||
ItalicFont?.Dispose();
|
||||
ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null;
|
||||
ItalicFont?.Dispose();
|
||||
ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null;
|
||||
|
||||
SenderFont?.Dispose();
|
||||
SenderFont = BuildSenderFontHandle(atlas);
|
||||
|
||||
MetaFont?.Dispose();
|
||||
MetaFont = BuildMetaFontHandle(atlas);
|
||||
}
|
||||
|
||||
_lastBuiltFingerprint = EffectiveFontFingerprint();
|
||||
}
|
||||
|
||||
public void SetTypographySource(Func<ThemeTypography?> source) => _typographySource = source;
|
||||
|
||||
internal float ResolveGlobalFontPt() =>
|
||||
FontSizeResolver.ResolveGlobalPt(
|
||||
_typographySource?.Invoke(),
|
||||
Plugin.Config.UseHellionFont,
|
||||
Plugin.Config.FontSizeV2,
|
||||
Plugin.Config.GlobalFontV2.SizePt
|
||||
);
|
||||
|
||||
internal float ResolveSymbolsFontPt() =>
|
||||
FontSizeResolver.ResolveSymbolsPt(
|
||||
_typographySource?.Invoke(),
|
||||
Plugin.Config.SymbolsFontSizeV2
|
||||
);
|
||||
|
||||
// Every size that can land in one message row. Roles follow the base size
|
||||
// arithmetically, but the resolved value is what the height cache has to key
|
||||
// on -- a theme override moves the base without moving any factor.
|
||||
internal (
|
||||
float Global,
|
||||
float Symbols,
|
||||
float Sender,
|
||||
float Meta,
|
||||
float Italic
|
||||
) EffectiveFontFingerprint()
|
||||
{
|
||||
var basePt = ResolveGlobalFontPt();
|
||||
return (
|
||||
basePt,
|
||||
ResolveSymbolsFontPt(),
|
||||
TypeScale.SizePtOf(TypeRole.Sender, basePt),
|
||||
TypeScale.SizePtOf(TypeRole.Meta, basePt),
|
||||
Plugin.Config.ItalicFontV2.SizePt
|
||||
);
|
||||
}
|
||||
|
||||
// Rebuilds only when the effective size changed (live fingerprint, TOCTOU-free).
|
||||
// The atlas rebuild must run on the framework/draw thread — callers ensure that.
|
||||
internal void RebuildDelegateFontsIfChanged()
|
||||
{
|
||||
if (EffectiveFontFingerprint() != _lastBuiltFingerprint)
|
||||
{
|
||||
RebuildDelegateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
// Instance method so Ranges / JpRange are reachable without parameter
|
||||
// plumbing; PascalCase field names follow the existing class style.
|
||||
// Shared CJK + symbols tail for both the regular and italic delegate
|
||||
// fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode),
|
||||
// so this runs AFTER the primary font is set as config.MergeFont. The CJK
|
||||
// fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true
|
||||
// (global=Inter-Light), so it stays in the chain — only its glyph range is
|
||||
// trimmed (CjkFallbackGlyphRange) to drop the Default-block/endonym overlap.
|
||||
// The Japanese merge keeps its own configured size and the full JpRange (which
|
||||
// owns Traditional Han such as 體 U+9AD4), so japanese↔fallback no longer overlap.
|
||||
private void AddCjkAndSymbols(
|
||||
IFontAtlasBuildToolkitPreBuild tk,
|
||||
SafeFontConfig config,
|
||||
float basePt
|
||||
)
|
||||
{
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
config.GlyphRanges = JpRange;
|
||||
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||
|
||||
// NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange. Merged last so earlier fonts win.
|
||||
config.SizePt = basePt;
|
||||
config.GlyphRanges = CjkFallbackGlyphRange;
|
||||
AddFontWithFallback(
|
||||
tk,
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
|
||||
config,
|
||||
"noto-cjk-fallback"
|
||||
);
|
||||
|
||||
config.SizePt = ResolveSymbolsFontPt();
|
||||
tk.AddGameSymbol(config);
|
||||
}
|
||||
|
||||
private IFontHandle BuildRegularFontHandle(IFontAtlas atlas) =>
|
||||
atlas.NewDelegateFontHandle(e =>
|
||||
e.OnPreBuild(tk =>
|
||||
{
|
||||
// UseHellionFont swaps the source font but keeps the size
|
||||
// selector tied to FontSizeV2 (the Hellion font ships as
|
||||
// a single weight).
|
||||
var basePt = Plugin.Config.UseHellionFont
|
||||
? Plugin.Config.FontSizeV2
|
||||
: Plugin.Config.GlobalFontV2.SizePt;
|
||||
var basePt = ResolveGlobalFontPt();
|
||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||
// Missing embedded resource falls back to the configured
|
||||
// system font instead of taking the whole UiBuilder down.
|
||||
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
|
||||
config.MergeFont = hellionBytes is not null
|
||||
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
|
||||
var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null;
|
||||
config.MergeFont = bundledBytes is not null
|
||||
? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
config.GlyphRanges = JpRange;
|
||||
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||
AddCjkAndSymbols(tk, config, basePt);
|
||||
|
||||
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
||||
tk.AddGameSymbol(config);
|
||||
tk.Font = config.MergeFont;
|
||||
})
|
||||
);
|
||||
|
||||
// Same outline as the body face, rasterised denser. Only works on the
|
||||
// delegate path: with FontsEnabled and UseHellionFont both off the game's own
|
||||
// Axis handle draws, and a game font handle has no such knob. The sender then
|
||||
// leans on channel colour alone, which is a deliberate limitation.
|
||||
private IFontHandle BuildSenderFontHandle(IFontAtlas atlas) =>
|
||||
atlas.NewDelegateFontHandle(e =>
|
||||
e.OnPreBuild(tk =>
|
||||
{
|
||||
var basePt = TypeScale.SizePtOf(TypeRole.Sender, ResolveGlobalFontPt());
|
||||
var config = new SafeFontConfig
|
||||
{
|
||||
SizePt = basePt,
|
||||
GlyphRanges = Ranges,
|
||||
RasterizerMultiply = SenderWeight,
|
||||
};
|
||||
var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null;
|
||||
config.MergeFont = bundledBytes is not null
|
||||
? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light-Sender")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "sender");
|
||||
|
||||
AddCjkAndSymbols(tk, config, basePt);
|
||||
|
||||
tk.Font = config.MergeFont;
|
||||
})
|
||||
);
|
||||
|
||||
private IFontHandle BuildMetaFontHandle(IFontAtlas atlas) =>
|
||||
atlas.NewDelegateFontHandle(e =>
|
||||
e.OnPreBuild(tk =>
|
||||
{
|
||||
var basePt = TypeScale.SizePtOf(TypeRole.Meta, ResolveGlobalFontPt());
|
||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||
var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null;
|
||||
config.MergeFont = bundledBytes is not null
|
||||
? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light-Meta")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "meta");
|
||||
|
||||
AddCjkAndSymbols(tk, config, basePt);
|
||||
|
||||
tk.Font = config.MergeFont;
|
||||
})
|
||||
@@ -162,12 +357,7 @@ public sealed class FontManager : IDisposable
|
||||
"italic"
|
||||
);
|
||||
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
config.GlyphRanges = JpRange;
|
||||
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||
|
||||
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
||||
tk.AddGameSymbol(config);
|
||||
AddCjkAndSymbols(tk, config, Plugin.Config.ItalicFontV2.SizePt);
|
||||
|
||||
tk.Font = config.MergeFont;
|
||||
})
|
||||
@@ -181,37 +371,43 @@ public sealed class FontManager : IDisposable
|
||||
// lifetime, so the plugin must not dispose it.
|
||||
RegularFont?.Dispose();
|
||||
ItalicFont?.Dispose();
|
||||
SenderFont?.Dispose();
|
||||
MetaFont?.Dispose();
|
||||
}
|
||||
|
||||
// Returns null when the embedded font resource is missing. Should not
|
||||
// happen on a signed release build, but a broken csproj or hand-rolled
|
||||
// dev build can land here. Caller falls back to the system font path
|
||||
// so the plugin still loads instead of crashing the whole UiBuilder.
|
||||
private static byte[]? TryGetHellionFontBytes()
|
||||
private static byte[]? TryGetBundledFontBytes()
|
||||
{
|
||||
if (HellionFontBytes is not null)
|
||||
return HellionFontBytes;
|
||||
if (BundledFontBytes is not null)
|
||||
return BundledFontBytes;
|
||||
|
||||
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||
"HellionFont.ttf"
|
||||
"Inter-Light.ttf"
|
||||
);
|
||||
if (stream is null)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"Hellion font resource missing — falling back to system default font."
|
||||
"Bundled Inter Light font resource missing, falling back to system default font."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
HellionFontBytes = ms.ToArray();
|
||||
return HellionFontBytes;
|
||||
BundledFontBytes = ms.ToArray();
|
||||
return BundledFontBytes;
|
||||
}
|
||||
|
||||
private unsafe void SetUpRanges()
|
||||
{
|
||||
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
|
||||
ushort[] BuildRange(
|
||||
IReadOnlyList<ushort>? chars,
|
||||
bool includeCommonExtras,
|
||||
params nint[] ranges
|
||||
)
|
||||
{
|
||||
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
|
||||
foreach (var range in ranges)
|
||||
@@ -229,31 +425,83 @@ public sealed class FontManager : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Ingame supported ranges
|
||||
var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
|
||||
foreach (var c in reader.Glyphs)
|
||||
builder.AddChar(c.Char);
|
||||
// Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics)
|
||||
// belong to the primary/Japanese ranges only. The trimmed CJK fallback
|
||||
// skips them so it stays a pure Hangul/Simplified-Han remainder and
|
||||
// does not re-merge the Default-block work the global font already did.
|
||||
if (includeCommonExtras)
|
||||
{
|
||||
// Ingame supported ranges
|
||||
var reader = new FdtReader(
|
||||
Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data
|
||||
);
|
||||
foreach (var c in reader.Glyphs)
|
||||
builder.AddChar(c.Char);
|
||||
|
||||
// French
|
||||
// Romanian
|
||||
builder.AddText("Œœ");
|
||||
builder.AddText("ĂăÂâÎîȘșȚț");
|
||||
// French
|
||||
// Romanian
|
||||
builder.AddText("Œœ");
|
||||
builder.AddText("ĂăÂâÎîȘșȚț");
|
||||
|
||||
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
|
||||
for (var i = 0x2460; i <= 0x24B5; i++)
|
||||
builder.AddChar((char)i);
|
||||
// v1.5.3: language-dropdown endonyms. The dropdown renders
|
||||
// with the currently active font range; without these glyphs
|
||||
// a user on an English UI cannot read non-Latin language names
|
||||
// before switching. Auto-activation in Settings.Apply then
|
||||
// pulls in the full ExtraGlyphRange for the chosen locale.
|
||||
builder.AddText(
|
||||
"Català Čeština Dansk Deutsch Ελληνικά English Español Suomi"
|
||||
+ " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands"
|
||||
+ " Polski Português Brasil (Portugal) Română Русский Svenska"
|
||||
+ " Türkçe Українська 简体中文 繁體中文"
|
||||
);
|
||||
|
||||
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
|
||||
for (var i = 0x2460; i <= 0x24B5; i++)
|
||||
builder.AddChar((char)i);
|
||||
|
||||
builder.AddChar('⓪');
|
||||
}
|
||||
|
||||
builder.AddChar('⓪');
|
||||
return builder.BuildRangesToArray();
|
||||
}
|
||||
|
||||
var ranges = new List<nint> { (nint)ImGui.GetIO().Fonts.GetGlyphRangesDefault() };
|
||||
var customChars = new List<ushort>();
|
||||
foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
|
||||
if (Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
|
||||
ranges.Add(extraRange.Range());
|
||||
{
|
||||
if (!Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
|
||||
continue;
|
||||
|
||||
Ranges = BuildRange(null, ranges.ToArray());
|
||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
||||
// LatinExtended and Greek use AddChar pairs because they have no
|
||||
// built-in ImGui range helper; everything else points to a native
|
||||
// ImGui glyph-range table.
|
||||
switch (extraRange)
|
||||
{
|
||||
case ExtraGlyphRanges.LatinExtended:
|
||||
customChars.AddRange(ExtraGlyphRangesExt.LatinExtendedPairs);
|
||||
break;
|
||||
case ExtraGlyphRanges.Greek:
|
||||
customChars.AddRange(ExtraGlyphRangesExt.GreekPairs);
|
||||
break;
|
||||
default:
|
||||
var ptr = extraRange.Range();
|
||||
if (ptr != 0)
|
||||
ranges.Add(ptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ranges = BuildRange(
|
||||
customChars.Count > 0 ? customChars : null,
|
||||
includeCommonExtras: true,
|
||||
ranges.ToArray()
|
||||
);
|
||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true);
|
||||
|
||||
// The fallback gets only the trimmed Hangul/Simplified-Han remainder.
|
||||
// No Default block, no endonyms — those are already merged by the global and
|
||||
// Japanese fonts, so re-merging them on the fallback was wasted atlas work.
|
||||
CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false);
|
||||
}
|
||||
|
||||
// Add font with fallback to NotoSansCjkRegular if unavailable
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using HellionChat.Themes;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// Pure size resolution, split out of FontManager so it is unit-testable without
|
||||
// building the font atlas. A typography override wins; null falls back to config.
|
||||
internal static class FontSizeResolver
|
||||
{
|
||||
internal static float ResolveGlobalPt(
|
||||
ThemeTypography? typography,
|
||||
bool useHellionFont,
|
||||
float fontSizeV2,
|
||||
float globalSizePt
|
||||
) => typography?.OverrideGlobalFontSizePt ?? (useHellionFont ? fontSizeV2 : globalSizePt);
|
||||
|
||||
internal static float ResolveSymbolsPt(ThemeTypography? typography, float symbolsSizePt) =>
|
||||
typography?.OverrideSymbolsFontSizePt ?? symbolsSizePt;
|
||||
}
|
||||
@@ -232,15 +232,13 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (c != '\0' && !char.IsControl(c))
|
||||
input = c.ToString();
|
||||
|
||||
try
|
||||
// Seed the just-typed character into our input field and focus it, the
|
||||
// same InputBar.AppendPending + Activate prefill path inventory item-links
|
||||
// use. Prefill only, deliberately: no tab switch.
|
||||
if (input != null)
|
||||
{
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input }
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
Plugin.InputBar.AppendPending(input);
|
||||
Plugin.InputBar.Activate = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -255,22 +253,12 @@ internal sealed unsafe class Chat : IDisposable
|
||||
addIfNotPresent = add;
|
||||
}
|
||||
|
||||
try
|
||||
// Route the addIfNotPresent token into the InputBar so inventory
|
||||
// right-click "Link item" reaches our input field instead of being lost.
|
||||
if (addIfNotPresent != null && !Plugin.InputBar.PendingMessage.Contains(addIfNotPresent))
|
||||
{
|
||||
// Prevent duplicate calls
|
||||
if (Plugin.ChatLogWindow.TellSpecial)
|
||||
return ChatLogRefreshHook!.Original(log, eventId, value);
|
||||
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(null))
|
||||
{
|
||||
AddIfNotPresent = addIfNotPresent,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
Plugin.InputBar.AppendPending(addIfNotPresent);
|
||||
Plugin.InputBar.Activate = true;
|
||||
}
|
||||
|
||||
return 1; // Prevent vanilla chat log from gaining focus
|
||||
@@ -329,6 +317,30 @@ internal sealed unsafe class Chat : IDisposable
|
||||
ReplyInSelectedChatModeHook!.Original(agent);
|
||||
}
|
||||
|
||||
// Pure /tell-prefill command builder, shared by the two native SetTellTarget
|
||||
// detours and the PayloadHandler Send-Tell payload. Empty/null world drops the
|
||||
// @World suffix (matches the old IsNullOrEmpty guard); trailing space lets the
|
||||
// user type straight after. internal static so the Build-Suite can pin it
|
||||
// frame-free. TEST-MIRROR: ../../../Hellion Build test/GameFunctions/PrefillTellCommandTests.cs
|
||||
internal static string BuildTellCommand(string name, string? world)
|
||||
{
|
||||
var command = $"/tell {name}";
|
||||
if (!string.IsNullOrEmpty(world))
|
||||
command += $"@{world}";
|
||||
command += " ";
|
||||
return command;
|
||||
}
|
||||
|
||||
// Prefills + focuses our own input bar with a /tell command. The DI/Dalamud
|
||||
// plumbing (Plugin.InputBar) lives here; the string assembly is BuildTellCommand.
|
||||
// The in-foray TellSpecial routing (SetEurekaTellChannel) is NOT this helper's
|
||||
// job — it stays at the call-site (v1.8.1 deferral).
|
||||
private void PrefillTellInput(string name, string? world)
|
||||
{
|
||||
Plugin.InputBar.SetPendingMessage(BuildTellCommand(name, world));
|
||||
Plugin.InputBar.Activate = true;
|
||||
}
|
||||
|
||||
private bool SetContextTellTarget(
|
||||
RaptureShellModule* a1,
|
||||
Utf8String* playerName,
|
||||
@@ -342,28 +354,14 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
if (playerName != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = new TellTarget(
|
||||
playerName->ToString(),
|
||||
worldId,
|
||||
contentId,
|
||||
(TellReason)reason
|
||||
);
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(
|
||||
new ChannelSwitchInfo(InputChannel.Tell, permanent: setChatType)
|
||||
)
|
||||
{
|
||||
TellReason = (TellReason)reason,
|
||||
TellTarget = target,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
}
|
||||
// Right-click -> Send Tell: prefill our input the same way our own
|
||||
// "Send Tell" payload menu does (PayloadHandler), then focus. Prefill
|
||||
// only, deliberately: no tab switch, no ChatActivatedArgs revival.
|
||||
// The game supplies worldName here, so no sheet lookup.
|
||||
PrefillTellInput(
|
||||
playerName->ToString(),
|
||||
worldName != null ? worldName->ToString() : null
|
||||
);
|
||||
}
|
||||
|
||||
return SetChatLogTellTargetHook!.Original(
|
||||
@@ -393,27 +391,13 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
if (playerName != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = new TellTarget(
|
||||
playerName->ToString(),
|
||||
worldId,
|
||||
contentId,
|
||||
(TellReason)reason
|
||||
);
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell))
|
||||
{
|
||||
TellReason = (TellReason)reason,
|
||||
TellTarget = target,
|
||||
TellSpecial = Sheets.IsInForay(), // Handle Eureka/Bozja special
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
}
|
||||
// In-foray right-click -> Send Tell: same prefill path as the non-foray
|
||||
// tell. The foray-specific TellSpecial channel routing stays deferred
|
||||
// (v1.8.1, SetEurekaTellChannel) -- prefill only here as well.
|
||||
PrefillTellInput(
|
||||
playerName->ToString(),
|
||||
worldName != null ? worldName->ToString() : null
|
||||
);
|
||||
}
|
||||
|
||||
ContextMenuTellInForayHook!.Original(
|
||||
@@ -467,6 +451,17 @@ internal sealed unsafe class Chat : IDisposable
|
||||
uint currentIndex,
|
||||
RotateMode rotate,
|
||||
Func<uint, bool> validFn
|
||||
) => RotateLinkshellIndex(currentIndex, rotate, validFn);
|
||||
|
||||
// Pure index-stepper (Dalamud-free): wrap (8 + currentIndex + delta) % 8 and return the
|
||||
// first index validFn accepts within 8 iterations, else null. Extracted so the
|
||||
// modulo/termination logic is unit-testable with a synthetic predicate; the
|
||||
// production caller binds validFn to InfoProxyLinkshell (in-game only).
|
||||
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/RotateLinkshellIndexTests.cs
|
||||
internal static uint? RotateLinkshellIndex(
|
||||
uint currentIndex,
|
||||
RotateMode rotate,
|
||||
Func<uint, bool> validFn
|
||||
)
|
||||
{
|
||||
if (rotate == RotateMode.None)
|
||||
@@ -570,9 +565,8 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
||||
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
||||
|
||||
// Send tell via CommandInner later and let the game handle it
|
||||
// Only works because we use the SetTellTargetInForay function to set all required information
|
||||
Plugin.ChatLogWindow.TellSpecial = true;
|
||||
// Send tell via CommandInner later and let the game handle it.
|
||||
// TellSpecial gate is offline until the new chat layer reads it.
|
||||
|
||||
var utfName = Utf8String.FromString(name);
|
||||
var utfWorld = Utf8String.FromString(worldName);
|
||||
|
||||
@@ -7,6 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Ui.Windows;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
||||
@@ -504,33 +505,188 @@ internal unsafe class KeybindManager : IDisposable
|
||||
if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info))
|
||||
return;
|
||||
|
||||
// Resolve the surface this keybind acts on FIRST: a focused pop-out otherwise
|
||||
// the main window. Channel-set/REPLY/prefill all write here so the action
|
||||
// follows the input the user is typing in.
|
||||
var (targetWindow, targetTab) = ResolveKeybindTarget();
|
||||
|
||||
// Surface + focus the resolved target ONCE, before routing. Main: ActivateChat
|
||||
// re-surfaces it from a hide/closed state (the chat-activation entry point
|
||||
// retired in v1.6.0). Pop-out: arm only its focus — NOT ActivateChat, which
|
||||
// would yank the main window to front and un-hide it on every pop-out-targeted
|
||||
// keybind: stay where the user types. Exactly one window arms focus per
|
||||
// keybind, so the next frame has no SetKeyboardFocusHere race.
|
||||
if (targetWindow is ChannelPopoutWindow)
|
||||
targetWindow.RequestInputFocus();
|
||||
else
|
||||
Plugin.Instance.MainWindow?.ActivateChat();
|
||||
|
||||
// The routing tail makes native game calls (GetTellHistoryInfo, UIModule,
|
||||
// RotateLinkshellHistory) on the framework tick — wrap it so one bad frame logs
|
||||
// instead of throwing into Dalamud's update loop (v1.5.6 parity).
|
||||
try
|
||||
{
|
||||
TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null;
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason });
|
||||
if (info.Channel is { } channel && info.Rotate == RotateMode.None)
|
||||
{
|
||||
// Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch
|
||||
// the game channel AND mirror it onto the resolved tab so the input pill
|
||||
// shows the real send target (pill-sync).
|
||||
Plugin.Instance.Functions.Chat.SetChannel(channel);
|
||||
// Only mirror onto the tab when the game actually accepted the switch — an
|
||||
// empty linkshell slot leaves the game channel untouched, so the pill must
|
||||
// stay put rather than show a target the game will not send to.
|
||||
if (Chat.IsChannelOrExistingLinkshell(channel) && targetTab is { } directTab)
|
||||
{
|
||||
directTab.CurrentChannel.SetChannel(channel);
|
||||
directTab.CurrentChannel.TellTarget = null;
|
||||
directTab.CurrentChannel.ResetTempChannel();
|
||||
}
|
||||
}
|
||||
else if (info.Channel is { } rotateChannel && info.Rotate != RotateMode.None)
|
||||
{
|
||||
// Rotation binds (REPLY / linkshell-cycle). Ported from v1.5.6's
|
||||
// ChatLogWindow.Activated (1d3b429:240-334) without the ChatActivatedArgs
|
||||
// indirection (gone in the rewrite). Writes onto the resolved surface's
|
||||
// tab, not Plugin.CurrentTab.
|
||||
if (targetTab is { } rotTab)
|
||||
{
|
||||
var targetChannel = (InputChannel?)rotateChannel;
|
||||
|
||||
// REPLY rotation: the reply target is ALWAYS temp (never permanent —
|
||||
// a permanent reply would leak the partner onto the tab) and ALWAYS
|
||||
// TellReason.Reply. info.Permanent does not gate this step; only the
|
||||
// channel-set tail below honours the _ALWAYS binds' permanence.
|
||||
if (rotateChannel == InputChannel.Tell)
|
||||
{
|
||||
var idx =
|
||||
rotTab.CurrentChannel.TempChannel != InputChannel.Tell ? 0
|
||||
: info.Rotate == RotateMode.Reverse ? -1
|
||||
: 1;
|
||||
|
||||
var tellInfo = Plugin.Instance.Functions.Chat.GetTellHistoryInfo(idx);
|
||||
if (tellInfo != null)
|
||||
rotTab.CurrentChannel.TempTellTarget = new TellTarget(
|
||||
tellInfo.Name,
|
||||
tellInfo.World,
|
||||
tellInfo.ContentId,
|
||||
TellReason.Reply
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cycling AWAY from Tell to a linkshell: drop any stale permanent
|
||||
// tell target so a typed line cannot silently route to the old
|
||||
// partner (v1.5.6 ChatLogWindow.cs:280, privacy guard).
|
||||
rotTab.CurrentChannel.TellTarget = null;
|
||||
}
|
||||
|
||||
// LS/CWLS cycle: permanent rotates the game's own history and reads the
|
||||
// landed cycle index back; temp resolves the next valid linkshell index
|
||||
// without touching game state. Both leave targetChannel null on failure
|
||||
// (no valid linkshell in 8 iterations) so the tail below logs + skips.
|
||||
if (rotateChannel is InputChannel.Linkshell1 or InputChannel.CrossLinkshell1)
|
||||
{
|
||||
var module = UIModule.Instance();
|
||||
if (info.Permanent)
|
||||
{
|
||||
if (rotateChannel == InputChannel.Linkshell1)
|
||||
{
|
||||
Chat.RotateLinkshellHistory(info.Rotate);
|
||||
targetChannel = rotateChannel + (uint)module->LinkshellCycle;
|
||||
}
|
||||
else
|
||||
{
|
||||
Chat.RotateCrossLinkshellHistory(info.Rotate);
|
||||
targetChannel =
|
||||
rotateChannel + (uint)module->CrossWorldLinkshellCycle;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetChannel = Chat.ResolveTempInputChannel(
|
||||
rotTab.CurrentChannel.TempChannel,
|
||||
rotateChannel,
|
||||
info.Rotate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared channel-set tail (runs for Tell too: IsChannelOrExistingLinkshell
|
||||
// is true for Tell and targetChannel stays Tell). Permanent => commit the
|
||||
// game channel; temp => arm UseTempChannel/TempChannel only. This is the
|
||||
// ONLY place info.Permanent decides temp vs permanent for the channel.
|
||||
if (
|
||||
targetChannel is null
|
||||
|| !Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||
)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rotation channel resolved to an invalid value '{Channel}', ignoring",
|
||||
targetChannel
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.Permanent)
|
||||
{
|
||||
// 1.5.6 parity (ChatLogWindow.SetChannel, 1d3b429:1476-1479):
|
||||
// committing the game channel also pre-targets the game's native input.
|
||||
// Forward the tab's reply target for Tell so the partner is armed
|
||||
// game-side (ChangeChatChannel code 17); null for a linkshell —
|
||||
// targetChannel is the FINAL resolved value (9..16/19..26 for LS, never
|
||||
// 0=Tell), so a stale TempTellTarget can never flip an LS cycle to Tell.
|
||||
var gameTarget =
|
||||
targetChannel.Value == InputChannel.Tell
|
||||
? rotTab.CurrentChannel.TempTellTarget
|
||||
?? rotTab.CurrentChannel.TellTarget
|
||||
: null;
|
||||
Plugin.Instance.Functions.Chat.SetChannel(targetChannel.Value, gameTarget);
|
||||
rotTab.CurrentChannel.SetChannel(targetChannel.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
rotTab.CurrentChannel.UseTempChannel = true;
|
||||
rotTab.CurrentChannel.TempChannel = targetChannel.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefill text binds (CMD_COMMAND seeds "/"): the token always goes to the
|
||||
// main InputBar (the focus contract does not expose pop-out buffers); a
|
||||
// focused pop-out already received focus above, so only token routing matters
|
||||
// here -- a documented scope limit.
|
||||
if (info.Text is { } text)
|
||||
Plugin.Instance.InputBar.SetPendingMessage(text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
_logger.LogError(ex, "Keybind routing failed for channel {Channel}", info.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
// v0.6.0 — central dispatch for ChatTabForward/Backward. If a pop-out
|
||||
// window currently has its compact input focused, the keybind is
|
||||
// forwarded into that pop-out's ChatInputBar so the user navigates
|
||||
// tabs in the window they are typing in. Otherwise the main window
|
||||
// handles it (= v0.5.x behavior).
|
||||
// Resolve which chat surface a keybind action targets: the open pop-out whose
|
||||
// input currently has focus, otherwise the main window. Both paths share it so a
|
||||
// channel-switch/REPLY/prefill follows the surface the user is typing in. The
|
||||
// returned tab is that surface's bound tab (pop-out: Bound; main: ActiveTab).
|
||||
// Null tab => skip the tab-write (early-load window where no tab exists yet).
|
||||
private (IFocusableChatWindow Window, Tab? Tab) ResolveKeybindTarget()
|
||||
{
|
||||
foreach (var popout in Plugin.Instance.ChannelPopoutPool.Instances)
|
||||
{
|
||||
if (popout.Bound is { } bound && popout.IsOpen && popout.HasFocusedInput)
|
||||
return (popout, bound);
|
||||
}
|
||||
|
||||
var main = Plugin.Instance.MainWindow;
|
||||
return (main!, main?.ActiveTab);
|
||||
}
|
||||
|
||||
// Tab-delta keybinds (ChatTabForward/Backward) stay main-window-only by design:
|
||||
// a channel-bound pop-out has no tab list to cycle. The focus contract is
|
||||
// consumed by the channel-set/REPLY/prefill tail, not here.
|
||||
private void DispatchTabDelta(int delta)
|
||||
{
|
||||
foreach (var popout in Plugin.ChatLogWindow.ActivePopouts)
|
||||
{
|
||||
if (popout.HasFocusedInputBar && popout.InputBar != null)
|
||||
{
|
||||
popout.InputBar.HandleKeybindForward(delta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Plugin.ChatLogWindow.ChangeTabDelta(delta);
|
||||
Plugin.Instance.MainWindow?.ChangeTabDelta(delta);
|
||||
}
|
||||
|
||||
private static Keybind GetKeybind(string id)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.5.1</Version>
|
||||
<Version>2.0.4</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageReference Include="MessagePack" Version="[3.1.7, 4.0.0)" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.DependencyInjection"
|
||||
@@ -26,6 +26,11 @@
|
||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||
<!-- NAudio.WinMM 2.2.1 MIT - WaveOutEvent/WinMM path is Wine-safe (WaveOut works under Wine,
|
||||
Media-Foundation-based codecs do not). Using the sub-package avoids pulling in
|
||||
NAudio.WinForms (which requires WindowsDesktop and does not build on Linux hosts).
|
||||
WaveOutEvent and WaveFileReader both live in NAudio.WinMM + NAudio.Core. -->
|
||||
<PackageReference Include="NAudio.WinMM" Version="2.3.0" />
|
||||
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
||||
</ItemGroup>
|
||||
@@ -50,16 +55,26 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Embedded resources: Hellion font (Exo 2, OFL-1.1) + manifest resource -->
|
||||
<!-- Embedded resources: bundled UI font (Inter Light, OFL-1.1) + manifest resource -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\HellionFont.ttf">
|
||||
<LogicalName>HellionFont.ttf</LogicalName>
|
||||
<EmbeddedResource Include="Resources\Inter-Light.ttf">
|
||||
<LogicalName>Inter-Light.ttf</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
||||
<EmbeddedResource Include="Resources\Inter-OFL.txt">
|
||||
<LogicalName>Inter-OFL.txt</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-banner.txt">
|
||||
<LogicalName>HellionChat.Branding.fox-banner.txt</LogicalName>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-banner.png">
|
||||
<LogicalName>HellionChat.Branding.fox-banner.png</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<!-- Bundled custom notification sounds, Mono 44.1 kHz 16-bit PCM WAV (Wine-safe) -->
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-1.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-1.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-2.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-2.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Sounds\notification-3.wav">
|
||||
<LogicalName>HellionChat.Sounds.notification-3.wav</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Branding\fox-mini.txt">
|
||||
<LogicalName>HellionChat.Branding.fox-mini.txt</LogicalName>
|
||||
|
||||
+21
-183
@@ -1,25 +1,11 @@
|
||||
name: Hellion Chat
|
||||
author: Jon Kazama (Hellion Forge)
|
||||
punchline: A Hellion Forge plugin — privacy-focused chat replacement for FFXIV, built for EU, US and JP data rules.
|
||||
punchline: Chat replacement for FFXIV that keeps your conversations and forgets the rest.
|
||||
description: |-
|
||||
Chat replacement for FINAL FANTASY XIV with privacy controls built around
|
||||
EU, US and JP data-protection rules.
|
||||
A chat window that stores your own conversations and nothing else. Public chat, NPC dialogue, system messages and battle logs stay out of the database until you switch them on.
|
||||
|
||||
By default only your own conversations are stored. Public chat, NPC
|
||||
dialogue and system messages stay out of the database unless you opt in.
|
||||
Retention windows are configurable per channel, history can be wiped
|
||||
retroactively, and everything can be exported on demand.
|
||||
Retention per channel, retroactive wipe, export to Markdown, JSON or CSV. Setup wizard with four profiles. 25 languages. Its own config and database.
|
||||
|
||||
Features:
|
||||
- Channel whitelist with a Privacy-First default
|
||||
- Per-channel retention with a daily background sweep
|
||||
- Retroactive cleanup (Ctrl+Shift confirm)
|
||||
- Export to Markdown, JSON or CSV
|
||||
- First-run wizard with three preset profiles
|
||||
- Bilingual UI (EN/DE) with live language switching
|
||||
- Own config and database — no shared state with other plugins
|
||||
|
||||
Based on Chat 2 by Infi and Anna (EUPL-1.2).
|
||||
Support: https://discord.gg/X9V7Kcv5gR
|
||||
repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat
|
||||
accepts_feedback: true
|
||||
@@ -27,7 +13,7 @@ icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/br
|
||||
image_urls:
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/firstRunWizard.png
|
||||
tags:
|
||||
- Social
|
||||
- UI
|
||||
@@ -35,184 +21,36 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
|
||||
**v2.0.4 — Housekeeping (2026-08-20)**
|
||||
|
||||
Hybrid FontManager refactor plus an embedded provenance mark.
|
||||
Nothing changes in how the plugin behaves. Two pieces of tidying that were overdue.
|
||||
|
||||
What changes under the hood:
|
||||
|
||||
- FontManager handle creation moves into the ctor inside a single
|
||||
atlas.SuppressAutoRebuild() block. The font atlas now builds once
|
||||
per plugin load instead of four to five times — less CPU and GPU
|
||||
pressure in the first seconds after a reload, less atlas texture
|
||||
memory churn.
|
||||
- Hybrid property model: Axis, AxisItalic and FontAwesome become
|
||||
init-only handles. RegularFont and ItalicFont stay mutable because
|
||||
the eight font settings still need to replace them at runtime —
|
||||
that path is funnelled through RebuildDelegateFonts() now and
|
||||
runs without a plugin reload.
|
||||
- FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle
|
||||
instead of building its own atlas slot. One delegate-build step
|
||||
less in the ctor.
|
||||
- BuildFontsAsync and BuildFonts are removed; the live mutation
|
||||
path is RebuildDelegateFonts() now.
|
||||
- Two FontManager self-test steps registered with /xlperf: ctor
|
||||
smoke (every handle non-null after Phase-1 resolve, no atlas
|
||||
load-exception) and push smoke (Push() returns without throwing).
|
||||
|
||||
Honorific full-gradient port (originally the v1.5.1 main item) was
|
||||
dropped: Honorific 3.2 exposes no IPC for the rendered gradient
|
||||
frame, and an in-plugin port of the colour palette was declined.
|
||||
The integration stays at the v1.4.7 glow-only shape.
|
||||
|
||||
User-visible:
|
||||
|
||||
- Hellion Forge signature: a small fox-head ASCII silhouette is
|
||||
emitted to /xllog on every plugin load, and a full fox banner
|
||||
with "Hellion Forge" set inside the body is available as a
|
||||
folded TreeNode in the First-Run Wizard and Settings ->
|
||||
Information tab. Drawn by Julia Moon, embedded in the plugin DLL.
|
||||
- No settings changes, no migration. v17 stays.
|
||||
|
||||
Note on performance: the cross-plugin baseline target from v1.5.0
|
||||
(matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did
|
||||
not land this cycle. HITCH stays around 80 ms because the cost is
|
||||
in the UiBuilder first-frame render path, not in the atlas build
|
||||
(which this cycle did reduce from 4-5 builds per load to 1). A
|
||||
first-frame render investigation is reserved for a later cycle.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
- **The installer description was long, and one line of it was wrong.** It ran to 908 characters and opened by naming a category instead of saying what the plugin does. It also claimed 24 interface languages when there are 25, and the two manifests carried different one-line summaries, so the store listing and the plugin details showed different sentences.
|
||||
- **30 translation keys for the webinterface are gone.** That feature, its HTTP routes and its frontend left the code in May; the strings stayed behind in all 25 languages, along with a generated property for each. Every key was checked against the whole repository before anything was removed.
|
||||
|
||||
---
|
||||
|
||||
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
|
||||
**v2.0.3 — Pop-out context menu, screenshot mode (2026-08-20)**
|
||||
|
||||
Major architecture cycle. The plugin bootstrap moves to a
|
||||
generic-host DI container (Microsoft.Extensions.Hosting +
|
||||
IServiceCollection) modelled on Lightless Sync. Service logging
|
||||
moves from a static Plugin.LogProxy locator to typed
|
||||
Microsoft.Extensions.Logging.ILogger<T> via constructor injection,
|
||||
bridged over Dalamud's IPluginLog by a custom DalamudLogger trio.
|
||||
|
||||
What changes under the hood:
|
||||
|
||||
- 18 instance-class services migrate to ILogger<T> via constructor
|
||||
injection across four slices: data layer (MessageStore,
|
||||
MessageManager, AutoTellTabsService), IPC and integrations
|
||||
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
|
||||
GameFunctions classes), UI window layer (ChatLogWindow,
|
||||
DbViewer, Popout, three settings tabs), and root (Commands,
|
||||
ThemeRegistry, PayloadHandler).
|
||||
- Plugin.LogProxy stays in place for the eight buckets ctor
|
||||
injection cannot reach: static helpers (EmoteCache,
|
||||
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected
|
||||
types (Configuration), the Message data class, and instance
|
||||
classes that only log from static methods (FontManager, one
|
||||
GameFunctions site).
|
||||
- Plugin.cs finishes at 1012 lines — virtually identical to the
|
||||
pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge
|
||||
wiring trade out exactly the service and window allocations
|
||||
that previously lived in LoadAsync.
|
||||
- Cross-plugin baseline confirms no performance penalty against
|
||||
Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2
|
||||
74 ms median. Lightless and XIVInstantMessenger sit around
|
||||
7 ms by deferring their font-atlas build past Finished
|
||||
loading — that pattern is the v1.5.1 follow-up.
|
||||
|
||||
User-visible:
|
||||
|
||||
- Slash-command insert fix: pasting a slash command into the
|
||||
chat input (Friend List "/tell" action, plugin-driven inserts
|
||||
from Artisan, AllaganTools etc.) now replaces the existing
|
||||
input instead of concatenating. Cherry-picked from ChatTwo
|
||||
upstream ee7768ac with namespace adaptation.
|
||||
|
||||
Migration v17 stays (no schema bump).
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
- **The context menu never opened in a pop-out window.** Right-clicking a name or an item there did nothing at all. Every chat surface shares one popup state, and whichever window drew first cleared the menu it had not opened before the pop-out got its turn. The menu now stays with the window the click came from.
|
||||
- **Screenshot mode could not be turned off.** It is a saved setting, but neither of its two toggles wrote the change to the config file, so switching it off never survived a reload and the mode came back on at every plugin load. Both toggles save now. If it is stuck on for you, one click off is enough.
|
||||
- **Text the game colours itself could render invisible.** Some of those colours arrive without an alpha byte, and the byte order swap then left them fully transparent.
|
||||
|
||||
---
|
||||
|
||||
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
|
||||
**v2.0.2 — Emotes out, placeholders fixed (2026-08-19)**
|
||||
|
||||
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
|
||||
Symbol picker for the chat input, a tell-history reload fix for
|
||||
users with many active partners, and a closing cleanup sweep
|
||||
before v1.5.0 picks up the DI-container adoption.
|
||||
|
||||
- Symbol picker: a small smile-icon button left of the channel
|
||||
indicator opens a popup with two tabs. The first lists all 161
|
||||
FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second
|
||||
carries 97 server-verified BMP symbols (latin marks, currency,
|
||||
the full Greek alphabet, geometric shapes, suits, notes) —
|
||||
every one of them round-tripped through /echo and /say in a
|
||||
four-round probe so the in-channel render matches what the
|
||||
picker shows. Click drops the glyph at the caret, multi-insert
|
||||
keeps the popup open, and a recent-used strip floats the last
|
||||
sixteen picks across both tabs. Toggle in Settings → Chat →
|
||||
Message behaviour, default on.
|
||||
- Pinned auto-tell tabs reload their full history again: a
|
||||
hidden 500-row scan cap in PreloadHistory used to override the
|
||||
user-configurable AutoTellTabsHistoryPreload setting, so
|
||||
less-frequent pinned partners (rare /tell sessions in an
|
||||
otherwise busy week) lost their backlog. The cap is removed;
|
||||
the (Receiver, Date) index keeps SQL fast, the client-side
|
||||
loop still respects your setting as the upper bound.
|
||||
- Slash-command teardown: /hellion, /hellionView,
|
||||
/hellionDebugger (and #if DEBUG /hellionSeString) wrappers are
|
||||
now cached as private fields. Plugin teardown detaches the
|
||||
live registration instead of re-Register'ing with identical
|
||||
args — closes a latent maintenance hazard from v1.4.9.
|
||||
- v1.4.x polish-sweep wraps up here. The ImGuiListClipper render
|
||||
refactor that was on the v1.4.10 reserve list got dropped
|
||||
after cross-platform smoke showed the scroll rubber-band is a
|
||||
Wine / Linux render-pipeline quirk, not universal — Windows
|
||||
users never saw it. It will get its own platform-targeted
|
||||
spike in a later patch. Next major cycle is v1.5.0 with the
|
||||
DI-container adoption (Microsoft.Extensions.Hosting +
|
||||
ILogger<T>) modelled on Lightless.
|
||||
- Migration v17 stays (no schema bump).
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
- **BetterTTV emote support is gone.** Its shared-emote endpoint went behind authentication, and that was where nearly all of them came from — what remained was a 65-entry global set, eleven of which are on the known-broken list, so 54 mostly static images from Twitch's early days. Exactly one was animated, and that one wanted 492 frames and 37 MB of video memory. The plugin now makes no outbound network calls at all. Messages stored with emotes still read fine; they show the code that was typed.
|
||||
- **Five settings descriptions printed `{0}` instead of the plugin name.** All 25 languages were affected, English included — it just hid better there, since "Hide {0} during cutscenes" still scans as a sentence while German puts the placeholder first. A test now walks every resource string with a placeholder and fails if one reaches a widget unformatted.
|
||||
- **New preview images.** The ones in the plugin installer were from 8 May and showed the interface as it looked before any of the window rebuilds.
|
||||
|
||||
---
|
||||
|
||||
**v1.4.9 — Plugin-Load Render Polish (2026-05-15)**
|
||||
**v2.0.1 — Hotfix (2026-08-19)**
|
||||
|
||||
Tenth sub-patch of the v1.4.x polish-sweep series. First-frame
|
||||
render cost drops from ~127 ms median to ~76 ms median,
|
||||
comfortably under Dalamud's 100 ms HITCH warning threshold.
|
||||
A same-day follow-up to 2.0.0 with no user-facing changes. Install it if you picked up 2.0.0 in the first hour; there is nothing new to look at, it just carries a dependency update the 2.0.0 archive was built without.
|
||||
|
||||
- First-frame defer: six non-essential rendering sections inside
|
||||
ChatLogWindow skip their first Draw and run one frame later
|
||||
(bottom status bar, channel-name SeString chunks, window bounds
|
||||
check, v0.6.1 hint banner, autocomplete, input-preview
|
||||
calculation). User-visible delay is ~17 ms at 60 fps, hidden
|
||||
inside the post-reload font-atlas build window.
|
||||
- Slash-command centralisation: /hellion, /hellionView,
|
||||
/hellionSeString and /hellionDebugger are registered in
|
||||
LoadAsync instead of inside the corresponding window
|
||||
constructors. The plugin-manager Open and configuration buttons
|
||||
hang on the same path.
|
||||
- Plugin-load profiling logs stay on at Information level
|
||||
(MessageStore connect/migrate, FilterAllTabs, auto-translate
|
||||
warmup) as a regression tripwire — a future load past 100 ms
|
||||
will show up in /xllog without a Debug filter.
|
||||
- ChatTwo IPC compatibility layer: HellionChat now mirrors
|
||||
ChatTwo's full IPC surface (GetChatInputState,
|
||||
ChatInputStateChanged, Register, Unregister, Available,
|
||||
Invoke) under the ChatTwo.* namespace in addition to our
|
||||
existing HellionChat.* provider gates. Third-party
|
||||
integrations that historically only subscribe to ChatTwo's
|
||||
IPC — for example Artisan's and AllaganTools' context-menu
|
||||
hooks — keep working without requiring a code change on their
|
||||
side. Conflict detection prevents ChatTwo from loading in
|
||||
parallel with HellionChat, so there is no slot-collision risk
|
||||
at runtime.
|
||||
- Migration v17 stays (no schema bump).
|
||||
- MessagePack raised from 3.1.4 to 3.1.7. It handles the payload serialisation behind the message database. The advisories are a recursion-depth limit in `Skip` and a fault in LZ4 decompression, both reachable only through crafted input — this plugin writes and reads its own bytes in a local file, so the practical exposure needs someone who already has write access to it. Lifted anyway, because it costs nothing.
|
||||
- The release workflow publishes through the Gitea API directly. The 2.0.0 build succeeded and then failed to attach its own archive, which is why that release had to be completed by hand.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Plugin;
|
||||
using HellionChat.Integrations;
|
||||
using HellionChat.Ipc;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Ui.Components;
|
||||
using HellionChat.Ui.Windows;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace HellionChat.Infrastructure.Hosting;
|
||||
@@ -11,16 +16,26 @@ namespace HellionChat.Infrastructure.Hosting;
|
||||
// at Build, which runs the service ctor (IPC subscribe etc.) right then
|
||||
// instead of lazily on first GetRequiredService.
|
||||
|
||||
internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : IHostedService
|
||||
internal sealed class ThemeRegistryInitHostedService(
|
||||
ThemeRegistry registry,
|
||||
FontManager fontManager
|
||||
) : IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
|
||||
// warm cache; otherwise the first Switch falls through to the built-in
|
||||
// default when Config.Theme points at a custom slug.
|
||||
foreach (var _ in registry.AllCustom()) { }
|
||||
registry.Switch(Plugin.Config.Theme);
|
||||
return Task.CompletedTask;
|
||||
registry.SwitchSilent(Plugin.Config.Theme);
|
||||
|
||||
// Point font sizes at the active theme's typography, wire future
|
||||
// theme switches to the atlas rebuild, and apply the boot theme's override.
|
||||
fontManager.SetTypographySource(() => registry.Active.Typography);
|
||||
registry.SetActiveChangedCallback(() => fontManager.RebuildDelegateFontsIfChanged());
|
||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
fontManager.RebuildDelegateFontsIfChanged()
|
||||
);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
@@ -85,3 +100,109 @@ internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService s
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal sealed class TellRouterServiceInitHostedService(Services.TellRouterService service)
|
||||
: IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
service.Initialize();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Eager-resolve trigger: resolving FailedTellNotifier in this adapter's ctor
|
||||
// enables its game hook during host startup. StartAsync itself is a no-op.
|
||||
internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier notifier)
|
||||
: IHostedService
|
||||
{
|
||||
// No-op adapter: the ctor dependency above is the actual eager-resolve
|
||||
// trigger. Field kept to match the IpcManager/TypingIpc/ExtraChat no-op
|
||||
// adapters and to avoid the CS9113 unread-parameter warning.
|
||||
private readonly FailedTellNotifier _notifier = notifier;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal sealed class PayloadHandlerInitHostedService(
|
||||
PayloadHandler payloadHandler,
|
||||
MessageList messageList
|
||||
) : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Cycle resolution: both singletons exist by the time HostedServices
|
||||
// run, so this is the first safe point to wire the setter.
|
||||
messageList.AttachPayloadHandler(payloadHandler);
|
||||
|
||||
// IAddonLifecycle thread-affinity is not explicitly documented; wrap is
|
||||
// defensive insurance — mirrors the window-registration RunOnFrameworkThread
|
||||
// pattern established in PluginLifecycle.cs.
|
||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
Plugin.AddonLifecycle.RegisterListener(
|
||||
AddonEvent.PostUpdate,
|
||||
"ItemDetail",
|
||||
payloadHandler.MoveTooltip
|
||||
);
|
||||
Plugin.AddonLifecycle.RegisterListener(
|
||||
AddonEvent.PostUpdate,
|
||||
"ActionDetail",
|
||||
payloadHandler.MoveTooltip
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
// Single call using the params-overload removes the delegate from all addons it was registered for (ItemDetail + ActionDetail both cleaned in one shot).
|
||||
Plugin.AddonLifecycle.UnregisterListener(payloadHandler.MoveTooltip);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wires MainWindow into CommandHelpWindow post-container-build. CommandHelpWindow
|
||||
// cannot take MainWindow as a ctor-param because that would close the cycle
|
||||
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
|
||||
// it through FactoryCallSite registrations and the resolve recurses silently).
|
||||
// Both singletons exist by host.StartAsync time, so this is the first safe point
|
||||
// to wire the setter — same setter-injection pattern as MessageList.AttachPayloadHandler.
|
||||
internal sealed class CommandHelpWindowInitHostedService(
|
||||
CommandHelpWindow commandHelpWindow,
|
||||
MainWindow mainWindow
|
||||
) : IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
commandHelpWindow.AttachMainWindow(mainWindow);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Attaches the singleton PayloadHandler to every pre-allocated pop-out
|
||||
// window's MessageList post-container-build. Pool/window cannot take the
|
||||
// PayloadHandler via ctor (that would close the silent FactoryCallSite cycle —
|
||||
// same setter-injection reason as MessageList.AttachPayloadHandler / CommandHelpWindow.
|
||||
// AttachMainWindow). Both singletons exist by host.StartAsync time.
|
||||
internal sealed class ChannelPopoutInitHostedService(
|
||||
ChannelPopoutPool pool,
|
||||
PayloadHandler payloadHandler
|
||||
) : IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var window in pool.Instances)
|
||||
window.AttachPayloadHandler(payloadHandler);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// Plays the three bundled WAV notification sounds via NAudio WaveOutEvent.
|
||||
// WaveOutEvent/WinMM is the correct backend for FFXIV on Wine: it works
|
||||
// without Media Foundation (which Wine does not support for MP3/AAC).
|
||||
//
|
||||
// Playback volume comes from Configuration.CustomSoundVolume via the Play
|
||||
// parameter, clamped to [0,1]. The 16 game sounds are unaffected — they go
|
||||
// through UIGlobals.PlaySoundEffect, which the plugin cannot scale.
|
||||
internal sealed class CustomAudioPlayer : IDisposable
|
||||
{
|
||||
// Sound bytes are read once at construction so each Play() wraps a fresh
|
||||
// MemoryStream rather than re-reading the manifest stream (which becomes
|
||||
// unreadable after the first read and would require Seek support).
|
||||
private readonly byte[][] _soundData;
|
||||
private readonly ILogger<CustomAudioPlayer> _logger;
|
||||
|
||||
private WaveOutEvent? _outputDevice;
|
||||
private WaveFileReader? _reader;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public CustomAudioPlayer(ILogger<CustomAudioPlayer> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_soundData = new byte[3][];
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var resourceName = $"HellionChat.Sounds.notification-{i + 1}.wav";
|
||||
using var stream = typeof(CustomAudioPlayer).Assembly.GetManifestResourceStream(
|
||||
resourceName
|
||||
);
|
||||
if (stream is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Embedded sound resource not found: {Resource}. "
|
||||
+ "Custom sound {Index} will be silent.",
|
||||
resourceName,
|
||||
i + 1
|
||||
);
|
||||
_soundData[i] = Array.Empty<byte>();
|
||||
continue;
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
_soundData[i] = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// customIndex is 1, 2, or 3, matching the sound file suffix.
|
||||
// Stops any currently playing sound before starting the new one.
|
||||
// NAudio playback runs on its own thread; this method returns immediately.
|
||||
public void Play(int customIndex, float volume)
|
||||
{
|
||||
if (customIndex < 1 || customIndex > 3)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"CustomAudioPlayer.Play called with out-of-range index {Index}",
|
||||
customIndex
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = _soundData[customIndex - 1];
|
||||
if (data.Length == 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sound data for index {Index} is empty; skipping playback",
|
||||
customIndex
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
StopCurrent();
|
||||
|
||||
var ms = new MemoryStream(data, writable: false);
|
||||
_reader = new WaveFileReader(ms);
|
||||
|
||||
_outputDevice = new WaveOutEvent();
|
||||
// Init opens the device and creates the WinMM handle. Volume
|
||||
// must be set after Init, otherwise waveOutSetVolume fails with
|
||||
// InvalidHandle.
|
||||
_outputDevice.Init(_reader);
|
||||
// AUDIO-1: volume comes from Configuration.CustomSoundVolume.
|
||||
// Clamp here too — a hand-edited config could carry an
|
||||
// out-of-range value, and WaveOutEvent.Volume rejects those.
|
||||
_outputDevice.Volume = Math.Clamp(volume, 0f, 1f);
|
||||
_outputDevice.Play();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to play custom notification sound {Index}",
|
||||
customIndex
|
||||
);
|
||||
StopCurrent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stops and tears down the active WaveOutEvent + WaveFileReader without
|
||||
// throwing. Called on Play (to interrupt previous sound) and from Dispose.
|
||||
// Guards Stop() with a PlaybackState check because waveOutReset blocks even
|
||||
// when playback already finished; under Wine this can stall the WinMM
|
||||
// callback thread if many sounds arrive in quick succession.
|
||||
private void StopCurrent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_outputDevice?.PlaybackState == PlaybackState.Playing)
|
||||
_outputDevice.Stop();
|
||||
_outputDevice?.Dispose();
|
||||
_outputDevice = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Exception while stopping current WaveOutEvent");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_reader?.Dispose();
|
||||
_reader = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Exception while disposing WaveFileReader");
|
||||
}
|
||||
}
|
||||
|
||||
// At plugin unload the PendingMessageThread is already cancelled and the
|
||||
// draw loop is gone, so _lock is uncontended here. Calling StopCurrent
|
||||
// outside the lock avoids holding it across the blocking waveOutReset /
|
||||
// WaveOutEvent.Dispose, which can freeze on Wine during unload.
|
||||
public void Dispose()
|
||||
{
|
||||
StopCurrent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// A minimal, failed-tell-specific game hook. A locale-robust "tell failed"
|
||||
// signal is not reachable over the processed message stream (Message carries
|
||||
// no LogMessage row id, ChatCode 60 is too broad). This hooks the one
|
||||
// ShowLogMessageString overload and toasts on a pinned id set. It is NOT the
|
||||
// broad ad-block hook layer.
|
||||
internal sealed class FailedTellNotifier : IDisposable
|
||||
{
|
||||
private readonly ILogger<FailedTellNotifier> _logger;
|
||||
private readonly Hook<RaptureLogModule.Delegates.ShowLogMessageString>? _hook;
|
||||
|
||||
public unsafe FailedTellNotifier(ILogger<FailedTellNotifier> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Creating/enabling a hook is safe off the framework thread (the
|
||||
// ctor runs during host startup on the framework thread,
|
||||
// eager-resolved via FailedTellNotifierInitHostedService).
|
||||
_hook =
|
||||
Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.ShowLogMessageString>(
|
||||
RaptureLogModule.MemberFunctionPointers.ShowLogMessageString,
|
||||
ShowLogMessageStringDetour
|
||||
);
|
||||
_hook.Enable();
|
||||
}
|
||||
|
||||
private unsafe void ShowLogMessageStringDetour(
|
||||
RaptureLogModule* module,
|
||||
uint logMessageId,
|
||||
Utf8String* value
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (
|
||||
FailedTellMatcher.ShouldNotify(
|
||||
logMessageId,
|
||||
Plugin.Config.NotifyFailedTell,
|
||||
FailedTellMatcher.FailedTellLogMessageIds
|
||||
)
|
||||
)
|
||||
{
|
||||
var recipient = value is null ? string.Empty : value->ToString();
|
||||
var content = string.IsNullOrEmpty(recipient)
|
||||
? HellionStrings.FailedTell_Notification_Generic
|
||||
: string.Format(HellionStrings.FailedTell_Notification_Named, recipient);
|
||||
WrapperUtil.AddNotification(content, NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "FailedTellNotifier detour threw");
|
||||
}
|
||||
|
||||
_hook!.Original(module, logMessageId, value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_hook?.Disable();
|
||||
_hook?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -195,4 +195,23 @@ internal sealed class HonorificService : IDisposable
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test seam: the three status fields are private-set and IPC-driven, which a
|
||||
// headless /xlperf run can't reach (Honorific is usually absent in tests).
|
||||
// Callers MUST snapshot the prior values and restore them in CleanUp, and
|
||||
// MUST drive Set -> Draw -> Assert within ONE synchronous RunStep (never
|
||||
// Waiting between Set and Assert) — a between-frame OnReady/OnTitleChanged
|
||||
// would otherwise clobber this state and a CleanUp restore can't un-corrupt a
|
||||
// mid-flight assertion. (A FontsReady precondition gate returning Waiting
|
||||
// BEFORE the snapshot/Set is fine — nothing is mutated yet.)
|
||||
internal void TestOnly_SetState(
|
||||
bool isAvailable,
|
||||
(uint Major, uint Minor)? detectedApiVersion,
|
||||
HonorificTitleData? title
|
||||
)
|
||||
{
|
||||
IsAvailable = isAvailable;
|
||||
DetectedApiVersion = detectedApiVersion;
|
||||
CurrentTitle = title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
internal enum HonorificStatusKind
|
||||
{
|
||||
NotInstalled,
|
||||
Incompatible,
|
||||
Detected,
|
||||
}
|
||||
|
||||
internal static class HonorificStatus
|
||||
{
|
||||
// Mirrors the 1.5.6 three-state discriminator (1d3b429:About.cs:171/183/196):
|
||||
// it keys on IsAvailable + the *nullability* of DetectedApiVersion, never a
|
||||
// recomputed major check. IsAvailable already encodes the compatibility
|
||||
// result HonorificService set during the initial pull. Null-safe: an
|
||||
// (isAvailable=true, detectedApiVersion=null) state a test seam can produce
|
||||
// resolves to NotInstalled rather than dereferencing null.
|
||||
internal static HonorificStatusKind Resolve(
|
||||
bool isAvailable,
|
||||
(uint Major, uint Minor)? detectedApiVersion
|
||||
)
|
||||
{
|
||||
if (isAvailable && detectedApiVersion is not null)
|
||||
return HonorificStatusKind.Detected;
|
||||
if (detectedApiVersion is not null)
|
||||
return HonorificStatusKind.Incompatible;
|
||||
return HonorificStatusKind.NotInstalled;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,10 @@ namespace HellionChat.Integrations;
|
||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||
// so HellionChat loads cleanly when Honorific is absent.
|
||||
//
|
||||
// Only Glow is rendered. Color3, GradientColourSet and GradientAnimationStyle
|
||||
// are parsed but unused — the animated gradient lives entirely inside Honorific
|
||||
// and is not exposed over IPC, so reproducing it here would mean shipping our
|
||||
// own copy of Honorific's colour palette. The fields stay in the DTO so the
|
||||
// JSON roundtrip remains lossless.
|
||||
// Color is rendered in the header title slot (HonorificHeader). Glow, Color3,
|
||||
// GradientColourSet and GradientAnimationStyle are parsed but not rendered —
|
||||
// the animated gradient lives inside Honorific and is not exposed over IPC.
|
||||
// The fields stay in the DTO so the JSON roundtrip remains lossless.
|
||||
internal sealed record HonorificTitleData(
|
||||
string? Title,
|
||||
bool IsPrefix,
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ExtraChat : IDisposable
|
||||
|
||||
// volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these.
|
||||
// Reference assignment is atomic on x64, but the barrier ensures visibility
|
||||
// across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01].
|
||||
// across threads (especially Mono/Wine). Raised in the 2026-05-05 audit.
|
||||
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours =>
|
||||
ChannelCommandColoursInternal;
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
|
||||
|
||||
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins
|
||||
// v1.4.9: ChatTwo IPC compatibility mirror. Some third-party plugins
|
||||
// have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
|
||||
// gates. HellionChat replaces ChatTwo (conflict detection prevents
|
||||
// parallel loading), so mirroring the ChatTwo provider slots lets those
|
||||
@@ -34,11 +34,13 @@ internal sealed class TypingIpc : IDisposable
|
||||
private ChatInputState LastState;
|
||||
private bool HasState;
|
||||
|
||||
private readonly Ui.Components.InputBar _inputBar;
|
||||
private readonly ILogger<TypingIpc> _logger;
|
||||
|
||||
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
|
||||
internal TypingIpc(Plugin plugin, Ui.Components.InputBar inputBar, ILogger<TypingIpc> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
_inputBar = inputBar;
|
||||
_logger = logger;
|
||||
|
||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||
@@ -48,7 +50,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
"HellionChat.ChatInputStateChanged"
|
||||
);
|
||||
|
||||
// v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment).
|
||||
// v1.4.9: ChatTwo-prefixed compatibility slots (see class-level comment).
|
||||
ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||
"ChatTwo.GetChatInputState"
|
||||
);
|
||||
@@ -62,25 +64,34 @@ internal sealed class TypingIpc : IDisposable
|
||||
|
||||
private ChatInputState BuildState()
|
||||
{
|
||||
var log = Plugin.ChatLogWindow;
|
||||
|
||||
var usedChannel = Plugin.CurrentTab.CurrentChannel;
|
||||
var inputChannel = usedChannel.UseTempChannel
|
||||
? usedChannel.TempChannel
|
||||
: usedChannel.Channel;
|
||||
var channelType = inputChannel.ToChatType();
|
||||
|
||||
// MainWindow is Phase-1-resolved and never reassigned;
|
||||
// the `?.` is defense-in-depth for pre-Phase-1 IPC-pulls.
|
||||
var mainWindowOpen = Plugin.MainWindow?.IsOpen ?? false;
|
||||
|
||||
// Stale-state guard: InputBar's focus and pending-buffer fields are
|
||||
// only written by DrawInputField. Closing MainWindow freezes them, so
|
||||
// gate all four state fields on mainWindowOpen.
|
||||
var inputFocused = mainWindowOpen && _inputBar.IsFocused;
|
||||
var hasText = mainWindowOpen && _inputBar.PendingLength > 0;
|
||||
var textLength = mainWindowOpen ? _inputBar.PendingLength : 0;
|
||||
|
||||
return (
|
||||
InputVisible: !log.IsHidden,
|
||||
log.InputFocused,
|
||||
HasText: log.Chat.Length > 0,
|
||||
IsTyping: log is { InputFocused: true, Chat.Length: > 0 },
|
||||
TextLength: log.Chat.Length,
|
||||
InputVisible: mainWindowOpen,
|
||||
InputFocused: inputFocused,
|
||||
HasText: hasText,
|
||||
IsTyping: hasText,
|
||||
TextLength: textLength,
|
||||
ChannelType: channelType
|
||||
);
|
||||
}
|
||||
|
||||
private ChatInputState GetState() => BuildState();
|
||||
internal ChatInputState GetState() => BuildState();
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
@@ -91,7 +102,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
HasState = true;
|
||||
LastState = state;
|
||||
StateChangedGate.SendMessage(state);
|
||||
// v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||
// v1.4.9: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||
ChatTwoStateChangedGate.SendMessage(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class IpcManager : IDisposable
|
||||
object?
|
||||
> InvokeGate { get; }
|
||||
|
||||
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with
|
||||
// v1.4.9: ChatTwo IPC compatibility mirror. Third-party plugins with
|
||||
// a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
|
||||
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
|
||||
// provider slots under the ChatTwo namespace lets those plugins keep
|
||||
@@ -65,7 +65,7 @@ internal sealed class IpcManager : IDisposable
|
||||
object?
|
||||
>("HellionChat.Invoke");
|
||||
|
||||
// v1.4.9 R4: ChatTwo-prefixed mirrors of the four context-menu slots
|
||||
// v1.4.9: ChatTwo-prefixed mirrors of the four context-menu slots
|
||||
// above. Share the same Register/Unregister backing methods so a
|
||||
// plugin that subscribes via either namespace lands in the same
|
||||
// Registered list. SendMessage on Invoke fans out to both gates.
|
||||
@@ -103,7 +103,7 @@ internal sealed class IpcManager : IDisposable
|
||||
)
|
||||
{
|
||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||
// v1.4.9 R4: fan out the same event to plugins listening on ChatTwo.Invoke.
|
||||
// v1.4.9: fan out the same event to plugins listening on ChatTwo.Invoke.
|
||||
ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||
}
|
||||
|
||||
|
||||
@@ -175,8 +175,6 @@ public partial class Message
|
||||
}
|
||||
|
||||
var nextIsAutoTranslate = false;
|
||||
var checkForEmotes =
|
||||
(Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
|
||||
foreach (var chunk in oldChunks)
|
||||
{
|
||||
// Use as is if it's not a text chunk, it already has a payload, or is auto translate
|
||||
@@ -207,25 +205,6 @@ public partial class Message
|
||||
var wordUsed = false;
|
||||
var tokenUsed = false;
|
||||
|
||||
if (
|
||||
checkForEmotes
|
||||
&& EmoteCache.Exists(word)
|
||||
&& !Plugin.Config.BlockedEmotes.Contains(word)
|
||||
)
|
||||
{
|
||||
// Add the previous sentence before adding the emote
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.ToString()));
|
||||
AddChunkWithMessage(
|
||||
new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word)
|
||||
{
|
||||
FallbackColour = text.FallbackColour,
|
||||
}
|
||||
);
|
||||
|
||||
wordUsed = true;
|
||||
sentenceBuilder.Clear();
|
||||
}
|
||||
|
||||
if (token.TokenType == Tokenizer.TokenType.UrlString)
|
||||
{
|
||||
// Add the previous sentence before adding the url
|
||||
|
||||
+165
-21
@@ -7,7 +7,9 @@ using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
@@ -161,8 +163,15 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
internal void ClearAllTabs()
|
||||
{
|
||||
// Snapshot the tab LIST under the shared lock so the worker-thread
|
||||
// add/remove can't tear the enumeration; tab.Clear() then runs lock-free
|
||||
// (each tab's Messages has its own SemaphoreSlim — lock order: list outer).
|
||||
List<Tab> tabsSnapshot;
|
||||
lock (Plugin.TabsListLock)
|
||||
tabsSnapshot = Plugin.Config.Tabs.ToList();
|
||||
|
||||
// TempTabs are session-only (not persisted); exclude them to preserve Tell history
|
||||
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
|
||||
foreach (var tab in tabsSnapshot.Where(t => !t.IsTempTab))
|
||||
tab.Clear();
|
||||
}
|
||||
|
||||
@@ -174,18 +183,19 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
|
||||
|
||||
// TempTabs are excluded; they maintain live state from AutoTellTabsService
|
||||
var pendingTabs = Plugin
|
||||
.Config.Tabs.Where(t => !t.IsTempTab)
|
||||
.Select(tab => (tab, new List<Message>()))
|
||||
.ToList();
|
||||
foreach (var message in messages)
|
||||
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
||||
pendingMessages.Add(message);
|
||||
// TempTabs excluded (live state from AutoTellTabsService). Bucket via the
|
||||
// pure MapMessagesToTabs so the assignment stays testable outside Dalamud.
|
||||
// Snapshot under the shared lock (list copy only — short critical
|
||||
// section). The Store query above and the AddSortPrune writes below stay
|
||||
// OUTSIDE the lock (lock order: list outer, MessageList inner).
|
||||
List<Tab> nonTempTabs;
|
||||
lock (Plugin.TabsListLock)
|
||||
nonTempTabs = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
|
||||
var buckets = MapMessagesToTabs(nonTempTabs, messages);
|
||||
|
||||
// Apply messages to chat log all at once.
|
||||
foreach (var (tab, pendingMessages) in pendingTabs)
|
||||
tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);
|
||||
foreach (var tab in nonTempTabs)
|
||||
tab.Messages.AddSortPrune(buckets[tab], MessageDisplayLimit);
|
||||
|
||||
if (!messages.DidError)
|
||||
return;
|
||||
@@ -204,6 +214,26 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Pure message->tab bucketing for the refilter. Dalamud-free + static so the
|
||||
// assignment can be unit-pinned in the build suite; the live caller owns the
|
||||
// Store query, the snapshot and the SemaphoreSlim writes.
|
||||
internal static Dictionary<Tab, List<Message>> MapMessagesToTabs(
|
||||
IReadOnlyList<Tab> tabs,
|
||||
IEnumerable<Message> messages
|
||||
)
|
||||
{
|
||||
var buckets = new Dictionary<Tab, List<Message>>(tabs.Count);
|
||||
foreach (var tab in tabs)
|
||||
buckets[tab] = new List<Message>();
|
||||
|
||||
foreach (var message in messages)
|
||||
foreach (var tab in tabs)
|
||||
if (tab.Matches(message))
|
||||
buckets[tab].Add(message);
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
internal void FilterAllTabsAsync()
|
||||
{
|
||||
Task.Run(() =>
|
||||
@@ -218,9 +248,8 @@ internal class MessageManager : IAsyncDisposable
|
||||
_logger.LogError(ex, "Error in FilterAllTabs");
|
||||
}
|
||||
|
||||
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
|
||||
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
||||
// regressions; remains in place after Sub-Task 3.4 Befund.
|
||||
// Information, not Debug, so the xllog tail surfaces this without a
|
||||
// filter. Kept as a guard against future plugin-load regressions.
|
||||
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
});
|
||||
}
|
||||
@@ -329,20 +358,135 @@ internal class MessageManager : IAsyncDisposable
|
||||
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
||||
Store.UpsertMessage(message);
|
||||
|
||||
var currentMatches = Plugin.CurrentTab.Matches(message);
|
||||
foreach (var tab in Plugin.Config.Tabs)
|
||||
{
|
||||
var unread = !(
|
||||
tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches
|
||||
);
|
||||
// Snapshot the list, not just the active tab. This loop runs on the worker
|
||||
// thread while SaveConfig's strip and the auto-tell spawn mutate Config.Tabs
|
||||
// under TabsListLock — enumerating it live throws "collection was modified",
|
||||
// and the catch in ProcessPendingMessages swallows that, silently dropping
|
||||
// the whole message: no tab entry, no sound, no MessageProcessed.
|
||||
List<Tab> tabsSnapshot;
|
||||
lock (Plugin.TabsListLock)
|
||||
tabsSnapshot = Plugin.Config.Tabs.ToList();
|
||||
|
||||
// Snapshot the active tab and whether it shows this message ONCE, so the
|
||||
// whole loop sees a consistent value (the getter is a cross-thread read of
|
||||
// MainWindow.ActiveTab).
|
||||
var currentTab = Plugin.CurrentTab;
|
||||
var currentTabMatches = currentTab.Matches(message);
|
||||
foreach (var tab in tabsSnapshot)
|
||||
{
|
||||
if (tab.Matches(message))
|
||||
tab.AddMessage(message, unread);
|
||||
tab.AddMessage(message, ShouldCountUnread(tab, currentTab, currentTabMatches));
|
||||
}
|
||||
|
||||
// Deliberate O(2n): the sound pick re-walks the tab list so the selection
|
||||
// stays pure and SelfTest-able; AddMessage above and playback below keep
|
||||
// the side effects.
|
||||
var notificationSound = SelectNotificationSound(
|
||||
tabsSnapshot,
|
||||
currentTab,
|
||||
message,
|
||||
Plugin.Config.PlaySounds,
|
||||
out var soundSource
|
||||
);
|
||||
|
||||
// The snapshot can outlive a tab (eviction, logout). Playing its sound would
|
||||
// be an audible artefact for a tab that is already gone, so re-check first.
|
||||
if (notificationSound is not null && soundSource is not null)
|
||||
{
|
||||
bool sourceStillPresent;
|
||||
lock (Plugin.TabsListLock)
|
||||
sourceStillPresent = Plugin.Config.Tabs.Contains(soundSource);
|
||||
if (!sourceStillPresent)
|
||||
notificationSound = null;
|
||||
}
|
||||
|
||||
if (notificationSound is { } soundId)
|
||||
{
|
||||
if (soundId is >= 1 and <= 16)
|
||||
{
|
||||
// ProcessMessage runs on the PendingMessageThread worker; the native
|
||||
// UIGlobals.PlaySoundEffect must be marshalled onto the framework
|
||||
// thread (reference_dalamud_framework_thread).
|
||||
Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UIGlobals.PlaySoundEffect(soundId);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (soundId >= 17)
|
||||
{
|
||||
// Custom bundled sounds (ids 17-19) go through NAudio WaveOutEvent.
|
||||
// NAudio manages its own playback thread, so no framework marshalling needed.
|
||||
Plugin.CustomAudioPlayer.Play((int)soundId - 16, Plugin.Config.CustomSoundVolume);
|
||||
}
|
||||
// soundId == 0 (hand-edited config) falls through: plays nothing.
|
||||
}
|
||||
|
||||
MessageProcessed?.Invoke(message);
|
||||
}
|
||||
|
||||
// Pure: picks the sound id for the first inactive tab that wants one, or null.
|
||||
// No AddMessage, no store write — those stay in the ProcessMessage loop so this
|
||||
// is exercisable from the SelfTest without polluting tab state. The "first
|
||||
// match wins" semantics live here via the running 'picked is null' guard,
|
||||
// keeping a message matching several background tabs from stacking sounds.
|
||||
// TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
|
||||
// Unseen ("count only what you haven't seen") suppresses unread on an inactive
|
||||
// tab when the active tab ALSO shows this message — you already saw it in the
|
||||
// tab you're looking at (1.5.6 / upstream ChatTwo behavior). The "active tab"
|
||||
// used to be pinned to Tabs[0], so this fired against the wrong one until
|
||||
// CurrentTab was recoupled to the real active tab, and currentTabMatches is
|
||||
// now measured against the tab you see. All -> always counts; None ->
|
||||
// counts here and is gated out at the display layer. Pure + SelfTest-able.
|
||||
internal static bool ShouldCountUnread(Tab tab, Tab currentTab, bool currentTabMatches) =>
|
||||
!(
|
||||
tab.UnreadMode == UnreadMode.Unseen
|
||||
&& !ReferenceEquals(currentTab, tab)
|
||||
&& currentTabMatches
|
||||
);
|
||||
|
||||
// Reports the tab the sound came from, so the caller can drop it if that tab
|
||||
// disappeared between snapshot and playback.
|
||||
internal static uint? SelectNotificationSound(
|
||||
IEnumerable<Tab> tabs,
|
||||
Tab currentTab,
|
||||
Message probe,
|
||||
bool playSounds,
|
||||
out Tab? source
|
||||
)
|
||||
{
|
||||
uint? picked = null;
|
||||
source = null;
|
||||
foreach (var tab in tabs)
|
||||
{
|
||||
if (!tab.Matches(probe))
|
||||
continue;
|
||||
if (
|
||||
picked is null
|
||||
&& TabSoundDecision.ShouldPlay(
|
||||
currentTab == tab,
|
||||
tab.EnableNotificationSound,
|
||||
playSounds
|
||||
)
|
||||
)
|
||||
{
|
||||
picked = tab.NotificationSoundId;
|
||||
source = tab;
|
||||
}
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
// SelfTest hook — same name discipline as InputBar.TestBuildOutgoingForSelfTest.
|
||||
internal static uint? TestSelectNotificationSoundForSelfTest(
|
||||
IEnumerable<Tab> tabs,
|
||||
Tab currentTab,
|
||||
Message probe,
|
||||
bool playSounds
|
||||
) => SelectNotificationSound(tabs, currentTab, probe, playSounds, out _);
|
||||
|
||||
internal class NameFormatting
|
||||
{
|
||||
internal string Before { get; private set; } = string.Empty;
|
||||
|
||||
+205
-57
@@ -90,6 +90,8 @@ public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
||||
return new PartyFinderPayload(reader.ReadUInt32());
|
||||
case PayloadMessagePackType.Uri:
|
||||
return new UriPayload(new Uri(reader.ReadString() ?? ""));
|
||||
// Read-only since 2.0.2: nothing writes this any more, but rows
|
||||
// stored before then carry it and must keep deserialising.
|
||||
case PayloadMessagePackType.Emote:
|
||||
return EmotePayload.ResolveEmote(reader.ReadString() ?? "");
|
||||
case PayloadMessagePackType.Other:
|
||||
@@ -245,7 +247,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private SqliteConnection Connect()
|
||||
{
|
||||
// v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired
|
||||
// v1.4.9 profiling: trace cost of SQLite open + pragma-apply. Paired
|
||||
// with the Migrate-Stopwatch below — Connect alone is the cheap half
|
||||
// (Open + a handful of PRAGMAs); the expensive half typically lives in
|
||||
// Migrate, especially on a large DB after a schema bump.
|
||||
@@ -260,7 +262,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
// v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a
|
||||
// v1.4.9 profiling: trace cost of the schema-migration chain. On a
|
||||
// large DB after a fresh schema bump this is the dominant SQLite cost
|
||||
// at plugin-load, not Connect.
|
||||
var migrateSw = System.Diagnostics.Stopwatch.StartNew();
|
||||
@@ -279,18 +281,25 @@ internal class MessageStore : IDisposable
|
||||
migrationsToDo.Add(Migrate2);
|
||||
migrationsToDo.Add(Migrate3);
|
||||
migrationsToDo.Add(Migrate4);
|
||||
migrationsToDo.Add(Migrate5);
|
||||
break;
|
||||
case 1:
|
||||
migrationsToDo.Add(Migrate2);
|
||||
migrationsToDo.Add(Migrate3);
|
||||
migrationsToDo.Add(Migrate4);
|
||||
migrationsToDo.Add(Migrate5);
|
||||
break;
|
||||
case 2:
|
||||
migrationsToDo.Add(Migrate3);
|
||||
migrationsToDo.Add(Migrate4);
|
||||
migrationsToDo.Add(Migrate5);
|
||||
break;
|
||||
case 3:
|
||||
migrationsToDo.Add(Migrate4);
|
||||
migrationsToDo.Add(Migrate5);
|
||||
break;
|
||||
case 4:
|
||||
migrationsToDo.Add(Migrate5);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -430,6 +439,23 @@ internal class MessageStore : IDisposable
|
||||
SetMigrationVersion(4);
|
||||
}
|
||||
|
||||
private void Migrate5()
|
||||
{
|
||||
_logger.LogInformation("Running migration 5: Add (Receiver, Date) index for tell history");
|
||||
|
||||
// GetTellHistoryWithSender filters on Receiver and orders by Date DESC.
|
||||
// Without a matching index SQLite sorts the whole receiver history into a
|
||||
// temp b-tree before returning row one, which defeats the early break in
|
||||
// the caller. (Receiver, ChatType, Date) does NOT help: the ChatType IN
|
||||
// filter sits between the equality prefix and the sort column.
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_receiver_date ON messages (Receiver, Date);";
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
SetMigrationVersion(5);
|
||||
}
|
||||
|
||||
private void SetMigrationVersion(int version)
|
||||
{
|
||||
_logger.LogInformation($"Setting version {version}");
|
||||
@@ -440,35 +466,60 @@ internal class MessageStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Drops the full-text index and marks it for a rebuild.
|
||||
//
|
||||
// messages_fts stores sender_text and content_text in the clear, and no
|
||||
// delete path touched it: ClearMessages, CleanupRetainOnly and the retention
|
||||
// sweep all removed rows from `messages` only. The plain text of every
|
||||
// "deleted" message stayed on disk.
|
||||
//
|
||||
// Worse, it was self-sealing. InitFtsReadyCache treats a non-empty index as
|
||||
// ready, so after a wipe the index stayed full, the readiness flag stayed
|
||||
// true, and the rebuild that would have cleared it never ran again.
|
||||
//
|
||||
// Wiping rather than deleting matched rows: message_guid is stored as a GUID
|
||||
// string while messages.Id is a BLOB, so the two cannot be joined in SQL.
|
||||
// The index is derived data and rebuilds from the surviving rows on the next
|
||||
// start, which is the cheap and provably complete option.
|
||||
//
|
||||
// Caller must already hold _readLock.
|
||||
private void InvalidateFtsIndex()
|
||||
{
|
||||
Connection.Execute("DELETE FROM messages_fts;");
|
||||
_ftsReady = false;
|
||||
}
|
||||
|
||||
internal void ClearMessages()
|
||||
{
|
||||
lock (_readLock)
|
||||
{
|
||||
Connection.Execute("DELETE FROM messages;");
|
||||
PerformMaintenance();
|
||||
InvalidateFtsIndex();
|
||||
TryPerformMaintenance();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a (ChatType, count) snapshot over non-deleted messages.
|
||||
// Used by the Privacy tab to preview retroactive cleanup impact.
|
||||
internal Dictionary<int, long> GetMessageCountsByChatType()
|
||||
//
|
||||
// Caller-owned connection, same reasoning as StreamForExport: this is a
|
||||
// GROUP BY over every row, and holding _readLock for it would stall
|
||||
// UpsertMessage on the framework thread for as long as the scan takes.
|
||||
internal Dictionary<int, long> GetMessageCountsByChatType(SqliteConnection conn)
|
||||
{
|
||||
lock (_readLock)
|
||||
var result = new Dictionary<int, long>();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;";
|
||||
cmd.CommandTimeout = 120;
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var result = new Dictionary<int, long>();
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;";
|
||||
cmd.CommandTimeout = 120;
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var chatType = reader.GetInt32(0);
|
||||
var count = reader.GetInt64(1);
|
||||
result[chatType] = count;
|
||||
}
|
||||
return result;
|
||||
var chatType = reader.GetInt32(0);
|
||||
var count = reader.GetInt64(1);
|
||||
result[chatType] = count;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Deletes messages older than the per-channel retention window, with a global
|
||||
@@ -505,6 +556,12 @@ internal class MessageStore : IDisposable
|
||||
var index = 0;
|
||||
foreach (var (type, days) in chatTypeDaysMap)
|
||||
{
|
||||
// Careful: 0 here is NOT the "keep forever" it means for
|
||||
// defaultDays below. A per-channel 0 puts the cutoff at now
|
||||
// and deletes the channel's entire history. No profile ships
|
||||
// a 0 and no UI can set one, which is why this is a comment
|
||||
// and not a guard -- but any editor added later has to
|
||||
// reconcile the two meanings before it exposes the value.
|
||||
var cutoff = nowMs - days * 86400000L;
|
||||
var typeParam = $"$type{index}";
|
||||
var cutoffParam = $"$cutoff{index}";
|
||||
@@ -538,7 +595,11 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
|
||||
if (deleted > 0)
|
||||
PerformMaintenance();
|
||||
{
|
||||
InvalidateFtsIndex();
|
||||
TryPerformMaintenance();
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
@@ -562,7 +623,51 @@ internal class MessageStore : IDisposable
|
||||
cmd.CommandTimeout = 600;
|
||||
deleted = cmd.ExecuteNonQuery();
|
||||
}
|
||||
PerformMaintenance();
|
||||
|
||||
// Skipped when nothing matched: VACUUM rewrites the whole file, and
|
||||
// running it for zero deleted rows costs seconds on a large database
|
||||
// for no benefit. DeleteByRetentionPolicy already guards this way.
|
||||
if (deleted > 0)
|
||||
{
|
||||
InvalidateFtsIndex();
|
||||
TryPerformMaintenance();
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
// Hard-deletes every message whose ChatType IS in the list, then VACUUMs.
|
||||
// Returns the number of rows deleted.
|
||||
//
|
||||
// The mirror image of CleanupRetainOnly, and the privacy filter needs both.
|
||||
// With the unknown-channel failsafe on, the rule keeps every channel this
|
||||
// build does not recognise -- and a retain-list can only name the ones that
|
||||
// were already in the database when the list was built, so a channel whose
|
||||
// first message arrives after that would be deleted. Naming what goes
|
||||
// instead of what stays removes the window entirely.
|
||||
internal long CleanupDeleteTypes(IReadOnlyCollection<int> deleteTypes)
|
||||
{
|
||||
if (deleteTypes.Count == 0)
|
||||
return 0;
|
||||
|
||||
lock (_readLock)
|
||||
{
|
||||
long deleted;
|
||||
using (var cmd = Connection.CreateCommand())
|
||||
{
|
||||
var placeholders = BindIntList(cmd, "dt", deleteTypes);
|
||||
cmd.CommandText = $"DELETE FROM messages WHERE ChatType IN ({placeholders});";
|
||||
cmd.CommandTimeout = 600;
|
||||
deleted = cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
InvalidateFtsIndex();
|
||||
TryPerformMaintenance();
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
@@ -581,6 +686,33 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Runs maintenance and swallows a failure, for the delete paths only.
|
||||
//
|
||||
// VACUUM needs the database to itself, and a lazily consumed reader on the
|
||||
// primary connection -- which GetMostRecentMessages hands out and the
|
||||
// refilter walks outside the lock -- makes it fail immediately with "cannot
|
||||
// VACUUM - SQL statements in progress". That happens after the DELETE has
|
||||
// committed, so letting it escape means the caller reports "nothing was
|
||||
// removed" about a wipe that emptied the database.
|
||||
//
|
||||
// The rows are gone either way. An uncompacted file is a housekeeping
|
||||
// problem; telling somebody their history is still there when it is not is
|
||||
// a different kind of problem.
|
||||
private void TryPerformMaintenance()
|
||||
{
|
||||
try
|
||||
{
|
||||
PerformMaintenance();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
e,
|
||||
"Maintenance after a delete failed; the rows are gone but the file was not compacted."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private string LogPath => DbPath + "-wal";
|
||||
|
||||
internal long DatabaseSize() => !File.Exists(DbPath) ? 0 : new FileInfo(DbPath).Length;
|
||||
@@ -643,9 +775,21 @@ internal class MessageStore : IDisposable
|
||||
internal SqliteConnection OpenSecondaryConnection()
|
||||
{
|
||||
var conn = new SqliteConnection(BuildConnectionString(DbPath));
|
||||
conn.Open();
|
||||
ApplyPragmas(conn);
|
||||
return conn;
|
||||
try
|
||||
{
|
||||
conn.Open();
|
||||
ApplyPragmas(conn);
|
||||
return conn;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Open can succeed and ApplyPragmas still throw: journal_mode=WAL
|
||||
// needs a lock and gives up after DefaultTimeout. Without this the
|
||||
// connection is neither returned nor closed, and with Pooling=false
|
||||
// it survives until a finalizer gets to it.
|
||||
conn.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Worker-only mutator. The bulk-insert worker is the single legitimate
|
||||
@@ -797,8 +941,7 @@ internal class MessageStore : IDisposable
|
||||
// storage form on both sides so the IN(...) compare matches. SQLite has a
|
||||
// hard parameter limit of 999 in default builds, so we chunk the input --
|
||||
// a 1000-hit FTS query never explodes the SELECT. Result ordering is not
|
||||
// guaranteed; callers re-sort (e.g. DbViewer sorts by Date descending in
|
||||
// Sub-Task 4.4).
|
||||
// guaranteed; callers re-sort (DbViewer sorts by Date descending).
|
||||
public IReadOnlyList<Message> LoadByGuids(IReadOnlyList<string> guidStrings)
|
||||
{
|
||||
if (guidStrings.Count == 0)
|
||||
@@ -909,53 +1052,57 @@ internal class MessageStore : IDisposable
|
||||
|
||||
// Streams messages for export, sorted ascending by Date, excluding soft-deleted rows.
|
||||
// Optional filters: chatTypes, from/to inclusive date range.
|
||||
// Caller is responsible for disposing the enumerator.
|
||||
// Lock caveat: lock guards command setup and ExecuteReader; the returned
|
||||
// MessageEnumerator is iterated lazily by the caller outside the lock.
|
||||
// Acceptable for v1.4.8 -- DbViewer iterates on its filter-worker Task and
|
||||
// any clash with UpsertMessage on the primary Connection is rare and
|
||||
// serialised by SQLite's own connection-level lock. v1.5.x DI cycle should
|
||||
// address this with a snapshot-to-list or connection pool.
|
||||
// Caller is responsible for disposing the enumerator and the connection.
|
||||
//
|
||||
// Takes a caller-owned connection from OpenSecondaryConnection rather than
|
||||
// using the primary one, and therefore takes no lock. The reader stays open
|
||||
// for as long as the export writes, which is seconds to minutes on a large
|
||||
// history, and chat keeps arriving throughout -- so the primary connection
|
||||
// would be read here and written by UpsertMessage at the same time, and
|
||||
// SqliteConnection is not thread-safe. Holding _readLock for the whole
|
||||
// export would trade that for freezing the game instead.
|
||||
//
|
||||
// WAL gives readers their own snapshot, so a live write cannot tear the
|
||||
// export mid-file either.
|
||||
internal MessageEnumerator StreamForExport(
|
||||
SqliteConnection conn,
|
||||
IReadOnlyCollection<int>? chatTypes,
|
||||
DateTimeOffset? from,
|
||||
DateTimeOffset? to
|
||||
)
|
||||
{
|
||||
lock (_readLock)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
var cmd = conn.CreateCommand();
|
||||
|
||||
var clauses = new List<string> { "deleted = false" };
|
||||
if (chatTypes is { Count: > 0 })
|
||||
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 clauses = new List<string> { "deleted = false" };
|
||||
if (chatTypes is { Count: > 0 })
|
||||
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");
|
||||
|
||||
cmd.CommandText =
|
||||
@"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
WHERE "
|
||||
+ string.Join(" AND ", clauses)
|
||||
+ @"
|
||||
+ string.Join(" AND ", clauses)
|
||||
+ @"
|
||||
ORDER BY Date ASC;";
|
||||
cmd.CommandTimeout = 600;
|
||||
cmd.CommandTimeout = 600;
|
||||
|
||||
if (from is not null)
|
||||
cmd.Parameters.AddWithValue("$From", from.Value.ToUnixTimeMilliseconds());
|
||||
if (to is not null)
|
||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
||||
if (from is not null)
|
||||
cmd.Parameters.AddWithValue("$From", from.Value.ToUnixTimeMilliseconds());
|
||||
if (to is not null)
|
||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
||||
|
||||
return new MessageEnumerator(
|
||||
cmd.ExecuteReader(),
|
||||
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||
);
|
||||
}
|
||||
// Logger first: an argument list evaluates left to right, so a throwing
|
||||
// CreateLogger -- which is what a disposed host gives you -- would leave
|
||||
// an open reader that no MessageEnumerator owns.
|
||||
var logger = _loggerFactory.CreateLogger<MessageEnumerator>();
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), logger);
|
||||
}
|
||||
|
||||
// Returns the most recent messages, oldest-first.
|
||||
@@ -1014,7 +1161,8 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
|
||||
// Returns up to `limit` tells exchanged with the named player, oldest-first.
|
||||
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index, then
|
||||
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index (migration
|
||||
// 5; before that the ordering fell back to a temp b-tree over all rows), then
|
||||
// the client-side loop runs PlayerPayload comparison and breaks once
|
||||
// `limit` partner matches accumulate. Earlier versions had a hardcoded
|
||||
// 500-row scan cap that cut less-frequent pinned partners off the back of
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using HellionChat.Resources;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// How a sender's name is rendered in the chat log. Kept in its own file
|
||||
// (no Dalamud usings) so the SenderNameFormatter pure-helper test stays
|
||||
// AppDomain-isolated (feedback_dalamud_test_isolation).
|
||||
|
||||
public enum WorldSuffixMode
|
||||
{
|
||||
Never,
|
||||
OtherWorldOnly,
|
||||
Always,
|
||||
}
|
||||
|
||||
public enum NameFormMode
|
||||
{
|
||||
Full,
|
||||
FirstNameOnly,
|
||||
Initials,
|
||||
}
|
||||
|
||||
public static class NameDisplayModeExt
|
||||
{
|
||||
public static string Name(this WorldSuffixMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
WorldSuffixMode.Never => HellionStrings.NameDisplay_WorldSuffix_Never,
|
||||
WorldSuffixMode.OtherWorldOnly => HellionStrings.NameDisplay_WorldSuffix_OtherWorldOnly,
|
||||
WorldSuffixMode.Always => HellionStrings.NameDisplay_WorldSuffix_Always,
|
||||
_ => mode.ToString(),
|
||||
};
|
||||
|
||||
public static string Name(this NameFormMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
NameFormMode.Full => HellionStrings.NameDisplay_NameForm_Full,
|
||||
NameFormMode.FirstNameOnly => HellionStrings.NameDisplay_NameForm_FirstNameOnly,
|
||||
NameFormMode.Initials => HellionStrings.NameDisplay_NameForm_Initials,
|
||||
_ => mode.ToString(),
|
||||
};
|
||||
}
|
||||
Executable → Regular
+462
-411
File diff suppressed because it is too large
Load Diff
+661
-158
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ namespace HellionChat;
|
||||
|
||||
// Builds the generic-host DI container that drives v1.5.0+. The factory is
|
||||
// invoked synchronously from Plugin.ctor (after the schema gate clears) so the
|
||||
// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the
|
||||
// container exists before PluginLifecycle.LoadAsync runs. For the
|
||||
// deliberate divergence from Lightless' deferred Func-delegate pattern.
|
||||
internal static class PluginHostFactory
|
||||
{
|
||||
@@ -29,6 +29,15 @@ internal static class PluginHostFactory
|
||||
logging.AddDalamudLogging(dependencies.PluginLog);
|
||||
logging.SetMinimumLevel(LogLevel.Trace);
|
||||
})
|
||||
// ValidateOnBuild eagerly instantiates every singleton at Build time
|
||||
// so missing registrations / ConstructorCallSite cycles throw on
|
||||
// load instead of producing a silent hang. ValidateScopes is cheap
|
||||
// (we only use singletons) but guards against future Scoped misuse.
|
||||
.UseDefaultServiceProvider(o =>
|
||||
{
|
||||
o.ValidateOnBuild = true;
|
||||
o.ValidateScopes = true;
|
||||
})
|
||||
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
|
||||
.Build();
|
||||
}
|
||||
@@ -39,7 +48,7 @@ internal static class PluginHostFactory
|
||||
PluginHostDependencies dependencies
|
||||
)
|
||||
{
|
||||
// Block A — Dalamud services (21 [PluginService] singletons).
|
||||
// Dalamud services (21 [PluginService] singletons).
|
||||
services.AddSingleton(dependencies);
|
||||
services.AddSingleton(dependencies.PluginInterface);
|
||||
services.AddSingleton(dependencies.PluginLog);
|
||||
@@ -68,7 +77,7 @@ internal static class PluginHostFactory
|
||||
services.AddSingleton(plugin.WindowSystem);
|
||||
services.AddSingleton<PluginLifecycle>();
|
||||
|
||||
// Block B — HellionChat singletons. Factory lambdas because most
|
||||
// HellionChat singletons. Factory lambdas because most
|
||||
// classes are internal-sealed and the default activator only sees
|
||||
// public ctors.
|
||||
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
||||
@@ -80,7 +89,6 @@ internal static class PluginHostFactory
|
||||
services.AddSingleton(sp => new FontManager(
|
||||
sp.GetRequiredService<IDalamudPluginInterface>()
|
||||
));
|
||||
services.AddSingleton(_ => new StatusBar());
|
||||
services.AddSingleton(sp => new IpcManager(sp.GetRequiredService<ILogger<IpcManager>>()));
|
||||
services.AddSingleton(sp => new ExtraChat(sp.GetRequiredService<ILogger<ExtraChat>>()));
|
||||
|
||||
@@ -92,6 +100,23 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<ILogger<ThemeRegistry>>()
|
||||
));
|
||||
|
||||
services.AddSingleton(_ => new Ui.StyleEngine.TokenResolver());
|
||||
|
||||
// Transient: each surface owns its motes, so two backdrops on screen do
|
||||
// not drift in lockstep.
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.SectionRenderer(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
|
||||
services.AddTransient(sp => new Ui.StyleEngine.SurfaceBackdrop(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.StyleEngine.PushStack(
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
|
||||
services.AddSingleton(sp => new GameFunctions.GameFunctions(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<ILogger<GameFunctions.GameFunctions>>(),
|
||||
@@ -99,6 +124,7 @@ internal static class PluginHostFactory
|
||||
));
|
||||
services.AddSingleton(sp => new TypingIpc(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<ILogger<TypingIpc>>()
|
||||
));
|
||||
|
||||
@@ -107,6 +133,143 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
||||
sp.GetRequiredService<IFramework>()
|
||||
));
|
||||
services.AddSingleton(sp => new Services.TellRouterService(
|
||||
sp.GetRequiredService<MessageManager>(),
|
||||
sp.GetRequiredService<ILogger<Services.TellRouterService>>()
|
||||
));
|
||||
|
||||
services.AddSingleton(sp => new Ui.Components.HonorificHeader(
|
||||
sp.GetRequiredService<Integrations.HonorificService>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Sidebar(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.Sidebar>>(),
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.MessageList(
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
|
||||
));
|
||||
services.AddSingleton(_ => new Ui.Components.SymbolPicker());
|
||||
services.AddSingleton(sp => new Ui.Components.ThemeQuickPicker(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Plugin>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.InputBar(
|
||||
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
|
||||
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
|
||||
sp.GetRequiredService<Ui.CommandHelpWindow>(),
|
||||
sp.GetRequiredService<Ui.Components.ThemeQuickPicker>(),
|
||||
() => sp.GetRequiredService<Plugin>().MainWindow.UserHide()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea(
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ColorPicker(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ThemeImportExportRow(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.Settings.ThemeImportExportRow>>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.FontsSection(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ChatColourPicker(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AppearanceTab(
|
||||
sp.GetRequiredService<Ui.Components.Settings.ThemePicker>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ColorPicker>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.LivePreviewPanel>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ThemeImportExportRow>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.FontsSection>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ChatColourPicker>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.GeneralTab(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChatTab(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.WindowTab(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChannelsTab(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.DataPrivacyTab(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.Settings.Tabs.DataPrivacyTab>>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AboutTab(
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Integrations.HonorificService>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<IPlatformUtil>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.StatusBar(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.TopTabBar(
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Windows.MainWindow(
|
||||
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
|
||||
sp.GetRequiredService<Ui.Components.Sidebar>(),
|
||||
sp.GetRequiredService<Ui.Components.TopTabBar>(),
|
||||
sp.GetRequiredService<Ui.Components.MessageList>(),
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<Ui.Components.StatusBar>(),
|
||||
sp.GetRequiredService<Lender<PayloadHandler>>(),
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.CustomAudioPlayer(
|
||||
sp.GetRequiredService<ILogger<Integrations.CustomAudioPlayer>>()
|
||||
));
|
||||
|
||||
services.AddSingleton(sp => new MessageManager(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
@@ -128,33 +291,116 @@ internal static class PluginHostFactory
|
||||
);
|
||||
});
|
||||
|
||||
// Block C — Windows. WindowSystem.AddWindow is called from
|
||||
// PluginLifecycle.LoadAsync on the framework thread.
|
||||
services.AddSingleton(sp => new ChatLogWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<ILogger<ChatLogWindow>>(),
|
||||
sp.GetRequiredService<ILoggerFactory>()
|
||||
// Factory-lambdas for ChunkRenderer, PayloadHandler, and Lender<PayloadHandler>
|
||||
// because all three are internal-sealed (ActivatorUtilities can't reflect into
|
||||
// internal ctors) and Lender<T> has an internal ctor by design.
|
||||
// PayloadHandler registered twice: once as singleton for G/H, once via Lender<T> for per-frame isolation (I/J/K).
|
||||
services.AddSingleton(sp => new Ui.Components.ChunkRenderer(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.ChunkRenderer>>(),
|
||||
sp.GetRequiredService<GameFunctions.GameFunctions>()
|
||||
));
|
||||
services.AddSingleton(sp => new SettingsWindow(
|
||||
services.AddSingleton(sp => MakePayloadHandler(sp));
|
||||
services.AddSingleton(sp => new Lender<PayloadHandler>(() => MakePayloadHandler(sp)));
|
||||
|
||||
// Pop-out windows: each gets its OWN MessageList + InputBar so the
|
||||
// channel pill and message scroll are per-window. The PayloadHandler is
|
||||
// attached post-build (ChannelPopoutInitHostedService), NEVER via ctor
|
||||
// (would close a silent FactoryCallSite cycle).
|
||||
services.AddSingleton<Func<int, Ui.Windows.ChannelPopoutWindow>>(sp =>
|
||||
slot => new Ui.Windows.ChannelPopoutWindow(
|
||||
slot,
|
||||
new Ui.Components.MessageList(
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
|
||||
),
|
||||
new Ui.Components.InputBar(
|
||||
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
|
||||
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
|
||||
sp.GetRequiredService<Ui.CommandHelpWindow>()
|
||||
),
|
||||
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutWindow>>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>(),
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
)
|
||||
);
|
||||
services.AddSingleton(sp => new Ui.Windows.ChannelPopoutPool(
|
||||
sp.GetRequiredService<Func<int, Ui.Windows.ChannelPopoutWindow>>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutPool>>()
|
||||
));
|
||||
|
||||
// Windows. WindowSystem.AddWindow is called from
|
||||
// PluginLifecycle.LoadAsync on the framework thread.
|
||||
services.AddSingleton(sp => new Ui.Windows.SettingsWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.TabSidebar>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ContentArea>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ThemePicker>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.ColorPicker>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.LivePreviewPanel>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.AppearanceTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.GeneralTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.ChatTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.WindowTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.ChannelsTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.DataPrivacyTab>(),
|
||||
sp.GetRequiredService<Ui.Components.Settings.Tabs.AboutTab>(),
|
||||
sp.GetRequiredService<ILoggerFactory>()
|
||||
));
|
||||
services.AddSingleton(sp => new DbViewer(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<ILogger<DbViewer>>()
|
||||
));
|
||||
services.AddSingleton(sp => new InputPreview(sp.GetRequiredService<ChatLogWindow>()));
|
||||
services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService<ChatLogWindow>()));
|
||||
services.AddSingleton(sp => new InputPreview(
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
||||
sp.GetRequiredService<Lender<PayloadHandler>>(),
|
||||
sp.GetRequiredService<Ui.Windows.MainWindow>(),
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<ILogger<InputPreview>>()
|
||||
));
|
||||
// No MainWindow ctor-param: breaks the InputBar -> CommandHelpWindow ->
|
||||
// MainWindow -> InputBar singleton cycle. MainWindow is wired post-build
|
||||
// via CommandHelpWindowInitHostedService.
|
||||
services.AddSingleton(sp => new CommandHelpWindow(
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
||||
sp.GetRequiredService<ILogger<CommandHelpWindow>>()
|
||||
));
|
||||
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
||||
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
|
||||
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
|
||||
#if DEBUG
|
||||
services.AddSingleton(sp => new Ui.Windows.WidgetGalleryWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
#endif
|
||||
// The style lab: variants side by side, in-game, against the live theme.
|
||||
// Permanent, and deliberately not behind DEBUG: style decisions get
|
||||
// made in the build that actually ships.
|
||||
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
||||
sp.GetRequiredService<Plugin>()
|
||||
));
|
||||
services.AddSingleton(sp => new DebuggerWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<PayloadHandler>()
|
||||
));
|
||||
services.AddSingleton(sp => new FirstRunWizard(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||
));
|
||||
|
||||
// Hosted-service adapters: thin wrappers around the existing init
|
||||
// methods so the service class bodies stay unchanged. FontManager
|
||||
// does not need one — its ctor runs the init inline inside a single
|
||||
// SuppressAutoRebuild block on eager resolve.
|
||||
services.AddHostedService(sp => new ThemeRegistryInitHostedService(
|
||||
sp.GetRequiredService<ThemeRegistry>()
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
services.AddHostedService(sp => new IpcManagerInitHostedService(
|
||||
sp.GetRequiredService<IpcManager>()
|
||||
@@ -172,7 +418,41 @@ internal static class PluginHostFactory
|
||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||
sp.GetRequiredService<AutoTellTabsService>()
|
||||
));
|
||||
// Must come AFTER AutoTell's registration: both subscribe MessageProcessed,
|
||||
// and AutoTell subscribing first lets the router's IsOpen-guard see the
|
||||
// already-opened pop-out (FIFO framework-tick ordering, no double-pop).
|
||||
services.AddHostedService(sp => new TellRouterServiceInitHostedService(
|
||||
sp.GetRequiredService<Services.TellRouterService>()
|
||||
));
|
||||
services.AddHostedService(
|
||||
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
||||
sp.GetRequiredService<Integrations.FailedTellNotifier>()
|
||||
)
|
||||
);
|
||||
services.AddHostedService(sp => new PayloadHandlerInitHostedService(
|
||||
sp.GetRequiredService<PayloadHandler>(),
|
||||
sp.GetRequiredService<Ui.Components.MessageList>()
|
||||
));
|
||||
services.AddHostedService(sp => new CommandHelpWindowInitHostedService(
|
||||
sp.GetRequiredService<Ui.CommandHelpWindow>(),
|
||||
sp.GetRequiredService<Ui.Windows.MainWindow>()
|
||||
));
|
||||
services.AddHostedService(sp => new ChannelPopoutInitHostedService(
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
|
||||
sp.GetRequiredService<PayloadHandler>()
|
||||
));
|
||||
}
|
||||
|
||||
private static PayloadHandler MakePayloadHandler(IServiceProvider sp) =>
|
||||
new(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<IpcManager>(),
|
||||
sp.GetRequiredService<GameFunctions.GameFunctions>(),
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<Ui.Windows.MainWindow>(),
|
||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
||||
sp.GetRequiredService<ILogger<PayloadHandler>>()
|
||||
);
|
||||
}
|
||||
|
||||
internal sealed record PluginHostDependencies(
|
||||
|
||||
@@ -58,14 +58,23 @@ internal sealed class PluginLifecycle : IAsyncDisposable
|
||||
|
||||
private static void RegisterWindows(Plugin plugin)
|
||||
{
|
||||
plugin.WindowSystem.AddWindow(plugin.ChatLogWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.MainWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.DbViewer);
|
||||
plugin.WindowSystem.AddWindow(plugin.InputPreview);
|
||||
plugin.WindowSystem.AddWindow(Plugin.InputPreview);
|
||||
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
|
||||
#if DEBUG
|
||||
plugin.WindowSystem.AddWindow(plugin.WidgetGallery);
|
||||
#endif
|
||||
plugin.WindowSystem.AddWindow(plugin.InputBarLab);
|
||||
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
||||
|
||||
// Pop-out pool: register all pre-allocated instances ONCE here on the
|
||||
// framework thread. Open/Close at runtime is IsOpen-only, never AddWindow.
|
||||
foreach (var popout in plugin.ChannelPopoutPool.Instances)
|
||||
plugin.WindowSystem.AddWindow(popout);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
|
||||
namespace HellionChat.Privacy;
|
||||
|
||||
// The eight buckets the privacy surface sorts channels into. Eighty-nine
|
||||
// checkboxes in one flat list is not a choice anybody makes; eight named groups
|
||||
// is. Shared by the export form and the persist grid so the two screens
|
||||
// describe channels the same way.
|
||||
//
|
||||
// Headings are functions, not strings, so a language switch at runtime relabels
|
||||
// them on the next frame. A captured string would keep the language the window
|
||||
// happened to be opened in.
|
||||
//
|
||||
// Game Master channels follow ChatTypeExt.Parent(), which already pairs each of
|
||||
// them with its player counterpart. Filing them all under system traffic reads
|
||||
// tidier but puts GmTell -- a private two-person conversation -- outside the
|
||||
// direct-messages group, and an access request that quietly drops part of what
|
||||
// it promises is the dangerous kind of gap.
|
||||
//
|
||||
// Every ChatType belongs to exactly one group, and a build-suite test pins that.
|
||||
// A channel in no group cannot be picked in any of these screens, which reads as
|
||||
// a missing checkbox rather than as an omission.
|
||||
internal static class ChannelGroups
|
||||
{
|
||||
internal static readonly (Func<string> Heading, ChatType[] Types)[] All =
|
||||
[
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_DirectMessages,
|
||||
[ChatType.TellIncoming, ChatType.TellOutgoing, ChatType.GmTell]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_PartyAlliance,
|
||||
[
|
||||
ChatType.Party,
|
||||
ChatType.CrossParty,
|
||||
ChatType.Alliance,
|
||||
ChatType.PvpTeam,
|
||||
ChatType.PvpTeamAnnouncement,
|
||||
ChatType.PvpTeamLoginLogout,
|
||||
ChatType.GmParty,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_FreeCompany,
|
||||
[
|
||||
ChatType.FreeCompany,
|
||||
ChatType.FreeCompanyAnnouncement,
|
||||
ChatType.FreeCompanyLoginLogout,
|
||||
ChatType.GmFreeCompany,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_Linkshells,
|
||||
[
|
||||
ChatType.Linkshell1,
|
||||
ChatType.Linkshell2,
|
||||
ChatType.Linkshell3,
|
||||
ChatType.Linkshell4,
|
||||
ChatType.Linkshell5,
|
||||
ChatType.Linkshell6,
|
||||
ChatType.Linkshell7,
|
||||
ChatType.Linkshell8,
|
||||
ChatType.GmLinkshell1,
|
||||
ChatType.GmLinkshell2,
|
||||
ChatType.GmLinkshell3,
|
||||
ChatType.GmLinkshell4,
|
||||
ChatType.GmLinkshell5,
|
||||
ChatType.GmLinkshell6,
|
||||
ChatType.GmLinkshell7,
|
||||
ChatType.GmLinkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_CrossLinkshells,
|
||||
[
|
||||
ChatType.CrossLinkshell1,
|
||||
ChatType.CrossLinkshell2,
|
||||
ChatType.CrossLinkshell3,
|
||||
ChatType.CrossLinkshell4,
|
||||
ChatType.CrossLinkshell5,
|
||||
ChatType.CrossLinkshell6,
|
||||
ChatType.CrossLinkshell7,
|
||||
ChatType.CrossLinkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_ExtraChat,
|
||||
[
|
||||
ChatType.ExtraChatLinkshell1,
|
||||
ChatType.ExtraChatLinkshell2,
|
||||
ChatType.ExtraChatLinkshell3,
|
||||
ChatType.ExtraChatLinkshell4,
|
||||
ChatType.ExtraChatLinkshell5,
|
||||
ChatType.ExtraChatLinkshell6,
|
||||
ChatType.ExtraChatLinkshell7,
|
||||
ChatType.ExtraChatLinkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_PublicChat,
|
||||
[
|
||||
ChatType.Say,
|
||||
ChatType.Shout,
|
||||
ChatType.Yell,
|
||||
ChatType.NoviceNetwork,
|
||||
ChatType.NoviceNetworkSystem,
|
||||
ChatType.CustomEmote,
|
||||
ChatType.StandardEmote,
|
||||
ChatType.GmSay,
|
||||
ChatType.GmShout,
|
||||
ChatType.GmYell,
|
||||
ChatType.GmNoviceNetwork,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_SystemLogs,
|
||||
[
|
||||
ChatType.System,
|
||||
ChatType.Notice,
|
||||
ChatType.Urgent,
|
||||
ChatType.Echo,
|
||||
ChatType.NpcDialogue,
|
||||
ChatType.NpcAnnouncement,
|
||||
ChatType.LootNotice,
|
||||
ChatType.LootRoll,
|
||||
ChatType.RetainerSale,
|
||||
ChatType.Crafting,
|
||||
ChatType.Gathering,
|
||||
ChatType.Sign,
|
||||
ChatType.RandomNumber,
|
||||
ChatType.MessageBook,
|
||||
ChatType.Alarm,
|
||||
ChatType.Orchestrion,
|
||||
ChatType.GlamourNotifications,
|
||||
ChatType.PeriodicRecruitmentNotification,
|
||||
ChatType.GatheringSystem,
|
||||
ChatType.Progress,
|
||||
ChatType.Debug,
|
||||
ChatType.Error,
|
||||
ChatType.Item,
|
||||
ChatType.Action,
|
||||
ChatType.BattleSystem,
|
||||
ChatType.Damage,
|
||||
ChatType.Healing,
|
||||
ChatType.Miss,
|
||||
ChatType.GainBuff,
|
||||
ChatType.GainDebuff,
|
||||
ChatType.LoseBuff,
|
||||
ChatType.LoseDebuff,
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace HellionChat.Privacy;
|
||||
|
||||
internal static class PrivacyDefaults
|
||||
{
|
||||
// F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||
// Failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||
// persist unknown channels so a major patch's added ChatType isn't silently
|
||||
// dropped before the user can opt in or out. Existing configs keep their
|
||||
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
||||
@@ -114,4 +114,29 @@ internal static class PrivacyDefaults
|
||||
[ChatType.StandardEmote] = 1,
|
||||
[ChatType.NoviceNetwork] = 1,
|
||||
};
|
||||
|
||||
// Roleplay: Privacy-First + Say + both emote types. Public-distance
|
||||
// channels (Shout, Yell) stay out — they are public-noise from
|
||||
// strangers, not story content. Novice Network also stays out;
|
||||
// it is not RP-adjacent and would dilute the profile's intent.
|
||||
internal static readonly IReadOnlySet<ChatType> RoleplayWhitelist = new HashSet<ChatType>(
|
||||
PrivacyFirstWhitelist
|
||||
)
|
||||
{
|
||||
ChatType.Say,
|
||||
ChatType.CustomEmote,
|
||||
ChatType.StandardEmote,
|
||||
};
|
||||
|
||||
// RP sessions function as story logs: Say + emotes need a longer
|
||||
// window than Casual's 1-day public-chat window. 30 days for Say
|
||||
// keeps in-character dialogue scrollable across multiple sessions,
|
||||
// 90 days for emotes mirrors the Privacy-First conversation default.
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> RoleplayRetentionOverrides =
|
||||
new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.Say] = 30,
|
||||
[ChatType.CustomEmote] = 90,
|
||||
[ChatType.StandardEmote] = 90,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace HellionChat.Privacy;
|
||||
|
||||
// The rule that decides whether a message is written to disk, as plain logic.
|
||||
//
|
||||
// It lives apart from Configuration because Configuration implements a Dalamud
|
||||
// interface, and the build suite cannot load Dalamud.dll -- the runtime resolves
|
||||
// the declaring type before it ever reaches the method body, so even a static
|
||||
// call on it fails. This is the single most consequential branch in the plugin,
|
||||
// and it belongs where it can be pinned.
|
||||
internal static class StorageRule
|
||||
{
|
||||
// v1.12.0 corrected the last term. A known channel the user had unticked
|
||||
// used to fall through to the unknown-type failsafe, so the channel grid did
|
||||
// nothing at all whenever that failsafe was on. It is on by default, so the
|
||||
// filter stored everything while its own description promised "only messages
|
||||
// from allowed channels are written to the database".
|
||||
internal static bool Allows(bool listed, bool knownType, bool persistUnknownTypes) =>
|
||||
listed || (!knownType && persistUnknownTypes);
|
||||
|
||||
// Carry-over for configs written before that correction. Where the failsafe
|
||||
// made the list irrelevant and no channel was ever picked, the old rule
|
||||
// stored everything; the corrected rule would store nothing. Switching the
|
||||
// filter off keeps the behaviour and states it where the user can see it.
|
||||
//
|
||||
// A config that does have picks keeps them and starts honouring them, which
|
||||
// is the point of the change.
|
||||
internal static bool ShouldDisableFilterOnV24(
|
||||
bool filterEnabled,
|
||||
bool persistUnknownTypes,
|
||||
int listedCount
|
||||
) => filterEnabled && persistUnknownTypes && listedCount == 0;
|
||||
|
||||
// Why a retroactive cleanup cannot be offered, or that it can.
|
||||
internal enum CleanupAvailability
|
||||
{
|
||||
Available,
|
||||
|
||||
// Nothing is filtered, so nothing in the database contradicts the rule.
|
||||
FilterDisabled,
|
||||
|
||||
// The rule keeps no channel at all. CleanupRetainOnly refuses an empty
|
||||
// allowlist on purpose -- that request is a full wipe, and a full wipe
|
||||
// has its own button with its own confirmation.
|
||||
NothingListed,
|
||||
}
|
||||
|
||||
internal static CleanupAvailability CleanupState(bool filterEnabled, int listedCount) =>
|
||||
!filterEnabled ? CleanupAvailability.FilterDisabled
|
||||
: listedCount == 0 ? CleanupAvailability.NothingListed
|
||||
: CleanupAvailability.Available;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 419 KiB |
@@ -1,68 +0,0 @@
|
||||
.:;+xXXX$$$$$$$$XXx+;:
|
||||
.X$+ .;+X$$$$$$$$$$$$$$$$$$$$$$$$$$$x:
|
||||
;$xx$$X+:... .....::+X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;.
|
||||
X$; .:+xXXX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X:
|
||||
$$; :++xX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;
|
||||
$$x. .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X.
|
||||
x$$; ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+;::::::;x$$$$$:
|
||||
:$$$; .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+:. .+$$$$$$$$$X+;;:
|
||||
;$$$+. :X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;: :$$$$$$$$$$$$$$$$X;.
|
||||
.+$$$X: ..;X$$$$$$$$$$$$$$$$$$$$$$$$$$X;.. :$$$$$$$$$$$$$$$$$$$$X:
|
||||
;$$$$$X+::::+X$$$$$$$$$$$$$$$$$$$$$X;. .$$$$$$$$$$$$$$$$$$$$$$$X;
|
||||
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+: Hellion Forge x$$$$$$$$$$$$$$$$$$$$$$$$$X:
|
||||
.;x$$$$$$$$$$$$$$$$$$$$$x;: .X$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||
.;+$$$$$$$$$$X+;:.. .X$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||
.X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;
|
||||
.X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
;$$$$$$xx$$$$$$$$$$$$$$$$$$$$$x
|
||||
.$$$$$$x+$$$$$$$$$$$$$$$$$$$$$x
|
||||
:+X$$$$$$X;$$$$$$$$$$$$$$$$$$$$$$:
|
||||
;$$$$$$$$$$;$$$$$$$$$$$$$$$$$$$$$$X.
|
||||
+$$$$$$$$$$;x$$$$$$$$$$$$$$$$$$$$$$+
|
||||
x$$$$$$$$$$:$$$$$$$$$$$$$$$$$$$$$$X:
|
||||
.X$$$$$$$$$.:$$$$$$$$$$$$$$$$$$$$$$;
|
||||
:X$$X;;;;: .$$$$$$$$$$$$$$$$$$$$$$X.
|
||||
.$$$$X .$$$$$$$$$$$$$$$$$$$$$$$:
|
||||
.$$$$+ .X$$$$$$$$$$$$$$$$$$$$$$;
|
||||
;$$$$: .X$$$$$$$$$$$$$$$$$$$$$$x
|
||||
:X$$$+ .$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
+$$$x :$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
;$$X: $$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
x$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||
+$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||
.+$$$$$$$$$$$$$$$$$$$$$$$$$$;
|
||||
. ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$:
|
||||
:X$x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||
.XX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||
;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+$;
|
||||
.. ++X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$:+$:
|
||||
:$$+. ;$$$$$$$$$$$$$$X$$$$$$$$$$$$$$$$$$$$$;:$$+
|
||||
.x+X$X: X$$$$$$$$$$x::;:;$$$$$$$$$$$$$$$$$$X: ;$X.
|
||||
:X.x$$$:.::::::;x+:X$$$$;$$$$$$$$$$$$$$$$$$: :X;
|
||||
:x.x$$$$$$$$$$$$$$$$$;;$:$$$$$$$$$$$$$$$$$: :$+
|
||||
:Xx$$$$$$$$$$$$$$$$$: ;X;$$$$$$$$$$$$$$$$: .+$$;
|
||||
;$$$$$$$$$$$$$$$$$$; .X+X$$$$$$$$$$$$$$$+ .+$+.
|
||||
+$$$$$$$$$$$$$$$$$$$$$$$;+$$$$$$$$$$$$$$X: .+X:
|
||||
+$$$$$$$$$$$$$$$$$$$$$$$$$+:$$$$$$$$$$$$$+.+$+.
|
||||
;$$$$$$$$$$$$$$$$$$$$$$$$$$$X;$$$$$$$$$$$$$$X:
|
||||
+X: .:X$$$$$$$$x+++x$$$$$$$$;:X$$$$$$$$$$$X:
|
||||
:x.;$;+$$$$$:. :X$$$$X :$$$$$$$$$$X:
|
||||
;x :X$$$; .x$$x X$$; .:+.$$$$$$$$$$x
|
||||
xx.X$$X: X$;.:$X:.X$$$$$$$$$:
|
||||
+$$$$X. ;$;::: .$$$$$$$$$:
|
||||
;$$$; :+X$$$$XX$; X$$$$$$$$:
|
||||
;$$X: .:x$x$$$$$X. x$$$$$$$$:
|
||||
:X$X: :+x; :$$$$$: +$$$$$$$X:
|
||||
:++$X+xXX;. +$$$$. +$$$$$$$+.
|
||||
... .X$$$X. +$$$$$$$:
|
||||
;$$$$; .X$$$$$$x.
|
||||
;$$X; :X$$$$$$;
|
||||
;$$$$$$x.
|
||||
.X$$$$$$;
|
||||
;$$$$$$+
|
||||
+$$$$$;
|
||||
:X$$$$;.
|
||||
;$$$$+.
|
||||
.x$$$X:
|
||||
.+$$X;
|
||||
Binary file not shown.
+244
-79
@@ -41,11 +41,9 @@ internal class HellionStrings
|
||||
private static string Get(string key)
|
||||
=> ResourceManager.GetString(key, resourceCulture) ?? key;
|
||||
|
||||
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));
|
||||
@@ -61,11 +59,10 @@ internal class HellionStrings
|
||||
internal static string Privacy_PersistUnknown_Name => Get(nameof(Privacy_PersistUnknown_Name));
|
||||
internal static string Privacy_PersistUnknown_Description => Get(nameof(Privacy_PersistUnknown_Description));
|
||||
|
||||
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
|
||||
internal static string Cleanup_Unavailable_FilterOff => Get(nameof(Cleanup_Unavailable_FilterOff));
|
||||
internal static string Cleanup_Unavailable_NothingListed => Get(nameof(Cleanup_Unavailable_NothingListed));
|
||||
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));
|
||||
@@ -94,8 +91,8 @@ internal class HellionStrings
|
||||
internal static string Retention_Tag_Global => Get(nameof(Retention_Tag_Global));
|
||||
internal static string Retention_Reset_Button => Get(nameof(Retention_Reset_Button));
|
||||
internal static string Retention_Apply_Label => Get(nameof(Retention_Apply_Label));
|
||||
internal static string Retention_Apply_Tooltip => Get(nameof(Retention_Apply_Tooltip));
|
||||
internal static string Retention_Running => Get(nameof(Retention_Running));
|
||||
internal static string Retention_RunNow_Tooltip => Get(nameof(Retention_RunNow_Tooltip));
|
||||
internal static string Retention_LastRun_Never => Get(nameof(Retention_LastRun_Never));
|
||||
internal static string Retention_LastRun_At => Get(nameof(Retention_LastRun_At));
|
||||
internal static string Retention_Success => Get(nameof(Retention_Success));
|
||||
@@ -116,8 +113,41 @@ internal class HellionStrings
|
||||
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
||||
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
||||
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
||||
internal static string Wizard_Step1_Title => Get(nameof(Wizard_Step1_Title));
|
||||
internal static string Wizard_Step1_Subtitle => Get(nameof(Wizard_Step1_Subtitle));
|
||||
internal static string Wizard_Step1_Footer_Hint => Get(nameof(Wizard_Step1_Footer_Hint));
|
||||
internal static string Wizard_Step1_PluginNotice => Get(nameof(Wizard_Step1_PluginNotice));
|
||||
internal static string Wizard_Step1_Heritage => Get(nameof(Wizard_Step1_Heritage));
|
||||
internal static string Wizard_Step1_Skip_Label => Get(nameof(Wizard_Step1_Skip_Label));
|
||||
internal static string Wizard_Step1_Skip_Tooltip => Get(nameof(Wizard_Step1_Skip_Tooltip));
|
||||
internal static string Wizard_Step2_Title => Get(nameof(Wizard_Step2_Title));
|
||||
internal static string Wizard_Profile_Recommended_Badge => Get(nameof(Wizard_Profile_Recommended_Badge));
|
||||
internal static string Wizard_Profile_Roleplay_Heading => Get(nameof(Wizard_Profile_Roleplay_Heading));
|
||||
internal static string Wizard_Profile_Roleplay_Description => Get(nameof(Wizard_Profile_Roleplay_Description));
|
||||
internal static string Wizard_Profile_Roleplay_Apply => Get(nameof(Wizard_Profile_Roleplay_Apply));
|
||||
internal static string Wizard_Nav_Back => Get(nameof(Wizard_Nav_Back));
|
||||
internal static string Wizard_Nav_Next => Get(nameof(Wizard_Nav_Next));
|
||||
internal static string Wizard_Nav_Finish => Get(nameof(Wizard_Nav_Finish));
|
||||
internal static string Wizard_Step3_Title => Get(nameof(Wizard_Step3_Title));
|
||||
internal static string Wizard_Step3_Section_History => Get(nameof(Wizard_Step3_Section_History));
|
||||
internal static string Wizard_Step3_Section_TellTabs => Get(nameof(Wizard_Step3_Section_TellTabs));
|
||||
internal static string Wizard_Step3_Section_Visual => Get(nameof(Wizard_Step3_Section_Visual));
|
||||
internal static string Wizard_Step3_FilterIncludePreviousSessions_Label => Get(nameof(Wizard_Step3_FilterIncludePreviousSessions_Label));
|
||||
internal static string Wizard_Step3_AutoTellTabsHistoryPreload_Label => Get(nameof(Wizard_Step3_AutoTellTabsHistoryPreload_Label));
|
||||
internal static string Wizard_Step3_UseCompactDensity_Label => Get(nameof(Wizard_Step3_UseCompactDensity_Label));
|
||||
internal static string Wizard_Step3_PrettierTimestamps_Label => Get(nameof(Wizard_Step3_PrettierTimestamps_Label));
|
||||
internal static string Wizard_Step3_Theme_Label => Get(nameof(Wizard_Step3_Theme_Label));
|
||||
internal static string Wizard_Step4_Title => Get(nameof(Wizard_Step4_Title));
|
||||
internal static string Wizard_Step4_SummaryHeading => Get(nameof(Wizard_Step4_SummaryHeading));
|
||||
internal static string Wizard_Step4_Summary_Profile => Get(nameof(Wizard_Step4_Summary_Profile));
|
||||
internal static string Wizard_Step4_Summary_History => Get(nameof(Wizard_Step4_Summary_History));
|
||||
internal static string Wizard_Step4_Summary_TellTabs => Get(nameof(Wizard_Step4_Summary_TellTabs));
|
||||
internal static string Wizard_Step4_Summary_Visual => Get(nameof(Wizard_Step4_Summary_Visual));
|
||||
internal static string Wizard_Step4_Summary_Unchanged => Get(nameof(Wizard_Step4_Summary_Unchanged));
|
||||
internal static string Wizard_Step4_Summary_Off => Get(nameof(Wizard_Step4_Summary_Off));
|
||||
internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint));
|
||||
internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint));
|
||||
|
||||
internal static string Export_Heading => Get(nameof(Export_Heading));
|
||||
internal static string Export_Help => Get(nameof(Export_Help));
|
||||
internal static string Export_Range_Label => Get(nameof(Export_Range_Label));
|
||||
internal static string Export_Sender_Label => Get(nameof(Export_Sender_Label));
|
||||
@@ -172,8 +202,6 @@ internal class HellionStrings
|
||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||
internal static string PinTab_MenuPin => Get(nameof(PinTab_MenuPin));
|
||||
internal static string PinTab_MenuUnpin => Get(nameof(PinTab_MenuUnpin));
|
||||
internal static string PinTab_MenuPromote => Get(nameof(PinTab_MenuPromote));
|
||||
internal static string PinTab_PromoteTooltip => Get(nameof(PinTab_PromoteTooltip));
|
||||
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
|
||||
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
|
||||
internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip));
|
||||
@@ -203,8 +231,6 @@ internal class HellionStrings
|
||||
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));
|
||||
@@ -216,24 +242,6 @@ internal class HellionStrings
|
||||
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
||||
|
||||
// v1.1.0 — Settings card-grid overview
|
||||
internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title));
|
||||
internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext));
|
||||
internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title));
|
||||
internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext));
|
||||
internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title));
|
||||
internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext));
|
||||
internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title));
|
||||
internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext));
|
||||
internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title));
|
||||
internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext));
|
||||
internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title));
|
||||
internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext));
|
||||
internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title));
|
||||
internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext));
|
||||
internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title));
|
||||
internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext));
|
||||
internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title));
|
||||
internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext));
|
||||
|
||||
// v1.1.0 — Themes-Settings-Tab
|
||||
internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes));
|
||||
@@ -246,11 +254,7 @@ internal class HellionStrings
|
||||
internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply));
|
||||
internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep));
|
||||
|
||||
// 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));
|
||||
internal static string Settings_Language_FFXIVCoverage_Warning => Get(nameof(Settings_Language_FFXIVCoverage_Warning));
|
||||
|
||||
// Hellion Chat — Appearance-Tab section headings
|
||||
internal static string Settings_Appearance_Theme_Heading => Get(nameof(Settings_Appearance_Theme_Heading));
|
||||
@@ -258,34 +262,27 @@ internal class HellionStrings
|
||||
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));
|
||||
// Hellion Chat — Window-Tab section headings (pre-cycle legacy, kept for reference)
|
||||
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 — Chat-Tab SymbolPicker
|
||||
internal static string Settings_Chat_SymbolPicker_Enable_Name => Get(nameof(Settings_Chat_SymbolPicker_Enable_Name));
|
||||
internal static string Settings_Chat_SymbolPicker_Enable_Description => Get(nameof(Settings_Chat_SymbolPicker_Enable_Description));
|
||||
|
||||
// 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));
|
||||
internal static string Settings_Database_Busy => Get(nameof(Settings_Database_Busy));
|
||||
internal static string Settings_Database_ClearHint => Get(nameof(Settings_Database_ClearHint));
|
||||
internal static string Settings_Database_ClearError => Get(nameof(Settings_Database_ClearError));
|
||||
internal static string Settings_Database_Op_RetentionSweep => Get(nameof(Settings_Database_Op_RetentionSweep));
|
||||
internal static string Settings_Database_Op_Export => Get(nameof(Settings_Database_Op_Export));
|
||||
internal static string Settings_Database_Op_Cleanup => Get(nameof(Settings_Database_Op_Cleanup));
|
||||
internal static string Settings_Database_Op_Clear => Get(nameof(Settings_Database_Op_Clear));
|
||||
internal static string Settings_Database_Op_Preview => Get(nameof(Settings_Database_Op_Preview));
|
||||
internal static string Settings_Database_Op_Maintenance => Get(nameof(Settings_Database_Op_Maintenance));
|
||||
|
||||
// Hellion Chat — Default tab presets (channel-themed)
|
||||
internal static string Tabs_Presets_System => Get(nameof(Tabs_Presets_System));
|
||||
internal static string Tabs_Presets_Emote => Get(nameof(Tabs_Presets_Emote));
|
||||
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));
|
||||
@@ -318,14 +315,8 @@ internal class HellionStrings
|
||||
internal static string Settings_Window_ResetPosition_Description => Get(nameof(Settings_Window_ResetPosition_Description));
|
||||
|
||||
// Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs
|
||||
internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText));
|
||||
internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck));
|
||||
internal static string Popout_v060_HintOpenSettings => Get(nameof(Popout_v060_HintOpenSettings));
|
||||
|
||||
// Hellion Chat — v0.6.1 pop-out header hint banner (discoverability)
|
||||
internal static string Hint_v061_PopOutHeader_Body => Get(nameof(Hint_v061_PopOutHeader_Body));
|
||||
internal static string Hint_v061_PopOutHeader_Ack => Get(nameof(Hint_v061_PopOutHeader_Ack));
|
||||
internal static string Hint_v061_PopOutHeader_OpenSettings => Get(nameof(Hint_v061_PopOutHeader_OpenSettings));
|
||||
|
||||
// Hellion Chat — v1.0.0 Chat 2 parallel-load conflict detection
|
||||
internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle));
|
||||
@@ -341,14 +332,7 @@ internal class HellionStrings
|
||||
internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description));
|
||||
|
||||
// Hellion Chat — v1.2.1 Settings Cleanup: new card titles + subtexts
|
||||
internal static string Settings_Card_ThemeAndLayout_Title => Get(nameof(Settings_Card_ThemeAndLayout_Title));
|
||||
internal static string Settings_Card_ThemeAndLayout_Subtext => Get(nameof(Settings_Card_ThemeAndLayout_Subtext));
|
||||
internal static string Settings_Card_FontsAndColours_Title => Get(nameof(Settings_Card_FontsAndColours_Title));
|
||||
internal static string Settings_Card_FontsAndColours_Subtext => Get(nameof(Settings_Card_FontsAndColours_Subtext));
|
||||
internal static string Settings_Card_DataManagement_Title => Get(nameof(Settings_Card_DataManagement_Title));
|
||||
internal static string Settings_Card_DataManagement_Subtext => Get(nameof(Settings_Card_DataManagement_Subtext));
|
||||
internal static string Settings_Card_Integrations_Title => Get(nameof(Settings_Card_Integrations_Title));
|
||||
internal static string Settings_Card_Integrations_Subtext => Get(nameof(Settings_Card_Integrations_Subtext));
|
||||
|
||||
// Hellion Chat — v1.2.1 Theme & Layout tab section headings + WindowOpacity slider
|
||||
internal static string Settings_ThemeAndLayout_Theme_Heading => Get(nameof(Settings_ThemeAndLayout_Theme_Heading));
|
||||
@@ -357,26 +341,107 @@ internal class HellionStrings
|
||||
internal static string Settings_ThemeAndLayout_WindowOpacity_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Name));
|
||||
internal static string Settings_ThemeAndLayout_WindowOpacity_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Description));
|
||||
|
||||
// Hellion Chat — v1.2.1 Fonts & Colours tab section headings
|
||||
internal static string Settings_FontsAndColours_Fonts_Heading => Get(nameof(Settings_FontsAndColours_Fonts_Heading));
|
||||
internal static string Settings_FontsAndColours_Colours_Heading => Get(nameof(Settings_FontsAndColours_Colours_Heading));
|
||||
|
||||
// Hellion Chat — v1.2.1 Data Management tab section headings
|
||||
internal static string Settings_DataManagement_Storage_Heading => Get(nameof(Settings_DataManagement_Storage_Heading));
|
||||
internal static string Settings_DataManagement_Retention_Heading => Get(nameof(Settings_DataManagement_Retention_Heading));
|
||||
internal static string Settings_DataManagement_Cleanup_Heading => Get(nameof(Settings_DataManagement_Cleanup_Heading));
|
||||
internal static string Settings_DataManagement_Export_Heading => Get(nameof(Settings_DataManagement_Export_Heading));
|
||||
internal static string Settings_DataManagement_DbViewer_Heading => Get(nameof(Settings_DataManagement_DbViewer_Heading));
|
||||
internal static string Settings_DataManagement_Advanced_Heading => Get(nameof(Settings_DataManagement_Advanced_Heading));
|
||||
|
||||
// Hellion Chat — v1.2.1 Window-tab Behaviour heading (replaces Frame heading)
|
||||
internal static string Settings_Window_Frame_Behaviour_Heading => Get(nameof(Settings_Window_Frame_Behaviour_Heading));
|
||||
// v1.5.6: Data & Privacy tab section titles
|
||||
internal static string Settings_Section_PrivacyFilter => Get(nameof(Settings_Section_PrivacyFilter));
|
||||
internal static string Settings_Section_Storage => Get(nameof(Settings_Section_Storage));
|
||||
internal static string Settings_Section_Retention => Get(nameof(Settings_Section_Retention));
|
||||
internal static string Settings_Section_Cleanup => Get(nameof(Settings_Section_Cleanup));
|
||||
internal static string Settings_Section_Export => Get(nameof(Settings_Section_Export));
|
||||
internal static string Settings_Section_Telemetry => Get(nameof(Settings_Section_Telemetry));
|
||||
internal static string Settings_Theme_ActiveTheme => Get(nameof(Settings_Theme_ActiveTheme));
|
||||
internal static string Settings_Theme_ForkAndEdit => Get(nameof(Settings_Theme_ForkAndEdit));
|
||||
internal static string Settings_Theme_ForkTooltip => Get(nameof(Settings_Theme_ForkTooltip));
|
||||
internal static string Settings_Theme_EditTheme => Get(nameof(Settings_Theme_EditTheme));
|
||||
internal static string Settings_Theme_Editing => Get(nameof(Settings_Theme_Editing));
|
||||
internal static string Settings_Theme_Group_Surfaces => Get(nameof(Settings_Theme_Group_Surfaces));
|
||||
internal static string Settings_Theme_Group_Borders => Get(nameof(Settings_Theme_Group_Borders));
|
||||
internal static string Settings_Theme_Group_Text => Get(nameof(Settings_Theme_Group_Text));
|
||||
internal static string Settings_Theme_Group_Identity => Get(nameof(Settings_Theme_Group_Identity));
|
||||
internal static string Settings_Theme_Group_Status => Get(nameof(Settings_Theme_Group_Status));
|
||||
internal static string Settings_Theme_Save => Get(nameof(Settings_Theme_Save));
|
||||
internal static string Settings_Theme_Cancel => Get(nameof(Settings_Theme_Cancel));
|
||||
internal static string Settings_Theme_ResetToSource => Get(nameof(Settings_Theme_ResetToSource));
|
||||
internal static string Settings_Theme_ResetUnavailable => Get(nameof(Settings_Theme_ResetUnavailable));
|
||||
internal static string Settings_Theme_LockedTooltip => Get(nameof(Settings_Theme_LockedTooltip));
|
||||
internal static string Settings_Theme_Custom => Get(nameof(Settings_Theme_Custom));
|
||||
internal static string Settings_Tabs_Duplicate => Get(nameof(Settings_Tabs_Duplicate));
|
||||
internal static string Settings_Theme_ForkActive => Get(nameof(Settings_Theme_ForkActive));
|
||||
internal static string Settings_Theme_ImportFile => Get(nameof(Settings_Theme_ImportFile));
|
||||
internal static string Settings_Theme_ImportPathHint => Get(nameof(Settings_Theme_ImportPathHint));
|
||||
internal static string Settings_Theme_ExportDialogTitle => Get(nameof(Settings_Theme_ExportDialogTitle));
|
||||
internal static string Settings_Theme_Category_Cool => Get(nameof(Settings_Theme_Category_Cool));
|
||||
internal static string Settings_Theme_Category_Natural => Get(nameof(Settings_Theme_Category_Natural));
|
||||
internal static string Settings_Theme_Category_Classic => Get(nameof(Settings_Theme_Category_Classic));
|
||||
internal static string Settings_Theme_Category_Retro => Get(nameof(Settings_Theme_Category_Retro));
|
||||
internal static string Settings_Fonts_Bundled => Get(nameof(Settings_Fonts_Bundled));
|
||||
internal static string Settings_Fonts_GameFont => Get(nameof(Settings_Fonts_GameFont));
|
||||
internal static string Settings_Fonts_Global => Get(nameof(Settings_Fonts_Global));
|
||||
internal static string Settings_Fonts_Active => Get(nameof(Settings_Fonts_Active));
|
||||
internal static string Settings_Preview_TypeAMessage => Get(nameof(Settings_Preview_TypeAMessage));
|
||||
internal static string StatusBar_Tabs_One => Get(nameof(StatusBar_Tabs_One));
|
||||
internal static string StatusBar_Tabs_Other => Get(nameof(StatusBar_Tabs_Other));
|
||||
internal static string StatusBar_Tells_One => Get(nameof(StatusBar_Tells_One));
|
||||
internal static string StatusBar_Tells_Other => Get(nameof(StatusBar_Tells_Other));
|
||||
internal static string StatusBar_Messages => Get(nameof(StatusBar_Messages));
|
||||
internal static string StatusBar_MessagesThousands => Get(nameof(StatusBar_MessagesThousands));
|
||||
internal static string Settings_Preview_TitleMock => Get(nameof(Settings_Preview_TitleMock));
|
||||
internal static string Settings_Preview_StatusOpen => Get(nameof(Settings_Preview_StatusOpen));
|
||||
internal static string ChannelHeader_NotLoggedIn => Get(nameof(ChannelHeader_NotLoggedIn));
|
||||
internal static string InputBar_More_Tooltip => Get(nameof(InputBar_More_Tooltip));
|
||||
internal static string Settings_Section_Links => Get(nameof(Settings_Section_Links));
|
||||
internal static string Settings_Section_Behaviour => Get(nameof(Settings_Section_Behaviour));
|
||||
internal static string Settings_Section_Keybinds => Get(nameof(Settings_Section_Keybinds));
|
||||
internal static string Settings_Section_Notifications => Get(nameof(Settings_Section_Notifications));
|
||||
internal static string Settings_Section_DisplayModes => Get(nameof(Settings_Section_DisplayModes));
|
||||
internal static string Settings_Section_History => Get(nameof(Settings_Section_History));
|
||||
internal static string Settings_Section_CommandHelp => Get(nameof(Settings_Section_CommandHelp));
|
||||
internal static string Settings_Section_PluginDisclosure => Get(nameof(Settings_Section_PluginDisclosure));
|
||||
internal static string Settings_Section_LayoutMode => Get(nameof(Settings_Section_LayoutMode));
|
||||
internal static string Settings_Section_Opacity => Get(nameof(Settings_Section_Opacity));
|
||||
internal static string Settings_Section_ResizeBehaviour => Get(nameof(Settings_Section_ResizeBehaviour));
|
||||
internal static string Settings_Section_TellAutoOpen => Get(nameof(Settings_Section_TellAutoOpen));
|
||||
internal static string Settings_Section_Sidebar => Get(nameof(Settings_Section_Sidebar));
|
||||
internal static string Settings_Section_Brand => Get(nameof(Settings_Section_Brand));
|
||||
internal static string Settings_Section_Integrations => Get(nameof(Settings_Section_Integrations));
|
||||
internal static string Settings_Section_Credits => Get(nameof(Settings_Section_Credits));
|
||||
internal static string Settings_Section_License => Get(nameof(Settings_Section_License));
|
||||
internal static string Settings_Keybinds_Hint => Get(nameof(Settings_Keybinds_Hint));
|
||||
internal static string Settings_Keybinds_CycleNext => Get(nameof(Settings_Keybinds_CycleNext));
|
||||
internal static string Settings_Keybinds_CyclePrevious => Get(nameof(Settings_Keybinds_CyclePrevious));
|
||||
internal static string Settings_General_Language_Description => Get(nameof(Settings_General_Language_Description));
|
||||
internal static string Settings_Chat_Clock24_Name => Get(nameof(Settings_Chat_Clock24_Name));
|
||||
internal static string Settings_Chat_PreviousSessions_Name => Get(nameof(Settings_Chat_PreviousSessions_Name));
|
||||
internal static string Settings_Chat_PreviousSessions_Description => Get(nameof(Settings_Chat_PreviousSessions_Description));
|
||||
internal static string Settings_Chat_CommandHelpSide_Name => Get(nameof(Settings_Chat_CommandHelpSide_Name));
|
||||
internal static string Settings_Chat_CommandHelpSide_Description => Get(nameof(Settings_Chat_CommandHelpSide_Description));
|
||||
internal static string Settings_Channels_TellAutoOpenMode_Name => Get(nameof(Settings_Channels_TellAutoOpenMode_Name));
|
||||
internal static string Settings_Channels_TellAutoOpenMode_Description => Get(nameof(Settings_Channels_TellAutoOpenMode_Description));
|
||||
internal static string Settings_Channels_TellSwitchAlways_Name => Get(nameof(Settings_Channels_TellSwitchAlways_Name));
|
||||
internal static string Settings_Channels_TellSwitchAlways_Description => Get(nameof(Settings_Channels_TellSwitchAlways_Description));
|
||||
internal static string Settings_Window_LayoutSidebar => Get(nameof(Settings_Window_LayoutSidebar));
|
||||
internal static string Settings_Window_LayoutTopTabs => Get(nameof(Settings_Window_LayoutTopTabs));
|
||||
internal static string Settings_Window_TabPlacement_Name => Get(nameof(Settings_Window_TabPlacement_Name));
|
||||
internal static string Settings_Window_TabPlacement_Description => Get(nameof(Settings_Window_TabPlacement_Description));
|
||||
internal static string Settings_Window_TitleBar_Name => Get(nameof(Settings_Window_TitleBar_Name));
|
||||
internal static string Settings_Window_PopoutTitleBar_Name => Get(nameof(Settings_Window_PopoutTitleBar_Name));
|
||||
internal static string Settings_Window_AllowMove_Name => Get(nameof(Settings_Window_AllowMove_Name));
|
||||
internal static string Settings_Window_AllowResize_Name => Get(nameof(Settings_Window_AllowResize_Name));
|
||||
internal static string Settings_Window_SidebarThreshold_Name => Get(nameof(Settings_Window_SidebarThreshold_Name));
|
||||
internal static string Settings_Window_SidebarThreshold_Description => Get(nameof(Settings_Window_SidebarThreshold_Description));
|
||||
internal static string Settings_Window_PreviewPosition_Name => Get(nameof(Settings_Window_PreviewPosition_Name));
|
||||
internal static string Settings_Window_PreviewOnlyTyping_Name => Get(nameof(Settings_Window_PreviewOnlyTyping_Name));
|
||||
internal static string Settings_About_GiteaRepo => Get(nameof(Settings_About_GiteaRepo));
|
||||
internal static string Settings_About_CustomRepo => Get(nameof(Settings_About_CustomRepo));
|
||||
internal static string Settings_Section_AutoTranslate => Get(nameof(Settings_Section_AutoTranslate));
|
||||
internal static string Settings_Telemetry_None => Get(nameof(Settings_Telemetry_None));
|
||||
internal static string Settings_Section_Database => Get(nameof(Settings_Section_Database));
|
||||
|
||||
// Hellion Chat — v1.2.1 Migration v15 → v16 toast
|
||||
internal static string Migration_v16_OverrideStyle_Toast => Get(nameof(Migration_v16_OverrideStyle_Toast));
|
||||
|
||||
// Hellion Chat — v1.3.0 Integrations tab (Honorific + Coming-Soon roadmap)
|
||||
internal static string Settings_Tab_Integrations => Get(nameof(Settings_Tab_Integrations));
|
||||
// Hellion Chat — v1.3.0 Integrations (Honorific + Coming-Soon roadmap) — now in About tab
|
||||
internal static string Settings_Integrations_Intro => Get(nameof(Settings_Integrations_Intro));
|
||||
internal static string Settings_Integrations_Honorific_SectionHeader => Get(nameof(Settings_Integrations_Honorific_SectionHeader));
|
||||
internal static string Settings_Integrations_Honorific_Status_Detected => Get(nameof(Settings_Integrations_Honorific_Status_Detected));
|
||||
@@ -411,4 +476,104 @@ internal class HellionStrings
|
||||
internal static string DbViewer_FullTextToggle => Get(nameof(DbViewer_FullTextToggle));
|
||||
internal static string DbViewer_FullTextToggle_Hint_Indexing => Get(nameof(DbViewer_FullTextToggle_Hint_Indexing));
|
||||
internal static string DbViewer_FullTextToggle_Hint_PhraseMode => Get(nameof(DbViewer_FullTextToggle_Hint_PhraseMode));
|
||||
|
||||
// Hellion Chat — v1.5.4 header quick-picker + reduce-motion toggle
|
||||
internal static string Settings_QuickPicker_Tooltip => Get(nameof(Settings_QuickPicker_Tooltip));
|
||||
|
||||
internal static string InputBar_InsertSymbol_Tooltip =>
|
||||
Get(nameof(InputBar_InsertSymbol_Tooltip));
|
||||
|
||||
internal static string InputBar_Settings_Tooltip => Get(nameof(InputBar_Settings_Tooltip));
|
||||
|
||||
internal static string InputBar_HideChat_Tooltip => Get(nameof(InputBar_HideChat_Tooltip));
|
||||
internal static string InputBar_PopIn_Tooltip => Get(nameof(InputBar_PopIn_Tooltip));
|
||||
internal static string Settings_QuickPicker_Themes_Header => Get(nameof(Settings_QuickPicker_Themes_Header));
|
||||
internal static string Settings_QuickPicker_Tabs_Header => Get(nameof(Settings_QuickPicker_Tabs_Header));
|
||||
internal static string Settings_ThemeAndLayout_ReduceMotion_Name => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Name));
|
||||
internal static string Settings_ThemeAndLayout_ReduceMotion_Description => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Description));
|
||||
|
||||
// Failed-tell notification
|
||||
internal static string FailedTell_Notification_Generic => Get(nameof(FailedTell_Notification_Generic));
|
||||
internal static string FailedTell_Notification_Named => Get(nameof(FailedTell_Notification_Named));
|
||||
internal static string Settings_Chat_NotifyFailedTell_Name => Get(nameof(Settings_Chat_NotifyFailedTell_Name));
|
||||
internal static string Settings_Chat_NotifyFailedTell_Description => Get(nameof(Settings_Chat_NotifyFailedTell_Description));
|
||||
|
||||
// Per-tab notification sound
|
||||
internal static string Tabs_NotificationSound_Enable_Name => Get(nameof(Tabs_NotificationSound_Enable_Name));
|
||||
internal static string Tabs_NotificationSound_Description => Get(nameof(Tabs_NotificationSound_Description));
|
||||
internal static string Tabs_NotificationSound_Option => Get(nameof(Tabs_NotificationSound_Option));
|
||||
internal static string Tabs_NotificationSound_Preview => Get(nameof(Tabs_NotificationSound_Preview));
|
||||
internal static string Tabs_NotificationSound_CustomOption => Get(nameof(Tabs_NotificationSound_CustomOption));
|
||||
|
||||
// Scroll-to-bottom and item/flag linking
|
||||
internal static string ChatLog_ScrollToBottom_Tooltip => Get(nameof(ChatLog_ScrollToBottom_Tooltip));
|
||||
internal static string ChatLog_Insert_MapFlag => Get(nameof(ChatLog_Insert_MapFlag));
|
||||
internal static string ChatLog_Insert_ItemLink => Get(nameof(ChatLog_Insert_ItemLink));
|
||||
|
||||
// v1.5.6: plugin-disclosure warning
|
||||
internal static string Settings_Chat_NotifyPluginDisclosure_Name => Get(nameof(Settings_Chat_NotifyPluginDisclosure_Name));
|
||||
internal static string Settings_Chat_NotifyPluginDisclosure_Description => Get(nameof(Settings_Chat_NotifyPluginDisclosure_Description));
|
||||
internal static string ChatInput_PluginDisclosure_Warning => Get(nameof(ChatInput_PluginDisclosure_Warning));
|
||||
|
||||
// v1.5.6: world suffix + name format display options
|
||||
internal static string Settings_Chat_WorldSuffix_Name => Get(nameof(Settings_Chat_WorldSuffix_Name));
|
||||
internal static string Settings_Chat_WorldSuffix_Description => Get(nameof(Settings_Chat_WorldSuffix_Description));
|
||||
internal static string Settings_Chat_NameForm_Name => Get(nameof(Settings_Chat_NameForm_Name));
|
||||
internal static string Settings_Chat_NameForm_Description => Get(nameof(Settings_Chat_NameForm_Description));
|
||||
internal static string NameDisplay_WorldSuffix_Never => Get(nameof(NameDisplay_WorldSuffix_Never));
|
||||
internal static string NameDisplay_WorldSuffix_OtherWorldOnly => Get(nameof(NameDisplay_WorldSuffix_OtherWorldOnly));
|
||||
internal static string NameDisplay_WorldSuffix_Always => Get(nameof(NameDisplay_WorldSuffix_Always));
|
||||
internal static string NameDisplay_NameForm_Full => Get(nameof(NameDisplay_NameForm_Full));
|
||||
internal static string NameDisplay_NameForm_FirstNameOnly => Get(nameof(NameDisplay_NameForm_FirstNameOnly));
|
||||
internal static string NameDisplay_NameForm_Initials => Get(nameof(NameDisplay_NameForm_Initials));
|
||||
|
||||
// v1.5.6: inactive window opacity
|
||||
internal static string Settings_ThemeAndLayout_WindowOpacityInactive_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacityInactive_Name));
|
||||
internal static string Settings_ThemeAndLayout_WindowOpacityInactive_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacityInactive_Description));
|
||||
|
||||
// v1.5.6: custom sound volume
|
||||
internal static string Settings_General_CustomSoundVolume_Name => Get(nameof(Settings_General_CustomSoundVolume_Name));
|
||||
internal static string Settings_General_CustomSoundVolume_Description => Get(nameof(Settings_General_CustomSoundVolume_Description));
|
||||
|
||||
// v1.5.6: General tab collapsible section titles
|
||||
internal static string Settings_Section_Input => Get(nameof(Settings_Section_Input));
|
||||
internal static string Settings_Section_Sound => Get(nameof(Settings_Section_Sound));
|
||||
internal static string Settings_Section_Language => Get(nameof(Settings_Section_Language));
|
||||
internal static string Settings_Section_Performance => Get(nameof(Settings_Section_Performance));
|
||||
internal static string Settings_Section_Sound_TabsHint => Get(nameof(Settings_Section_Sound_TabsHint));
|
||||
|
||||
// v1.5.6: Chat tab collapsible section titles
|
||||
internal static string Settings_Section_Messages => Get(nameof(Settings_Section_Messages));
|
||||
internal static string Settings_Section_InputPreview => Get(nameof(Settings_Section_InputPreview));
|
||||
internal static string Settings_Section_AutoTellTabs => Get(nameof(Settings_Section_AutoTellTabs));
|
||||
internal static string Settings_Section_LinksTooltips => Get(nameof(Settings_Section_LinksTooltips));
|
||||
internal static string Settings_Section_NoviceNetwork => Get(nameof(Settings_Section_NoviceNetwork));
|
||||
|
||||
// v1.5.6: Appearance tab collapsible section titles
|
||||
internal static string Settings_Section_Theme => Get(nameof(Settings_Section_Theme));
|
||||
internal static string Settings_Section_Fonts => Get(nameof(Settings_Section_Fonts));
|
||||
internal static string Settings_Section_Colours => Get(nameof(Settings_Section_Colours));
|
||||
internal static string Settings_Section_WindowStyle => Get(nameof(Settings_Section_WindowStyle));
|
||||
internal static string Settings_Section_Timestamps => Get(nameof(Settings_Section_Timestamps));
|
||||
internal static string Settings_Section_Animations => Get(nameof(Settings_Section_Animations));
|
||||
|
||||
// v1.5.6: Window tab collapsible section titles
|
||||
internal static string Settings_Section_Hide => Get(nameof(Settings_Section_Hide));
|
||||
internal static string Settings_Section_InactivityHide => Get(nameof(Settings_Section_InactivityHide));
|
||||
internal static string Settings_Section_Frame => Get(nameof(Settings_Section_Frame));
|
||||
|
||||
// v1.5.6: Tabs tab per-tab-item sub-section titles
|
||||
internal static string Settings_Section_Tab_Channels => Get(nameof(Settings_Section_Tab_Channels));
|
||||
internal static string Settings_Section_Tab_Display => Get(nameof(Settings_Section_Tab_Display));
|
||||
internal static string Settings_Section_Tab_Notification => Get(nameof(Settings_Section_Tab_Notification));
|
||||
internal static string Settings_Section_Tab_Input => Get(nameof(Settings_Section_Tab_Input));
|
||||
internal static string Settings_Section_Tab_PopOut => Get(nameof(Settings_Section_Tab_PopOut));
|
||||
internal static string Settings_Section_Tab_Volume_AllTabsHint => Get(nameof(Settings_Section_Tab_Volume_AllTabsHint));
|
||||
|
||||
// v1.5.6: About tab collapsible section titles
|
||||
internal static string Settings_Section_Extensions => Get(nameof(Settings_Section_Extensions));
|
||||
internal static string Settings_Section_PluginInfo => Get(nameof(Settings_Section_PluginInfo));
|
||||
internal static string Settings_Section_Project => Get(nameof(Settings_Section_Project));
|
||||
internal static string Settings_Section_Translators => Get(nameof(Settings_Section_Translators));
|
||||
internal static string Settings_Section_Changelog => Get(nameof(Settings_Section_Changelog));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Executable → Regular
+93
-93
@@ -1,93 +1,93 @@
|
||||
Copyright 2013 The Exo 2 Project Authors (https://github.com/googlefonts/Exo-2.0)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
+19
-270
@@ -1859,7 +1859,25 @@ namespace HellionChat.Resources {
|
||||
return ResourceManager.GetString("ExtraGlyphRanges_Vietnamese_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Latin Extended.
|
||||
/// </summary>
|
||||
internal static string ExtraGlyphRanges_LatinExtended_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("ExtraGlyphRanges_LatinExtended_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Greek.
|
||||
/// </summary>
|
||||
internal static string ExtraGlyphRanges_Greek_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("ExtraGlyphRanges_Greek_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pick a folder location for export..
|
||||
/// </summary>
|
||||
@@ -3678,150 +3696,6 @@ namespace HellionChat.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to - Expect multi-boxing to work with this (only first client works).
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_DoNot_Multibox {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_DoNot_Multibox", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to - Forward the port used (9000).
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_DoNot_Port {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_DoNot_Port", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to - Share your authentication code with anyone else.
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_DoNot_Share {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_DoNot_Share", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unable to resolve hostname..
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Hostname_Fail {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Hostname_Fail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Note: This will require at least a semi-modern browser in order to function correctly..
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Note {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Note", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface.
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Tab {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Tab", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Do Not:.
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Warning_DoNot {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Warning_DoNot", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself..
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Warning_Header {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Warning_Header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!.
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Warning_Reason {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Warning_Reason", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately..
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Warning_Support {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Warning_Support", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface will start alongside the plugin automatically.
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceAutoStart_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceAutoStart_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start automatically.
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceAutoStart_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceAutoStart_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enables the webinterface that can be accessed with a browser..
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceEnable_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceEnable_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enabled.
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceEnable_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceEnable_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Limits the amount of log lines to show in the webinterface. This will improve loading performance..
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceMaxLinesToSend_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceMaxLinesToSend_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Log line limit for the webinterface.
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceMaxLinesToSend_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceMaxLinesToSend_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Window opacity.
|
||||
/// </summary>
|
||||
@@ -4002,130 +3876,5 @@ namespace HellionChat.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Button_Start {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Button_Start", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Stop.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Button_Stop {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Button_Stop", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Control Panel:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Controls {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Controls", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Active:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Controls_Active {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Controls_Active", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to URL:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Controls_Url {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Controls_Url", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authcode:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_CurrentPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_CurrentPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The port this webinterface will be running on..
|
||||
/// </summary>
|
||||
internal static string Webinterface_Option_Port_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Option_Port_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Port.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Option_Port_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Option_Port_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset your password and invalidate all session tokens..
|
||||
/// </summary>
|
||||
internal static string Webinterface_PasswordReset_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_PasswordReset_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface failed to start. Check /xllog for more information..
|
||||
/// </summary>
|
||||
internal static string Webinterface_Start_Failed {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Start_Failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface was started..
|
||||
/// </summary>
|
||||
internal static string Webinterface_Start_Success {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Start_Success", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface failed to stop. Check /xllog for more information..
|
||||
/// </summary>
|
||||
internal static string Webinterface_Stop_Failed {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Stop_Failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface was stopped..
|
||||
/// </summary>
|
||||
internal static string Webinterface_Stop_Success {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Stop_Success", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Usage Notice.
|
||||
/// </summary>
|
||||
internal static string Webinterface_UsageNotice {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_UsageNotice", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+26
-119
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Miscel·lània</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Use Dalamud's default language</value>
|
||||
</data>
|
||||
@@ -527,10 +524,10 @@
|
||||
<value>Finestra emergent</value>
|
||||
</data>
|
||||
<data name="Options_HideSameTimestamps_Name">
|
||||
<value>Hide timestamps when redundant</value>
|
||||
<value>Amaga les marques de temps redundants</value>
|
||||
</data>
|
||||
<data name="Options_HideSameTimestamps_Description">
|
||||
<value>Hide timestamps when previous messages have the same timestamp.</value>
|
||||
<value>Amaga la marca de temps quan el missatge anterior ja en té la mateixa.</value>
|
||||
</data>
|
||||
<data name="Options_ShowPopOutTitleBar_Name">
|
||||
<value>Show title bar for popped-out tabs</value>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>If this is enabled, the Auto Translate list will be sorted alphabetically.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Override Style</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the chat window. This may slightly improve performance.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>An error occurred while loading chat history. Please see plugin logs for more information to report this issue.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Hide during battle</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Hide the chat during battles.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Not Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes available:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Displays a preview with special parameter evaluated, like emotes and <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>S'ha produït un error enviant aquest missatge privat</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was stopped.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to stop. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface was started.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to start. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>The port this webinterface will be running on.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Forward the port used (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Share your authentication code with anyone else</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Expect multi-boxing to work with this (only first client works)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Unable to resolve hostname.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note: This will require at least a semi-modern browser in order to function correctly.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Usage Notice</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Tenyeix el selector de canal amb el color del canal</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>El botó selector de canal al costat del camp d'entrada es tenyeix amb el color del canal actiu. Coincideix amb la tonalitat del text d'entrada.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Amaga mentre el menú New Game+ estigui obert</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Amaga el xat mentre el menú New Game+ estigui obert. En tancar el menú, el xat torna a aparèixer.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Llatí estès</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Grec</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+20
-119
@@ -1,4 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Language.de.resx — Hellion Forge maintainer-extended translation
|
||||
|
||||
Locale: de (German)
|
||||
Maintainer: Hellion Forge / Hellion Online Media
|
||||
Status: Native-speaker maintained
|
||||
Review: Continuous (native maintainer)
|
||||
|
||||
Hellion Forge maintains this file with native-speaker quality,
|
||||
including the keys post-dating the last upstream Chat 2 Crowdin sync.
|
||||
|
||||
Corrections welcome via the Hellion Forge Discord:
|
||||
https://discord.gg/X9V7Kcv5gR
|
||||
-->
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -479,9 +493,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Sonstiges</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Dalamud's Standardsprache verwenden</value>
|
||||
</data>
|
||||
@@ -569,18 +580,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Wenn dies aktiviert ist, wird die Auto-Übersetzerliste alphabetisch sortiert.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Aktiviert</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Aktiviert das Webinterface, auf das mit einem Browser zugegriffen werden kann.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Automatisch starten</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Das Webinterface wird automatisch zusammen mit dem Plugin gestartet</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Stil überschreiben</value>
|
||||
</data>
|
||||
@@ -593,21 +592,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Tastenbelegung zum Rückwärts wechseln des Chat-Tabs</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Setze dein Passwort zurück und mache alle Sitzungstoken ungültig.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Einstellungen:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Aktiv:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>keins festgelegt</value>
|
||||
</data>
|
||||
@@ -1124,12 +1108,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Begrenzt die Anzahl der Nachrichten, die im Chatfenster angezeigt werden sollen. Dies kann die Leistung leicht verbessern.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>Beim Laden des Chat-Verlaufs ist ein Fehler aufgetreten. Weitere Informationen finden Sie unter /xllog, um dieses Problem zu melden.</value>
|
||||
</data>
|
||||
@@ -1175,21 +1153,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Der Eingabefokus wird beibehalten, selbst wenn du einen Kampf betrittst oder eine andere Aktion ausführst.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Emotes anzeigen</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Ersetzt Wörter mit ihrer Emote-Version, unterstützt derzeit BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blockierte Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Während des Kampfes ausblenden</value>
|
||||
</data>
|
||||
@@ -1202,18 +1165,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Blendet den Chat aus, solange das New-Game+ Menü geöffnet ist. Schließen des Menüs blendet den Chat wieder ein.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote-Statistik</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Bereit</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Nicht bereit</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes verfügbar:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Zeigt eine Vorschau mit speziellen Parametern, wie Emotes und <item>.</value>
|
||||
</data>
|
||||
@@ -1364,62 +1315,6 @@ Sie wurden gewarnt.</value>
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>Beim Senden dieser Nachricht ist ein Fehler aufgetreten</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface wurde gestoppt.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface konnte nicht gestoppt werden. Schau in /xllog nach weiteren Informationen.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface wurde gestartet.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface konnte nicht gestartet werden. Schau in /xllog nach weiteren Informationen.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>Der Port, auf dem dieses Webinterface laufen wird.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>The informal translation for the provided text in German would be:
|
||||
|
||||
Nachdem du 'Aktiviert' angeklickt und auf 'Start' gedrückt hast, wird die eingebaute Web-Oberfläche von Chat2 geladen. Dadurch können Geräte in deinem Netzwerk auf den In-Game-Chat zugreifen. Diese Funktion kann verwendet werden, um einem Handy oder einem anderen Computer zu ermöglichen, Chat2-Aktivitäten zu sehen, Kanäle zu wechseln und Nachrichten zu senden, als ob du direkt in FFXIV tippst.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>Aus Sicherheitsgründen ist diese Funktion nicht für die Nutzung außerhalb deines lokalen Netzwerks gedacht. Du wurdest gewarnt!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Folgendes ist dir untersagt:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Weiterleitung des benutzten Ports (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Dein Authentifizierungscode mit anderen zu teilen</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Zu erwarten, dass Multiboxing damit funktioniert (nur der erste Client funktioniert)."**</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>Es wird keine Unterstützung bereitgestellt, wenn eine der Untersagungen nicht respektiert oder eingehalten wird.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Hostname kann nicht ermittelt werden.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Hinweis: Dies erfordert mindestens einen halbwegs modernen Browser, um richtig zu funktionieren.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Nutzungshinweis</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1481,4 +1376,10 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latein erweitert</value>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Griechisch</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+25
-118
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Otros</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Interfaz Web</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Usar el idioma por defecto de Dalamud</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Si esto está habilitado, la lista del Traductor Automático se ordenará alfabéticamente.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Habilitado</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Habilita la interfaz web a la que se puede acceder con un navegador.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Iniciar automáticamente</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>La interfaz web comenzará junto al plugin automáticamente</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Sobrescribir estilo</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Ciclo de la pestaña de chat hacia atrás</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Código de autor:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Restablecer su contraseña e invalidar todos los tokens de sesión.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Panel de control:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Activo:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>ninguno establecido</value>
|
||||
</data>
|
||||
@@ -731,7 +701,7 @@
|
||||
<value>Decir</value>
|
||||
</data>
|
||||
<data name="ChatType_Shout">
|
||||
<value>Shout</value>
|
||||
<value>Vociferar</value>
|
||||
</data>
|
||||
<data name="ChatType_TellOutgoing">
|
||||
<value>Tell (saliente)</value>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limita la cantidad de líneas de registro que se pueden mostrar en la ventana de chat. Esto puede mejorar ligeramente el rendimiento.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Límite de líneas de registro para la interfaz web</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limita la cantidad de líneas de registro a mostrar en la interfaz web. Esto mejorará el rendimiento de la carga.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>Ha ocurrido un error al cargar el historial del chat. Por favor comprueba los registros del plugin para más informacion para reportar este problema.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Mantiene el foco de entrada, incluso si entras en combate o realizas otras acciones.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Mostrar emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Reemplaza las palabras por su versión de emote, actualmente soporta BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Gestos</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Emotes bloqueados</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Gesto</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Ocultar durante el combate</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Ocultar el chat durante combates.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Estadísticas de gestos</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Listo</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>No listo</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Gestos disponibles:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Muestra una vista previa con un parámetro especial evaluado, como emotes o <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>Se produjo un error al enviar este mensaje privado</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Interfaz web detenida.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>La interfaz web no se ha detenido. Revisa /xllog para más información.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Detener</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Iniciar</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Interfaz web iniciada.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>La interfaz web no pudo iniciarse. Revisa /xllog para más información.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Puerto</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>Puerto en el que se ejecutará esta interfaz web.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>Después de marcar 'Activado' y hacer clic en 'Iniciar', se cargará la interfaz web integrada de Chat2, que permitirá a los dispositivos de tu red acceder al chat en el juego. Esta función se puede utilizar para permitir que un teléfono u otro equipo vea la actividad de Chat2, cambie de canal, y envíe mensajes como si estuvieras escribiendo en FFXIV.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>Por razones de seguridad de la cuenta, esta característica no está pensada para su uso fuera de su red local, ¡usted ha sido advertido!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>No:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>Redirijas el puerto usado (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>Compartas tu código con nadie más</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>Esperes que el multi-Boeing funcione con esto (solo el primer cliente funciona).</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No se proporcionará ningún soporte a las cláusulas de "NO" que no sean respetadas y adheridas apropiadamente.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Incapaz de resolver nombre del host.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Nota: Esto requerirá al menos un navegador semi moderno para funcionar correctamente</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Aviso de Uso</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Teñir el selector de canal con el color del canal</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>El botón selector de canal junto al campo de entrada se tiñe con el color del canal activo. Coincide con el tinte del texto de entrada.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Ocultar mientras el menú New Game+ esté abierto</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Oculta el chat mientras el menú New Game+ esté abierto. Al cerrar el menú, el chat se muestra de nuevo.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latín extendido</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Griego</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Divers</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Interface web</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Utiliser la langue par défaut de Dalamud</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Si cette option est activée, la liste de Traduction Automatique sera triée par ordre alphabétique.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Activé</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Activer l'interface web accessible depuis un navigateur.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Démarrer automatiquement</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>L'interface web démarrera automatiquement avec le plugin</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Remplacer le style</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Raccourci clavier pour faire défiler l'onglet de discussion vers l'arrière</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Code d'authentification :</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Réinitialisez votre mot de passe et invalidez tous les jetons de session.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Panneau de configuration :</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Actif :</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL :</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>aucune valeur définie</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limite le nombre de lignes à afficher dans la fenêtre de chat. Cela peut légèrement améliorer les performances.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Nombre de lignes maximum de l'interface web</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limite le nombre de lignes à afficher dans l'interface web. Cela améliore la performance des chargements.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>Une erreur s'est produite lors du chargement de l'historique de la conversation. Veuillez consulter les logs du plugin pour plus d'informations pour signaler ce problème.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Maintient le focus de saisie, même si vous entrez au combat ou faites d'autres actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Afficher les emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Remplace les mots par leur version emote, prend actuellement en charge BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Émotes bloquées</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Cacher pendant le combat</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Cacher le chat pendant les combats.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Statistiques de l'emote</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Prêt</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Pas prêt</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes disponibles :</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Affiche un aperçu avec un paramètre spécial estimé, comme les emotes et <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>Une erreur s'est produite pendant l'envoi du message</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>L'interface web a été arrêtée.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>L'arrêt de l'interface Web a échoué. Consultez /xllog pour plus d'informations.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Arrêter</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Démarrer</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>L'interface Web a été démarrée.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Échec du démarrage de l'interface Web. Vérifiez /xllog pour plus d'informations.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>Le port sur lequel s'exécutera cette interface web.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>Après avoir coché 'Activé' et cliquez sur 'Démarrer', cela chargera l'interface web intégrée de Chat2, qui permettra aux périphériques de votre réseau d'accéder au chat en jeu. Cette fonctionnalité peut être utilisée pour permettre à un téléphone ou à un autre ordinateur de voir l'activité de Chat2, changer de canal, et envoyer des messages comme si vous écriviez dans FFXIV.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>Pour des raisons de sécurité du compte, cette fonctionnalité n'est pas destinée à être utilisée en dehors de votre réseau local, vous avez été averti !</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Ne pas faire :</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Rediriger le port utilisé (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Partager votre code d'authentification avec n'importe qui d'autre</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Vous attendre à ce que le multi-boxing fonctionne avec ça (seul le premier client fonctionne)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>Aucune assistance ne sera fournie si l'une des clauses "Ne pas faire" n'est pas respectée et observée de manière appropriée.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Impossible de résoudre le nom d'hôte.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note : Cela nécessitera au moins un navigateur semi-moderne pour fonctionner correctement.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Notice d'utilisation</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Teinter le sélecteur de canal avec la couleur du canal</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>Le bouton sélecteur de canal à côté du champ de saisie est teinté avec la couleur du canal actif. Correspond à la teinte du texte de saisie.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Masquer pendant que le menu New Game+ est ouvert</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Masque le chat pendant que le menu New Game+ est ouvert. Fermer le menu réaffiche le chat.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latin étendu</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Grec</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+26
-119
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Miscellaneous</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Interfaccia web</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Utilizza la lingua predefinita di Dalamud</value>
|
||||
</data>
|
||||
@@ -527,10 +524,10 @@
|
||||
<value>Pop out</value>
|
||||
</data>
|
||||
<data name="Options_HideSameTimestamps_Name">
|
||||
<value>Hide timestamps when redundant</value>
|
||||
<value>Nascondi gli orari ridondanti</value>
|
||||
</data>
|
||||
<data name="Options_HideSameTimestamps_Description">
|
||||
<value>Hide timestamps when previous messages have the same timestamp.</value>
|
||||
<value>Nasconde l'orario quando il messaggio precedente ha già lo stesso.</value>
|
||||
</data>
|
||||
<data name="Options_ShowPopOutTitleBar_Name">
|
||||
<value>Show title bar for popped-out tabs</value>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>If this is enabled, the Auto Translate list will be sorted alphabetically.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Abilita</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Override Style</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the chat window. This may slightly improve performance.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>An error occurred while loading chat history. Please see plugin logs for more information to report this issue.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Nascondi in battaglia</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Nascondi la chat in battaglia.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Pronto</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Non pronto</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes available:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Displays a preview with special parameter evaluated, like emotes and <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>Si è verificato un errore durante l'invio del messaggio privato</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>L'interfaccia web è stata interrotta.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>L'interfaccia web non si è interrotta. Controlla /xllog per maggiori informazioni.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Interrompi</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Inizia</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>L'interfaccia web è stata avviata.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Avvio interfaccia web non riuscito. Controlla /xllog per maggiori informazioni.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Porta</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>La porta su cui verrà attivata questa interfaccia web.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>Dopo aver selezionato 'Abilita' e aver cliccato su 'Inizia' questo caricherà l'interfaccia web integrata di Chat2, che permetterà ai dispositivi di rete di accedere alla chat in-game. Questa funzione può essere utilizzata per consentire a un telefono o a un altro computer di vedere l'attività di Chat2, cambiare canali e inviare messaggi come se si stesse digitando su FFXIV.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>Per motivi di sicurezza dell'account, questa funzione non è destinata ad essere utilizzata al di fuori della rete locale, sei stato avvisato!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Non Fare:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Inoltrare la porta utilizzata (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Condividere il tuo codice di autenticazione con altre persone</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Aspettarsi che il multi-boxing funzioni (funziona solo il primo client)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>Non sarà fornito alcun supporto se una qualsiasi delle clausole 'Non Fare' non è rispettata in modo adeguato.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Impossibile risolvere il nome host.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Nota: Questo richiederà almeno un browser semi-moderno per funzionare correttamente.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Avviso Di Utilizzo</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Colora il selettore di canale con il colore del canale</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>Il pulsante selettore di canale accanto al campo di input viene colorato con il colore del canale attivo. Corrisponde alla colorazione del testo di input.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Nascondi mentre il menu New Game+ è aperto</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Nasconde la chat mentre il menu New Game+ è aperto. Chiudendo il menu, la chat riappare.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latino esteso</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Greco</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>その他の設定</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webインターフェイス</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Dalamudのデフォルト言語を使用する</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>これが有効な場合、自動翻訳リストはアルファベット順にソートされます。</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>有効</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>ブラウザでアクセスできるWebインターフェースを有効にします。</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>自動で開始</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webインターフェイスは自動的にプラグインと一緒に起動します</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>スタイルを上書き</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>チャットタブの逆方向キーバインド</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>認証コード:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>パスワードをリセットし、すべてのセッショントークンを無効にします。</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>コントロールパネル:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>有効:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>未設定</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>チャットウィンドウに表示するログ行数を制限します。これによりパフォーマンスが若干向上する可能性があります。</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Webインターフェースのログ行数制限</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Webインターフェースに表示するログの数を制限します。これにより読み込みのパフォーマンスが向上します。</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>チャット履歴の読み込み中にエラーが発生しました。この問題を報告するには、プラグインのログを参照してください。</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>戦闘に入ったり、他のアクションを行ったりしても、入力フォーカスを維持します。</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>エモートを表示</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>単語をエモートバージョンに置き換え、現在BetterTTVをサポートしています。</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>エモート</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>ブロックされたエモート</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>エモート</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>戦闘中は非表示</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>戦闘中はチャットを非表示にします。</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>準備完了</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>未準備</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>利用可能なエモート:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>emotes や <item> のように、特別なパラメータが評価されたプレビューを表示します。</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>このメッセージを送信中にエラーが発生しました</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webインターフェースが停止しました。</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webインターフェイスの停止に失敗しました。詳細は /xllog を確認してください。</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webインターフェースを開始しました。</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webインターフェースの起動に失敗しました。詳細は /xllog を確認してください。</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>ポート</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>このWebインターフェイスのポートは動作します。</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>「有効」にチェックを入れて「開始」をクリックすると、Chat2の内蔵Webインターフェースが読み込まれ、ネットワーク上のデバイスがゲーム内チャットにアクセスできるようになります。 この機能は、電話または別のコンピュータがチャット2アクティビティを表示できるようにするために使用できます。 FFXIV を入力しているかのようにチャネルを切り替え、メッセージを送信します。</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>アカウントのセキュリティ上の理由から、この機能はローカルネットワーク外での使用を意図したものではありません。注意してください!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- 使用するポートを転送 (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- あなたの認証コードを他の人と共有します</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- マルチボクシングがこれで動作することを期待します (最初のクライアントのみ)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>「Do Not」条項のいずれかが尊重されず、適切に遵守されていない場合は、サポートは提供されません。</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>ホスト名を解決できません</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>注意: 正しく機能するためには、少なくとも半最新のブラウザが必要です。</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>使用上の注意</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>チャンネルセレクターをチャンネル色で着色する</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>入力フィールドの隣のチャンネルセレクターボタンが、現在アクティブなチャンネルの色で着色されます。入力テキスト自体の色合いと一致します。</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>ニューゲーム+メニューが開いている間は非表示</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>ニューゲーム+メニューが開いている間、チャットを非表示にします。メニューを閉じるとチャットが再表示されます。</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>拡張ラテン</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>ギリシャ語</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>기타</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>달라무드의 기본 언어를 사용</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>이 옵션이 활성화되면, 상용구 사전이 가나다 순서로 정렬됩니다.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>스타일 덮어쓰기</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>채팅창에 표시할 로그 줄 수의 양을 제한합니다. 성능을 약간 향상시킬 수 있습니다.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>채팅 기록을 불러오지 못했습니다. 자세한 정보는 플러그인 로그를 확인해주세요.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Hide during battle</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Hide the chat during battles.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Not Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes available:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Displays a preview with special parameter evaluated, like emotes and <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>An error occured while sending this tell message</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was stopped.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to stop. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface was started.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to start. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>The port this webinterface will be running on.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Forward the port used (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Share your authentication code with anyone else</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Expect multi-boxing to work with this (only first client works)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Unable to resolve hostname.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note: This will require at least a semi-modern browser in order to function correctly.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Usage Notice</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>채널 선택기를 채널 색상으로 채색</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>입력 필드 옆의 채널 선택기 버튼이 현재 활성 채널 색상으로 채색됩니다. 입력 텍스트 자체의 색조와 일치합니다.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>뉴게임+ 메뉴가 열려 있는 동안 숨김</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>뉴게임+ 메뉴가 열려 있는 동안 채팅을 숨깁니다. 메뉴를 닫으면 채팅이 다시 표시됩니다.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>확장 라틴</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>그리스어</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
Generated
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Overig</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Gebruik de standaard taal van Dalamud</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Als dit is ingeschakeld, zal de Automatisch Vertalingen lijst alfabetisch worden gesorteerd.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Ingeschakeld</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Schakelt de webinterface in die toegankelijk is met een browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatisch</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface start automatisch naast de plugin</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Stijl overschrijven</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cyclus chat tab achterwaarts toetsbinding</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset je wachtwoord en maak alle sessietokens ongeldig.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Bedieningspaneel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Actief:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>niet ingesteld</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limiteer het maximum aantal regels die worden getoont in het chatvester.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Regel limiet voor de webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Beperkt het aantal regels om weer te geven in de webinterface. Dit zal de laadprestaties verbeteren.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>Er is een fout opgetreden tijdens het laden van de chatgeschiedenis. Bekijk de plugin logs voor meer informatie om dit probleem te rapporteren.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Houdt de invoerfocus vast, zelfs als je een gevecht aangaat of andere acties uitvoert.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Emotes tonen</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Vervangt woorden door hun emote-versie, ondersteunt momenteel BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Geblokkeerde emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Verbergen tijdens gevecht</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Verberg de chat tijdens gevechten.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote-statistieken</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Gereed</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Niet Gereed</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes beschikbaar:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Toont een voorvertoning met geëvalueerde speciale parameters, zoals emotes en <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>Er is een fout opgetreden tijdens het verzenden van dit bericht</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was gestopt.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface kan niet stoppen. Controleer /xllog voor meer informatie.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Begin</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface werd gestart.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface is niet gestart. Controleer /xllog voor meer informatie.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Poort</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>De poort waarop deze webinterface zal draaien.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>Nadat je 'Ingeschakeld' hebt aangevinkt en op 'Start' hebt geklikt, wordt de ingebouwde webinterface van Chat2 geladen, waarmee apparaten op je netwerk toegang krijgen tot in-game chat. Deze functie kan worden gebruikt om een telefoon of een andere computer Chat2-activiteit te laten zien, van kanaal te laten wisselen en berichten te laten versturen alsof je in FFXIV zelf aan het typen bent.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>Om redenen van accountbeveiliging is deze functie niet bedoeld voor gebruik buiten je lokale netwerk, je bent gewaarschuwd!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Niet doen:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- De gebruikte poort doorsturen (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Je verificatiecode met iemand anders delen</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Verwacht dat multi-boxing hiermee werkt (alleen eerste client werkt)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>Er wordt geen ondersteuning geboden als een van de 'Niet doen'-clausules niet wordt gerespecteerd en nageleefd.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Kan hostnaam niet oplossen.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Opmerking: Dit vereist ten minste een semi-moderne browser om correct te functioneren.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Gebruiksaanwijzing</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Kanaalkiezer kleuren met kanaalkleur</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>De kanaalkiezerknop naast het invoerveld krijgt de kleur van het actieve kanaal. Komt overeen met de tint van de invoertekst zelf.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Verbergen terwijl het New Game+ menu open is</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Verberg de chat terwijl het New Game+ menu open is. Het sluiten van het menu toont de chat weer.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latijn uitgebreid</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Grieks</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+1386
File diff suppressed because it is too large
Load Diff
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Diversos</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Usar idioma padrão do Dalamud</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Se esta opção estiver habilitada, a lista de tradução automática será classificada em ordem alfabética.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Substituir estilo</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limita a quantidade de linhas de registro para mostrar na janela do bate-papo. Isso pode melhorar um pouco o desempenho.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>Ocorreu um erro ao carregar o histórico do bate-papo. Por favor, consulte os registros do plugin para obter mais informações para reportar este problema.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Mantém o foco de entrada, mesmo que você entre em batalha ou faça outras ações.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Exibir gestos</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Substitui as palavras por sua versão de gesto, atualmente suporta BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Gesto</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Gestos bloqueados</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Gesto</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Esconder durante combate</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Esconder o bate-papo durante combate.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Status de Gestos</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Pronto</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Não está pronto</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Gestos disponíveis:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Exibe uma prévia com parâmetro especial avaliado, como gestos e <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>An error occured while sending this tell message</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was stopped.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to stop. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface was started.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to start. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>The port this webinterface will be running on.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Forward the port used (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Share your authentication code with anyone else</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Expect multi-boxing to work with this (only first client works)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Unable to resolve hostname.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note: This will require at least a semi-modern browser in order to function correctly.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Usage Notice</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Colorir o seletor de canal com a cor do canal</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>O botão seletor de canal ao lado do campo de entrada é colorido com a cor do canal ativo. Combina com a coloração do próprio texto de entrada.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Ocultar enquanto o menu New Game+ estiver aberto</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Oculta o chat enquanto o menu New Game+ estiver aberto. Fechar o menu mostra o chat novamente.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latim estendido</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Grego</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
+1386
File diff suppressed because it is too large
Load Diff
@@ -478,9 +478,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Miscellaneous</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Use Dalamud's default language</value>
|
||||
</data>
|
||||
@@ -568,18 +565,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>If this is enabled, the Auto Translate list will be sorted alphabetically.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Override Style</value>
|
||||
</data>
|
||||
@@ -592,21 +577,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1123,12 +1093,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the chat window. This may slightly improve performance.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>An error occurred while loading chat history. Please see plugin logs for more information to report this issue.</value>
|
||||
</data>
|
||||
@@ -1174,21 +1138,6 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Hide during battle</value>
|
||||
</data>
|
||||
@@ -1201,18 +1150,6 @@
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Not Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes available:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Displays a preview with special parameter evaluated, like emotes and <item>.</value>
|
||||
</data>
|
||||
@@ -1363,60 +1300,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>An error occured while sending this tell message</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was stopped.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to stop. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface was started.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to start. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>The port this webinterface will be running on.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Forward the port used (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Share your authentication code with anyone else</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Expect multi-boxing to work with this (only first client works)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Unable to resolve hostname.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note: This will require at least a semi-modern browser in order to function correctly.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Usage Notice</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1478,4 +1361,10 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latin Extended</value>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Greek</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Generated
+24
-117
@@ -472,9 +472,6 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Diverse</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Folosește limba implicită a lui Dalamud</value>
|
||||
</data>
|
||||
@@ -562,18 +559,6 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>Dacă această opțiune este activată, lista de traduceri automate va fi sortată alfabetic.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Name">
|
||||
<value>Start automatically</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceAutoStart_Description">
|
||||
<value>Webinterface will start alongside the plugin automatically</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Override Style</value>
|
||||
</data>
|
||||
@@ -586,21 +571,6 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Authcode:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls">
|
||||
<value>Control Panel:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Controls_Url">
|
||||
<value>URL:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
@@ -1117,12 +1087,6 @@
|
||||
<data name="Options_MaxLinesToShow_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the chat window. This may slightly improve performance.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Name" xml:space="preserve">
|
||||
<value>Log line limit for the webinterface</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceMaxLinesToSend_Description" xml:space="preserve">
|
||||
<value>Limits the amount of log lines to show in the webinterface. This will improve loading performance.</value>
|
||||
</data>
|
||||
<data name="LoadMessages_Error" xml:space="preserve">
|
||||
<value>An error occurred while loading chat history. Please see plugin logs for more information to report this issue.</value>
|
||||
</data>
|
||||
@@ -1168,39 +1132,12 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions.</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Name" xml:space="preserve">
|
||||
<value>Hide during battle</value>
|
||||
</data>
|
||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||
<value>Hide the chat during battles.</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||
<value>Emote Stats</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Ready" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_NotReady" xml:space="preserve">
|
||||
<value>Not Ready</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Loaded" xml:space="preserve">
|
||||
<value>Emotes available:</value>
|
||||
</data>
|
||||
<data name="Options_Preview_Description" xml:space="preserve">
|
||||
<value>Displays a preview with special parameter evaluated, like emotes and <item>.</value>
|
||||
</data>
|
||||
@@ -1351,60 +1288,6 @@
|
||||
<data name="Chat_SendTell_Error" xml:space="preserve">
|
||||
<value>An error occured while sending this tell message</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Success" xml:space="preserve">
|
||||
<value>Webinterface was stopped.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Stop_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to stop. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Webinterface_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Success" xml:space="preserve">
|
||||
<value>Webinterface was started.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Start_Failed" xml:space="preserve">
|
||||
<value>Webinterface failed to start. Check /xllog for more information.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Name" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Webinterface_Option_Port_Description" xml:space="preserve">
|
||||
<value>The port this webinterface will be running on.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Header" xml:space="preserve">
|
||||
<value>After checking 'Enabled' and clicking 'Start' this will load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Reason" xml:space="preserve">
|
||||
<value>For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_DoNot" xml:space="preserve">
|
||||
<value>Do Not:</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Port" xml:space="preserve">
|
||||
<value>- Forward the port used (9000)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Share" xml:space="preserve">
|
||||
<value>- Share your authentication code with anyone else</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_DoNot_Multibox" xml:space="preserve">
|
||||
<value>- Expect multi-boxing to work with this (only first client works)</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Warning_Support" xml:space="preserve">
|
||||
<value>No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Hostname_Fail" xml:space="preserve">
|
||||
<value>Unable to resolve hostname.</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Note" xml:space="preserve">
|
||||
<value>Note: This will require at least a semi-modern browser in order to function correctly.</value>
|
||||
</data>
|
||||
<data name="Webinterface_UsageNotice" xml:space="preserve">
|
||||
<value>Usage Notice</value>
|
||||
</data>
|
||||
<data name="Options_Help_SenderMessages" xml:space="preserve">
|
||||
<value>This disables the channel selection and shows all messages coming from the target.</value>
|
||||
</data>
|
||||
@@ -1466,4 +1349,28 @@ Your old database can still be recovered, please contact the plugin author for h
|
||||
<data name="ChatExport_Initial" xml:space="preserve">
|
||||
<value>Loading logs ...</value>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||
<value>Colorează selectorul de canal cu culoarea canalului</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||
<value>Butonul selector de canal de lângă câmpul de intrare este colorat cu culoarea canalului activ. Se potrivește cu nuanța textului de intrare.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||
<value>Ascunde cât timp meniul New Game+ este deschis</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||
<value>Ascunde chatul cât timp meniul New Game+ este deschis. Închiderea meniului afișează chatul din nou.</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_LatinExtended_Name" xml:space="preserve">
|
||||
<value>Latină extinsă</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
<data name="ExtraGlyphRanges_Greek_Name" xml:space="preserve">
|
||||
<value>Greacă</value>
|
||||
<comment>AI-assisted machine translation. Pending native-speaker review.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user