Compare commits
10 Commits
fe731ff670
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1d71ee33a
|
|||
|
89fa1a999f
|
|||
|
1f6e86ec2d
|
|||
|
d225a3844a
|
|||
|
d4a46910f9
|
|||
|
b59a579f56
|
|||
|
7706ef1fa7
|
|||
|
fca730557e
|
|||
|
6572fdcc27
|
|||
|
bf770f19d9
|
Submodule Glamourer.Api updated: 54c1944dc7...59a7ab5fa9
2
MareAPI
2
MareAPI
Submodule MareAPI updated: fa9b7bce43...0abb078c21
@@ -1,48 +0,0 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace MareSynchronos.Localization;
|
||||
|
||||
public static class LocalizationExtensions
|
||||
{
|
||||
public static string Loc(this string fallbackEnglish, params object[] formatArgs)
|
||||
{
|
||||
var service = LocalizationService.Instance;
|
||||
if (service == null) return FormatFallback(fallbackEnglish, formatArgs);
|
||||
return service.GetString(fallbackEnglish, formatArgs);
|
||||
}
|
||||
|
||||
public static string LocKey(this string key, string fallbackEnglish, params object[] formatArgs)
|
||||
{
|
||||
var service = LocalizationService.Instance;
|
||||
if (service == null) return FormatFallback(fallbackEnglish, formatArgs);
|
||||
return service.GetString(key, fallbackEnglish, formatArgs);
|
||||
}
|
||||
|
||||
public static string LocLabel(this string labelWithId, params object[] formatArgs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(labelWithId)) return labelWithId;
|
||||
|
||||
var separatorIndex = labelWithId.IndexOf("##", StringComparison.Ordinal);
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
return labelWithId.Loc(formatArgs);
|
||||
}
|
||||
|
||||
var label = labelWithId[..separatorIndex];
|
||||
var id = labelWithId[separatorIndex..];
|
||||
return string.Concat(label.Loc(formatArgs), id);
|
||||
}
|
||||
|
||||
private static string FormatFallback(string fallback, params object[] args)
|
||||
{
|
||||
if (args == null || args.Length == 0) return fallback;
|
||||
try
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, fallback, args);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Dalamud.Plugin;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Localization;
|
||||
|
||||
public class LocalizationService
|
||||
{
|
||||
private readonly ILogger<LocalizationService> _logger;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly Dictionary<LocalizationLanguage, Dictionary<string, string>> _translations = new();
|
||||
private readonly HashSet<string> _missingLocalizationsLogged = new(StringComparer.Ordinal);
|
||||
private readonly Lock _missingLocalizationsLock = new();
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
public static LocalizationService? Instance { get; private set; }
|
||||
|
||||
public LocalizationService(ILogger<LocalizationService> logger, IDalamudPluginInterface pluginInterface, MareConfigService configService)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginInterface = pluginInterface;
|
||||
_configService = configService;
|
||||
|
||||
Instance = this;
|
||||
|
||||
foreach (var language in Enum.GetValues<LocalizationLanguage>())
|
||||
{
|
||||
_translations[language] = LoadLanguage(language);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizationLanguage CurrentLanguage => _configService.Current.Language;
|
||||
|
||||
public static IEnumerable<LocalizationLanguage> SupportedLanguages => Enum.GetValues<LocalizationLanguage>();
|
||||
|
||||
public string GetString(string fallbackEnglish, params object[] formatArgs)
|
||||
{
|
||||
return GetStringInternal(null, fallbackEnglish, formatArgs);
|
||||
}
|
||||
|
||||
public string GetString(string key, string fallbackEnglish, params object[] formatArgs)
|
||||
{
|
||||
return GetStringInternal(key, fallbackEnglish, formatArgs);
|
||||
}
|
||||
|
||||
public string GetLanguageDisplayName(LocalizationLanguage language)
|
||||
{
|
||||
var fallback = language switch
|
||||
{
|
||||
LocalizationLanguage.French => "Français",
|
||||
LocalizationLanguage.English => "English",
|
||||
_ => language.ToString()
|
||||
};
|
||||
|
||||
return GetRawString($"Language.DisplayName.{language}", fallback);
|
||||
}
|
||||
|
||||
public void ReloadLanguage(LocalizationLanguage language)
|
||||
{
|
||||
_translations[language] = LoadLanguage(language);
|
||||
}
|
||||
|
||||
public void ReloadAll()
|
||||
{
|
||||
foreach (var language in Enum.GetValues<LocalizationLanguage>())
|
||||
{
|
||||
ReloadLanguage(language);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetStringInternal(string? key, string fallbackEnglish, params object[] formatArgs)
|
||||
{
|
||||
var usedKey = string.IsNullOrWhiteSpace(key) ? fallbackEnglish : key;
|
||||
var text = GetRawString(usedKey, fallbackEnglish);
|
||||
|
||||
if (formatArgs != null && formatArgs.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, text, formatArgs);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Localization format mismatch for key {Key} with text '{Text}'", usedKey, text);
|
||||
try
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, fallbackEnglish, formatArgs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallbackEnglish;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private string GetRawString(string key, string fallbackEnglish)
|
||||
{
|
||||
if (TryGetString(CurrentLanguage, key, out var localized))
|
||||
{
|
||||
return localized;
|
||||
}
|
||||
|
||||
LogMissingLocalization(CurrentLanguage, key, fallbackEnglish);
|
||||
|
||||
if (TryGetString(LocalizationLanguage.English, key, out var english))
|
||||
{
|
||||
return english;
|
||||
}
|
||||
|
||||
if (CurrentLanguage != LocalizationLanguage.English)
|
||||
{
|
||||
LogMissingLocalization(LocalizationLanguage.English, key, fallbackEnglish);
|
||||
}
|
||||
|
||||
return fallbackEnglish;
|
||||
}
|
||||
|
||||
private bool TryGetString(LocalizationLanguage language, string key, out string value)
|
||||
{
|
||||
var dictionary = GetDictionary(language);
|
||||
if (dictionary.TryGetValue(key, out var text) && !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
value = text;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> GetDictionary(LocalizationLanguage language)
|
||||
{
|
||||
if (!_translations.TryGetValue(language, out var dictionary))
|
||||
{
|
||||
dictionary = LoadLanguage(language);
|
||||
_translations[language] = dictionary;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> LoadLanguage(LocalizationLanguage language)
|
||||
{
|
||||
var baseDirectory = _pluginInterface.AssemblyLocation.DirectoryName ?? string.Empty;
|
||||
var localizationDirectory = Path.Combine(baseDirectory, "Localization");
|
||||
var languageCode = GetLanguageCode(language);
|
||||
var filePath = Path.Combine(localizationDirectory, $"{languageCode}.json");
|
||||
|
||||
Dictionary<string, string>? translations = null;
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(filePath);
|
||||
translations = DeserializeTranslations(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load localization data from {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (translations == null)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"{assembly.GetName().Name}.Localization.{languageCode}.json";
|
||||
using var resourceStream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (resourceStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
translations = DeserializeTranslations(resourceStream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load embedded localization resource {Resource}", resourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (translations == null)
|
||||
{
|
||||
_logger.LogDebug("Localization data for {Language} not found on disk or embedded, using empty dictionary.", language);
|
||||
translations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
private void LogMissingLocalization(LocalizationLanguage language, string key, string fallback)
|
||||
{
|
||||
var marker = $"{language}:{key}";
|
||||
using var scope = _missingLocalizationsLock.EnterScope();
|
||||
if (_missingLocalizationsLogged.Contains(marker)) return;
|
||||
_missingLocalizationsLogged.Add(marker);
|
||||
|
||||
_logger.LogDebug("Missing localization for {Language} ({Key}). Using fallback '{Fallback}'.", language, key, fallback);
|
||||
}
|
||||
|
||||
private static string GetLanguageCode(LocalizationLanguage language) => language switch
|
||||
{
|
||||
LocalizationLanguage.French => "fr",
|
||||
LocalizationLanguage.English => "en",
|
||||
_ => language.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
private static Dictionary<string, string>? DeserializeTranslations(Stream stream)
|
||||
{
|
||||
var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(stream, JsonOptions);
|
||||
return translations != null
|
||||
? new Dictionary<string, string>(translations, StringComparer.OrdinalIgnoreCase)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
{
|
||||
"Language.DisplayName.French": "French",
|
||||
"Language.DisplayName.English": "English",
|
||||
"Settings.Plugins.MandatoryHeading": "Mandatory Plugins",
|
||||
"Settings.Plugins.MandatoryLabel": "Mandatory Plugins:",
|
||||
"Settings.Plugins.OptionalHeading": "Optional Addons",
|
||||
"Settings.Plugins.OptionalLabel": "Optional Addons:",
|
||||
"Settings.Plugins.OptionalDescription": "These addons are not required for basic operation, but without them you may not see others as intended.",
|
||||
"Settings.Plugins.Tooltip.Available": "{0} is available and up to date.",
|
||||
"Settings.Plugins.Tooltip.Unavailable": "{0} is unavailable or not up to date.",
|
||||
"Settings.Plugins.WarningMandatoryMissing": "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra.",
|
||||
"Settings.General.LocalizationHeading": "Localization",
|
||||
"Settings.General.Language": "Language",
|
||||
"Settings.General.Language.Description": "Select the plugin language. Any missing translations will be shown in English.",
|
||||
"Settings.General.NotesHeading": "Notes",
|
||||
"Settings.General.Notes.Export": "Export all your user notes to clipboard",
|
||||
"Settings.General.Notes.Import": "Import notes from clipboard",
|
||||
"Settings.General.Notes.Overwrite": "Overwrite existing notes",
|
||||
"Settings.General.Notes.Overwrite.Description": "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes.",
|
||||
"Settings.General.Notes.Import.Success": "User Notes successfully imported",
|
||||
"Settings.General.Notes.Import.Failure": "Attempt to import notes from clipboard failed. Check formatting and try again",
|
||||
"Settings.General.Notes.OpenPopup": "Open Notes Popup on user addition",
|
||||
"Settings.General.Notes.OpenPopup.Description": "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs.",
|
||||
"Settings.Transfers.Blocked.Description": "Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. Ask your paired friend to send you the mod in question through other means or acquire the mod yourself.",
|
||||
"Settings.Transfers.Blocked.Column.Hash": "Hash/Filename",
|
||||
"Settings.Transfers.Blocked.Column.ForbiddenBy": "Forbidden by",
|
||||
"Settings.Transfers.Blocked.Tab": "Blocked Transfers",
|
||||
"Settings.Transfers.Heading": "Transfer Settings",
|
||||
"Settings.Transfers.GlobalLimit.Label": "Global Download Speed Limit",
|
||||
"Settings.Transfers.GlobalLimit.Unit.BytePerSec": "Byte/s",
|
||||
"Settings.Transfers.GlobalLimit.Unit.KiloBytePerSec": "KB/s",
|
||||
"Settings.Transfers.GlobalLimit.Unit.MegaBytePerSec": "MB/s",
|
||||
"Settings.Transfers.GlobalLimit.Hint": "0 = No limit/infinite",
|
||||
"Settings.Transfers.MaxParallelDownloads": "Maximum Parallel Downloads",
|
||||
"Settings.Transfers.AutoDetect.Heading": "AutoDetect",
|
||||
"Settings.Transfers.AutoDetect.EnableNearby": "Enable Nearby detection (beta)",
|
||||
"Settings.Transfers.AutoDetect.AllowRequests": "Allow pair requests",
|
||||
"Settings.Transfers.AutoDetect.Notification.Title": "Nearby Detection",
|
||||
"Settings.Transfers.AutoDetect.Notification.Enabled": "Pair requests enabled: others can invite you.",
|
||||
"Settings.Transfers.AutoDetect.Notification.Disabled": "Pair requests disabled: others cannot invite you.",
|
||||
"Settings.Transfers.AutoDetect.MaxDistance": "Max distance (meters)",
|
||||
"Settings.Transfers.UI.Heading": "Transfer UI",
|
||||
"Settings.Transfers.UI.ShowWindow": "Show separate transfer window",
|
||||
"Settings.Transfers.UI.ShowWindow.Description": "The download window will show the current progress of outstanding downloads.\n\nWhat do W/Q/P/D stand for?\nW = Waiting for Slot (see Maximum Parallel Downloads)\nQ = Queued on Server, waiting for queue ready signal\nP = Processing download (aka downloading)\nD = Decompressing download",
|
||||
"Settings.Transfers.UI.EditWindowPosition": "Edit Transfer Window position",
|
||||
"Settings.Transfers.UI.ShowTransferBars": "Show transfer bars rendered below players",
|
||||
"Settings.Transfers.UI.ShowTransferBars.Description": "This will render a progress bar during the download at the feet of the player you are downloading from.",
|
||||
"Settings.Transfers.UI.ShowDownloadText": "Show Download Text",
|
||||
"Settings.Transfers.UI.ShowDownloadText.Description": "Shows download text (amount of MiB downloaded) in the transfer bars",
|
||||
"Settings.Transfers.UI.BarWidth": "Transfer Bar Width",
|
||||
"Settings.Transfers.UI.BarWidth.Description": "Width of the displayed transfer bars (will never be less wide than the displayed text)",
|
||||
"Settings.Transfers.UI.BarHeight": "Transfer Bar Height",
|
||||
"Settings.Transfers.UI.BarHeight.Description": "Height of the displayed transfer bars (will never be less tall than the displayed text)",
|
||||
"Settings.Transfers.UI.ShowUploading": "Show 'Uploading' text below players that are currently uploading",
|
||||
"Settings.Transfers.UI.ShowUploading.Description": "This will render an 'Uploading' text at the feet of the player that is in progress of uploading data.",
|
||||
"Settings.Transfers.UI.ShowUploadingBigText": "Large font for 'Uploading' text",
|
||||
"Settings.Transfers.UI.ShowUploadingBigText.Description": "This will render an 'Uploading' text in a larger font.",
|
||||
"Settings.Transfers.Current.Heading": "Current Transfers",
|
||||
"Settings.Transfers.Current.Tab": "Transfers",
|
||||
"Settings.Transfers.Current.Uploads": "Uploads",
|
||||
"Settings.Transfers.Current.Uploads.Column.File": "File",
|
||||
"Settings.Transfers.Current.Uploads.Column.Uploaded": "Uploaded",
|
||||
"Settings.Transfers.Current.Uploads.Column.Size": "Size",
|
||||
"Settings.Transfers.Current.Downloads": "Downloads",
|
||||
"Settings.Transfers.Current.Downloads.Column.User": "User",
|
||||
"Settings.Transfers.Current.Downloads.Column.Server": "Server",
|
||||
"Settings.Transfers.Current.Downloads.Column.Files": "Files",
|
||||
"Settings.Transfers.Current.Downloads.Column.Download": "Download",
|
||||
"Settings.Storage.Heading": "Storage",
|
||||
"Settings.Storage.Description": "Umbra stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage.",
|
||||
"Settings.Service.ActionsHeading": "Service Actions",
|
||||
"Settings.Service.Actions.DeleteAccount": "Delete account",
|
||||
"Settings.Service.Actions.DeleteAccountPopup": "Delete your account?",
|
||||
"Settings.Service.Actions.DeleteAccount.Description": "Completely deletes your currently connected account.",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Body1": "Your account and all associated files and data on the service will be deleted.",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Body2": "Your UID will be removed from all pairing lists.",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Confirm": "Are you sure you want to continue?",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Cancel": "Cancel",
|
||||
"Settings.Service.SettingsHeading": "Service & Character Settings",
|
||||
"Settings.Service.ReconnectWarning": "For any changes to be applied to the current service you need to reconnect to the service.",
|
||||
"Settings.Service.Tabs.CharacterAssignments": "Character Assignments",
|
||||
"Settings.Service.Tabs.SecretKey": "Secret Key Management",
|
||||
"Settings.Service.Tabs.ServiceSettings": "Service Settings",
|
||||
"Settings.Service.Character.Assignments.Description": "Characters listed here will connect with the specified secret key.",
|
||||
"Settings.Service.Character.Assignments.TooltipCurrent": "Current character",
|
||||
"Settings.Service.Character.Assignments.DeleteTooltip": "Delete character assignment",
|
||||
"Settings.Service.Character.Assignments.AddCurrent": "Add current character",
|
||||
"Settings.Service.Character.Assignments.NoKeys": "You need to add a Secret Key first before adding Characters.",
|
||||
"Settings.Service.SecretKey.DisplayName": "Secret Key Display Name",
|
||||
"Settings.Service.SecretKey.Value": "Secret Key",
|
||||
"Settings.Service.SecretKey.AssignCurrent": "Assign current character",
|
||||
"Settings.Service.SecretKey.AssignTooltip": "Use this secret key for {0} @ {1}",
|
||||
"Settings.Service.SecretKey.Delete": "Delete Secret Key",
|
||||
"Settings.Service.SecretKey.DeleteTooltip": "Hold CTRL to delete this secret key entry",
|
||||
"Settings.Service.SecretKey.InUse": "This key is currently assigned to a character and cannot be edited or deleted.",
|
||||
"Settings.Service.SecretKey.Add": "Add new Secret Key",
|
||||
"Settings.Service.SecretKey.NewFriendlyName": "New Secret Key",
|
||||
"Settings.Service.SecretKey.RegisterAccount": "Register a new Umbra account",
|
||||
"Settings.Service.SecretKey.RegisterFailed": "An unknown error occured. Please try again later.",
|
||||
"Settings.Service.SecretKey.RegisterSuccess": "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC.",
|
||||
"Settings.Service.SecretKey.RegisteredFriendlyName": "{0} (registered {1})",
|
||||
"Settings.Service.SecretKey.Registering": "Sending request...",
|
||||
"Settings.Service.ServiceTab.Uri": "Service URI",
|
||||
"Settings.Service.ServiceTab.UriReadOnlyHint": "You cannot edit the URI of the main service.",
|
||||
"Settings.Service.ServiceTab.Name": "Service Name",
|
||||
"Settings.Service.ServiceTab.NameReadOnlyHint": "You cannot edit the name of the main service.",
|
||||
"Settings.Service.ServiceTab.Delete": "Delete Service",
|
||||
"Settings.Service.ServiceTab.DeleteHint": "Hold CTRL to delete this service",
|
||||
"Settings.Advanced.Heading": "Advanced",
|
||||
"Settings.Advanced.Tab": "Advanced",
|
||||
"Settings.Advanced.Api.Enable": "Enable Umbra Sync API",
|
||||
"Settings.Advanced.Api.Description": "Enables handling of the Umbra Sync API. This currently includes:\n\n - MCDF loading support for other plugins\n - Blocking Moodles applications to paired users\n\nIf the Umbra Sync plugin is loaded while this option is enabled, control of its API will be relinquished.",
|
||||
"Settings.Advanced.Api.Status.Active": "Umbra API active!",
|
||||
"Settings.Advanced.Api.Status.Disabled": "Umbra API inactive: Option is disabled",
|
||||
"Settings.Advanced.Api.Status.PluginLoaded": "Umbra API inactive: Umbra plugin is loaded",
|
||||
"Settings.Advanced.Api.Status.Unknown": "Umbra API inactive: Unknown reason",
|
||||
"Settings.Advanced.EventViewer.LogToDisk": "Log Event Viewer data to disk",
|
||||
"Settings.Advanced.EventViewer.Open": "Open Event Viewer",
|
||||
"Settings.Advanced.HoldCombat": "Hold application during combat",
|
||||
"Settings.Advanced.SerializedApplications": "Serialized player applications",
|
||||
"Settings.Advanced.SerializedApplications.Description": "Experimental - May reduce issues in crowded areas",
|
||||
"Settings.Advanced.DebugHeading": "Debug",
|
||||
"Settings.Advanced.Debug.LastCreatedTree": "Last created character data",
|
||||
"Settings.Advanced.Debug.CopyButton": "[DEBUG] Copy Last created Character Data to clipboard",
|
||||
"Settings.Advanced.Debug.CopyError": "ERROR: No created character data, cannot copy.",
|
||||
"Settings.Advanced.Debug.CopyTooltip": "Use this when reporting mods being rejected from the server.",
|
||||
"Settings.Advanced.LogLevel": "Log Level",
|
||||
"Settings.Advanced.Performance.LogCounters": "Log Performance Counters",
|
||||
"Settings.Advanced.Performance.LogCounters.Description": "Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended.",
|
||||
"Settings.Advanced.Performance.PrintStats": "Print Performance Stats to /xllog",
|
||||
"Settings.Advanced.Performance.PrintStatsRecent": "Print Performance Stats (last 60s) to /xllog",
|
||||
"Settings.Advanced.ActiveBlocks": "Active Character Blocks",
|
||||
"Settings.UI.Heading": "UI",
|
||||
"Settings.UI.EnableRightClick": "Enable Game Right Click Menu Entries",
|
||||
"Settings.UI.EnableRightClick.Description": "This will add Umbra related right click menu entries in the game UI on paired players.",
|
||||
"Settings.UI.EnableDtrEntry": "Display status and visible pair count in Server Info Bar",
|
||||
"Settings.UI.EnableDtrEntry.Description": "This will add Umbra connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings.",
|
||||
"Settings.UI.Dtr.ShowUid": "Show visible character's UID in tooltip",
|
||||
"Settings.UI.Dtr.PreferNotes": "Prefer notes over player names in tooltip",
|
||||
"Settings.UI.Dtr.UseColors": "Color-code the Server Info Bar entry according to status",
|
||||
"Settings.UI.Dtr.ColorDefault": "Default",
|
||||
"Settings.UI.Dtr.ColorNotConnected": "Not Connected",
|
||||
"Settings.UI.Dtr.ColorPairsInRange": "Pairs in Range",
|
||||
"Settings.UI.NameColors.Enable": "Color nameplates of paired players",
|
||||
"Settings.UI.NameColors.Character": "Character Name Color",
|
||||
"Settings.UI.NameColors.Blocked": "Blocked Character Color",
|
||||
"Settings.UI.VisibleGroup": "Show separate Visible group",
|
||||
"Settings.UI.VisibleGroup.Description": "This will show all currently visible users in a special 'Visible' group in the main UI.",
|
||||
"Settings.UI.OfflineGroup": "Show separate Offline group",
|
||||
"Settings.UI.OfflineGroup.Description": "This will show all currently offline users in a special 'Offline' group in the main UI.",
|
||||
"Settings.UI.ShowPlayerNames": "Show player names",
|
||||
"Settings.UI.ShowPlayerNames.Description": "This will show character names instead of UIDs when possible",
|
||||
"Settings.UI.Profiles.Show": "Show Profiles on Hover",
|
||||
"Settings.UI.Profiles.Show.Description": "This will show the configured user profile after a set delay",
|
||||
"Settings.UI.Profiles.PopoutRight": "Popout profiles on the right",
|
||||
"Settings.UI.Profiles.PopoutRight.Description": "Will show profiles on the right side of the main UI",
|
||||
"Settings.UI.Profiles.HoverDelay": "Hover Delay",
|
||||
"Settings.UI.Profiles.HoverDelay.Description": "Delay until the profile should be displayed",
|
||||
"Settings.UI.Profiles.ShowNsfw": "Show profiles marked as NSFW",
|
||||
"Settings.UI.Profiles.ShowNsfw.Description": "Will show profiles that have the NSFW tag enabled",
|
||||
"Settings.Notifications.Heading": "Notifications",
|
||||
"Settings.Notifications.InfoDisplay": "Info Notification Display",
|
||||
"Settings.Notifications.InfoDisplay.Description": "The location where \"Info\" notifications will display.\n'Nowhere' will not show any Info notifications\n'Chat' will print Info notifications in chat\n'Toast' will show Warning toast notifications in the bottom right corner\n'Both' will show chat as well as the toast notification",
|
||||
"Settings.Notifications.WarningDisplay": "Warning Notification Display",
|
||||
"Settings.Notifications.WarningDisplay.Description": "The location where \"Warning\" notifications will display.\n'Nowhere' will not show any Warning notifications\n'Chat' will print Warning notifications in chat\n'Toast' will show Warning toast notifications in the bottom right corner\n'Both' will show chat as well as the toast notification",
|
||||
"Settings.Notifications.ErrorDisplay": "Error Notification Display",
|
||||
"Settings.Notifications.ErrorDisplay.Description": "The location where \"Error\" notifications will display.\n'Nowhere' will not show any Error notifications\n'Chat' will print Error notifications in chat\n'Toast' will show Error toast notifications in the bottom right corner\n'Both' will show chat as well as the toast notification",
|
||||
"Settings.Notifications.Location.Nowhere": "Nowhere",
|
||||
"Settings.Notifications.Location.Chat": "Chat",
|
||||
"Settings.Notifications.Location.Toast": "Toast",
|
||||
"Settings.Notifications.Location.Both": "Both",
|
||||
"Settings.Notifications.DisableOptionalWarnings": "Disable optional plugin warnings",
|
||||
"Settings.Notifications.DisableOptionalWarnings.Description": "Enabling this will not show any \"Warning\" labeled messages for missing optional plugins.",
|
||||
"Settings.Notifications.EnableOnlineNotifications": "Enable online notifications",
|
||||
"Settings.Notifications.EnableOnlineNotifications.Description": "Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online.",
|
||||
"Settings.Notifications.IndividualPairsOnly": "Notify only for individual pairs",
|
||||
"Settings.Notifications.IndividualPairsOnly.Description": "Enabling this will only show online notifications (type: Info) for individual pairs.",
|
||||
"Settings.Notifications.NamedPairsOnly": "Notify only for named pairs",
|
||||
"Settings.Notifications.NamedPairsOnly.Description": "Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note.",
|
||||
"Compact.Version.UnsupportedTitle": "UNSUPPORTED VERSION",
|
||||
"Compact.Version.Outdated": "Your UmbraSync installation is out of date, the current version is {0}.{1}.{2}. It is highly recommended to keep UmbraSync up to date. Open /xlplugins and update the plugin.",
|
||||
"Compact.Toggle.IndividualPairs": "Individual pairs",
|
||||
"Compact.Toggle.Syncshells": "Syncshells",
|
||||
"Compact.AddUser.ModalTitle": "Set Notes for New User",
|
||||
"Compact.AddUser.Description": "You have successfully added {0}. Set a local note for the user in the field below:",
|
||||
"Compact.AddUser.NoteHint": "Note for {0}",
|
||||
"Compact.AddUser.Save": "Save Note",
|
||||
"Compact.AddCharacter.Button": "Add current character with secret key",
|
||||
"Compact.AddCharacter.SecretKeyLabel": "Secret Key",
|
||||
"Compact.AddCharacter.NoKeys": "No secret keys are configured for the current server.",
|
||||
"Compact.AddPair.Hint": "Other player's UID/Alias",
|
||||
"Compact.AddPair.Tooltip": "Pair with {0}",
|
||||
"Compact.AddPair.Tooltip.DefaultUser": "other user",
|
||||
"Compact.Filter.Hint": "Filter for UID/notes",
|
||||
"Compact.Filter.ToggleTooltip": "Hold Control to {0} pairing with {1} out of {2} displayed users.",
|
||||
"Compact.Filter.ToggleTooltip.Resume": "resume",
|
||||
"Compact.Filter.ToggleTooltip.Pause": "pause",
|
||||
"Compact.Filter.CooldownTooltip": "Next execution is available at {0} seconds",
|
||||
"Compact.Nearby.Title": "Nearby ({0})",
|
||||
"Compact.Nearby.Button": "Nearby",
|
||||
"Compact.Nearby.None": "No nearby players detected.",
|
||||
"Compact.Nearby.Tooltip.AlreadyPaired": "Already paired on Umbra",
|
||||
"Compact.Nearby.Tooltip.RequestsDisabled": "Pair requests are disabled for this player",
|
||||
"Compact.Nearby.Tooltip.SendInvite": "Send Umbra invitation",
|
||||
"Compact.Nearby.Tooltip.CannotInvite": "Unable to invite this player",
|
||||
"Compact.Nearby.Incoming": "Incoming requests",
|
||||
"Compact.Nearby.Incoming.Entry": "{0} [{1}]",
|
||||
"Compact.Nearby.Incoming.Accept": "Accept and add as pair",
|
||||
"Compact.Nearby.Incoming.Dismiss": "Dismiss request",
|
||||
"Compact.Header.SettingsTooltip": "Open the UmbraSync settings",
|
||||
"Compact.Header.CopyUid": "Copy your UID to clipboard",
|
||||
"Compact.ServerStatus.UsersOnline": "Users Online",
|
||||
"Compact.ServerStatus.Shard": "Shard: {0}",
|
||||
"Compact.ServerStatus.NotConnected": "Not connected to any server",
|
||||
"Compact.ServerStatus.EditProfile": "Edit your Profile",
|
||||
"Compact.ServerStatus.Disconnect": "Disconnect from {0}",
|
||||
"Compact.ServerStatus.Connect": "Connect to {0}",
|
||||
"Compact.ServerError.Connecting": "Attempting to connect to the server.",
|
||||
"Compact.ServerError.Reconnecting": "Connection to server interrupted, attempting to reconnect to the server.",
|
||||
"Compact.ServerError.Disconnected": "You are currently disconnected from the sync server.",
|
||||
"Compact.ServerError.Disconnecting": "Disconnecting from the server",
|
||||
"Compact.ServerError.Unauthorized": "Server Response: {0}",
|
||||
"Compact.ServerError.Offline": "Your selected sync server is currently offline.",
|
||||
"Compact.ServerError.VersionMismatch": "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.",
|
||||
"Compact.ServerError.RateLimited": "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.",
|
||||
"Compact.ServerError.NoSecretKey": "You have no secret key set for this current character. Use the button below or open the settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.",
|
||||
"Compact.ServerError.MultiChara": "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.",
|
||||
"Compact.Transfers.CharacterAnalysis": "Character Analysis",
|
||||
"Compact.Transfers.CharacterDataHub": "Character Data Hub",
|
||||
"Compact.UidText.Reconnecting": "Reconnecting",
|
||||
"Compact.UidText.Connecting": "Connecting",
|
||||
"Compact.UidText.Disconnected": "Disconnected",
|
||||
"Compact.UidText.Disconnecting": "Disconnecting",
|
||||
"Compact.UidText.Unauthorized": "Unauthorized",
|
||||
"Compact.UidText.VersionMismatch": "Version mismatch",
|
||||
"Compact.UidText.Offline": "Unavailable",
|
||||
"Compact.UidText.RateLimited": "Rate Limited",
|
||||
"Compact.UidText.NoSecretKey": "No Secret Key",
|
||||
"Compact.UidText.MultiChara": "Duplicate Characters",
|
||||
"UserPair.Status.Online": "User is online",
|
||||
"UserPair.Status.Offline": "User is offline",
|
||||
"UserPair.Tooltip.NotAddedBack": "{0} has not added you back",
|
||||
"UserPair.Tooltip.Paused": "Pairing with {0} is paused",
|
||||
"UserPair.Tooltip.Visible": "{0} is visible: {1}\nClick to target this player",
|
||||
"UserPair.Tooltip.Visible.LastPrefix": "(Last) ",
|
||||
"UserPair.Tooltip.Visible.ModsInfo": "Mods Info",
|
||||
"UserPair.Tooltip.Visible.FilesSize": "Files Size: {0}",
|
||||
"UserPair.Tooltip.Visible.Vram": "Approx. VRAM Usage: {0}",
|
||||
"UserPair.Tooltip.Visible.Tris": "Triangle Count (excl. Vanilla): {0}",
|
||||
"UserPair.Tooltip.Pause": "Pause pairing with {0}",
|
||||
"UserPair.Tooltip.Resume": "Resume pairing with {0}",
|
||||
"UserPair.Tooltip.Permission.Header": "Individual user permissions",
|
||||
"UserPair.Tooltip.Permission.Sound": "Sound sync disabled with {0}",
|
||||
"UserPair.Tooltip.Permission.Animation": "Animation sync disabled with {0}",
|
||||
"UserPair.Tooltip.Permission.Vfx": "VFX sync disabled with {0}",
|
||||
"UserPair.Tooltip.Permission.Status": "You: {0}, They: {1}",
|
||||
"UserPair.Tooltip.Permission.State.Disabled": "Disabled",
|
||||
"UserPair.Tooltip.Permission.State.Enabled": "Enabled",
|
||||
"UserPair.Tooltip.SharedData": "This user has shared {0} Character Data Sets with you.",
|
||||
"UserPair.Tooltip.SharedData.OpenHub": "Click to open the Character Data Hub and show the entries.",
|
||||
"UserPair.Menu.Target": "Target player",
|
||||
"UserPair.Menu.OpenProfile": "Open Profile",
|
||||
"UserPair.Menu.OpenProfile.Tooltip": "Opens the profile for this user in a new window",
|
||||
"UserPair.Menu.OpenAnalysis": "Open Analysis",
|
||||
"UserPair.Menu.ReloadData": "Reload last data",
|
||||
"UserPair.Menu.ReloadData.Tooltip": "This reapplies the last received character data to this character",
|
||||
"UserPair.Menu.CyclePause": "Cycle pause state",
|
||||
"UserPair.Menu.PairGroups": "Pair Groups",
|
||||
"UserPair.Menu.PairGroups.Tooltip": "Choose pair groups for {0}",
|
||||
"UserPair.Menu.EnableSoundSync": "Enable sound sync",
|
||||
"UserPair.Menu.DisableSoundSync": "Disable sound sync",
|
||||
"UserPair.Menu.EnableAnimationSync": "Enable animation sync",
|
||||
"UserPair.Menu.DisableAnimationSync": "Disable animation sync",
|
||||
"UserPair.Menu.EnableVfxSync": "Enable VFX sync",
|
||||
"UserPair.Menu.DisableVfxSync": "Disable VFX sync",
|
||||
"UserPair.Menu.Unpair": "Unpair Permanently",
|
||||
"UserPair.Menu.Unpair.Tooltip": "Hold CTRL and click to unpair permanently from {0}",
|
||||
"Popup.Generic.Close": "Close",
|
||||
"Popup.BanUser.Description": "User {0} will be banned and removed from this Syncshell.",
|
||||
"Popup.BanUser.ReasonHint": "Ban Reason",
|
||||
"Popup.BanUser.Button": "Ban User",
|
||||
"Popup.BanUser.ReasonNote": "The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason.",
|
||||
"Popup.Report.Title": "Report {0} Profile",
|
||||
"Popup.Report.Note": "Note: Sending a report will disable the offending profile globally.\nThe report will be sent to the team of your currently connected server.\nDepending on the severity of the offense the users profile or account can be permanently disabled or banned.",
|
||||
"Popup.Report.Warning": "Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.",
|
||||
"Popup.Report.Scope": "This is not for reporting misbehavior but solely for the actual profile. Reports that are not solely for the profile will be ignored.",
|
||||
"Popup.Report.Button": "Send Report",
|
||||
"PairGroups.Popup.Title": "Choose Groups for {0}",
|
||||
"PairGroups.Popup.SelectPrompt": "Select the groups you want {0} to be in.",
|
||||
"PairGroups.Popup.CreatePrompt": "Create a new group for {0}.",
|
||||
"PairGroups.Popup.NewGroupHint": "New Group",
|
||||
"PairGroups.SelectPairs.Title": "Choose Users for Group {0}",
|
||||
"PairGroups.SelectPairs.SelectPrompt": "Select users for group {0}",
|
||||
"PairGroups.SelectPairs.FilterHint": "Filter",
|
||||
"UidDisplay.Tooltip": "Left click to switch between UID display and nick\nRight click to change nick for {0}\nMiddle Mouse Button to open their profile in a separate window",
|
||||
"UidDisplay.EditNotes.Hint": "Nick/Notes",
|
||||
"DataAnalysis.WindowTitle": "Character Data Analysis",
|
||||
"DataAnalysis.Bc7.ModalTitle": "BC7 Conversion in Progress",
|
||||
"DataAnalysis.Bc7.Status": "BC7 Conversion in progress: {0}/{1}",
|
||||
"DataAnalysis.Bc7.CurrentFile": "Current file: {0}",
|
||||
"DataAnalysis.Bc7.Cancel": "Cancel conversion",
|
||||
"DataAnalysis.Description": "This window shows you all files and their sizes that are currently in use through your character and associated entities",
|
||||
"DataAnalysis.Analyzing": "Analyzing {0}/{1}",
|
||||
"DataAnalysis.Button.CancelAnalysis": "Cancel analysis",
|
||||
"DataAnalysis.Analyze.MissingNotice": "Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
|
||||
"DataAnalysis.Button.StartMissing": "Start analysis (missing entries)",
|
||||
"DataAnalysis.Button.StartAll": "Start analysis (recalculate all entries)",
|
||||
"DataAnalysis.TotalFiles": "Total files:",
|
||||
"DataAnalysis.Tooltip.FileSummary": "{0}: {1} files, size: {2}, compressed: {3}",
|
||||
"DataAnalysis.TotalSizeActual": "Total size (actual):",
|
||||
"DataAnalysis.TotalSizeDownload": "Total size (download size):",
|
||||
"DataAnalysis.Tooltip.CalculateDownloadSize": "Click \"Start analysis\" to calculate download size",
|
||||
"DataAnalysis.TotalTriangles": "Total modded model triangles: {0}",
|
||||
"DataAnalysis.FilesFor": "Files for {0}",
|
||||
"DataAnalysis.Object.SizeActual": "{0} size (actual):",
|
||||
"DataAnalysis.Object.SizeDownload": "{0} size (download size):",
|
||||
"DataAnalysis.Object.Vram": "{0} VRAM usage:",
|
||||
"DataAnalysis.Object.Triangles": "{0} modded model triangles: {1}",
|
||||
"DataAnalysis.FileGroup.Count": "{0} files",
|
||||
"DataAnalysis.FileGroup.SizeActual": "{0} files size (actual):",
|
||||
"DataAnalysis.FileGroup.SizeDownload": "{0} files size (download size):",
|
||||
"DataAnalysis.Bc7.EnableMode": "Enable BC7 Conversion Mode",
|
||||
"DataAnalysis.Bc7.WarningTitle": "WARNING BC7 CONVERSION:",
|
||||
"DataAnalysis.Bc7.WarningIrreversible": "Converting textures to BC7 is irreversible!",
|
||||
"DataAnalysis.Bc7.WarningDetails": "- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures.\n- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts.\n- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues.\n- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically.\n- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete.",
|
||||
"DataAnalysis.Bc7.StartConversion": "Start conversion of {0} texture(s)",
|
||||
"DataAnalysis.Table.Hash": "Hash",
|
||||
"DataAnalysis.Table.Filepaths": "Filepaths",
|
||||
"DataAnalysis.Table.Gamepaths": "Gamepaths",
|
||||
"DataAnalysis.Table.FileSize": "File Size",
|
||||
"DataAnalysis.Table.DownloadSize": "Download Size",
|
||||
"DataAnalysis.Table.Format": "Format",
|
||||
"DataAnalysis.Table.ConvertToBc7": "Convert to BC7",
|
||||
"DataAnalysis.Table.Triangles": "Triangles",
|
||||
"DataAnalysis.SelectedFile": "Selected file:",
|
||||
"DataAnalysis.LocalFilePath": "Local file path:",
|
||||
"DataAnalysis.MoreCount": "(and {0} more)",
|
||||
"DataAnalysis.GamePath": "Used by game path:",
|
||||
"DownloadUi.WindowTitle": "Umbra Downloads",
|
||||
"DownloadUi.UploadStatus": "Compressing+Uploading {0}/{1}",
|
||||
"DownloadUi.DownloadStatus": "{0} [W:{1}/Q:{2}/P:{3}/D:{4}]",
|
||||
"DownloadUi.UploadingLabel": "Uploading",
|
||||
"EventViewer.WindowTitle": "Event Viewer",
|
||||
"EventViewer.Button.Unfreeze": "Unfreeze View",
|
||||
"EventViewer.Button.Freeze": "Freeze View",
|
||||
"EventViewer.Tooltip.NewEvents": "New events are available. Click to resume updating.",
|
||||
"EventViewer.FilterLabel": "Filter lines",
|
||||
"EventViewer.Button.OpenLog": "Open EventLog folder",
|
||||
"EventViewer.Column.Time": "Time",
|
||||
"EventViewer.Column.Source": "Source",
|
||||
"EventViewer.Column.Uid": "UID",
|
||||
"EventViewer.Column.Character": "Character",
|
||||
"EventViewer.Column.Event": "Event",
|
||||
"EventViewer.Severity.Informational": "Informational",
|
||||
"EventViewer.Severity.Warning": "Warning",
|
||||
"EventViewer.Severity.Error": "Error",
|
||||
"EventViewer.NoValue": "--",
|
||||
"DtrEntry.EntryName": "Umbra",
|
||||
"DtrEntry.Tooltip.Connected": "Umbra: Connected",
|
||||
"DtrEntry.Tooltip.Disconnected": "Umbra: Not Connected",
|
||||
"PermissionWindow.Title": "Permissions for {0}",
|
||||
"PermissionWindow.Pause.Label": "Pause Sync",
|
||||
"PermissionWindow.Pause.HelpMain": "Pausing will completely cease any sync with this user.",
|
||||
"PermissionWindow.Pause.HelpNote": "Note: this is bidirectional, either user pausing will cease sync completely.",
|
||||
"PermissionWindow.OtherPaused.True": "{0} has paused you",
|
||||
"PermissionWindow.OtherPaused.False": "{0} has not paused you",
|
||||
"PermissionWindow.Sounds.Label": "Disable Sounds",
|
||||
"PermissionWindow.Sounds.HelpMain": "Disabling sounds will remove all sounds synced with this user on both sides.",
|
||||
"PermissionWindow.Sounds.HelpNote": "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides.",
|
||||
"PermissionWindow.OtherSoundDisabled.True": "{0} has disabled sound sync with you",
|
||||
"PermissionWindow.OtherSoundDisabled.False": "{0} has not disabled sound sync with you",
|
||||
"PermissionWindow.Animations.Label": "Disable Animations",
|
||||
"PermissionWindow.Animations.HelpMain": "Disabling sounds will remove all animations synced with this user on both sides.",
|
||||
"PermissionWindow.Animations.HelpNote": "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides.",
|
||||
"PermissionWindow.OtherAnimationDisabled.True": "{0} has disabled animation sync with you",
|
||||
"PermissionWindow.OtherAnimationDisabled.False": "{0} has not disabled animation sync with you",
|
||||
"PermissionWindow.Vfx.Label": "Disable VFX",
|
||||
"PermissionWindow.Vfx.HelpMain": "Disabling sounds will remove all VFX synced with this user on both sides.",
|
||||
"PermissionWindow.Vfx.HelpNote": "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides.",
|
||||
"PermissionWindow.OtherVfxDisabled.True": "{0} has disabled VFX sync with you",
|
||||
"PermissionWindow.OtherVfxDisabled.False": "{0} has not disabled VFX sync with you",
|
||||
"PermissionWindow.Button.Save": "Save",
|
||||
"PermissionWindow.Tooltip.Save": "Save and apply all changes",
|
||||
"PermissionWindow.Button.Revert": "Revert",
|
||||
"PermissionWindow.Tooltip.Revert": "Revert all changes",
|
||||
"PermissionWindow.Button.Reset": "Reset to Default",
|
||||
"PermissionWindow.Tooltip.Reset": "This will set all permissions to their default setting",
|
||||
"EditProfile.WindowTitle": "Umbra Edit Profile",
|
||||
"EditProfile.CurrentProfile": "Current Profile (as saved on server)",
|
||||
"EditProfile.Button.UploadPicture": "Upload new profile picture",
|
||||
"EditProfile.Dialog.PictureTitle": "Select new Profile picture",
|
||||
"EditProfile.Tooltip.UploadPicture": "Select and upload a new profile picture",
|
||||
"EditProfile.Button.ClearPicture": "Clear uploaded profile picture",
|
||||
"EditProfile.Tooltip.ClearPicture": "Clear your currently uploaded profile picture",
|
||||
"EditProfile.Error.PictureTooLarge": "The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size",
|
||||
"EditProfile.Checkbox.Nsfw": "Profile is NSFW",
|
||||
"EditProfile.Help.Nsfw": "If your profile description or image can be considered NSFW, toggle this to ON",
|
||||
"EditProfile.DescriptionCounter": "Description {0}/1500",
|
||||
"EditProfile.PreviewLabel": "Preview (approximate)",
|
||||
"EditProfile.Button.SaveDescription": "Save Description",
|
||||
"EditProfile.Tooltip.SaveDescription": "Sets your profile description text",
|
||||
"EditProfile.Button.ClearDescription": "Clear Description",
|
||||
"EditProfile.Tooltip.ClearDescription": "Clears your profile description text",
|
||||
"Intro.Welcome.Title": "Welcome to Umbra",
|
||||
"Intro.Welcome.Paragraph1": "Umbra is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.",
|
||||
"Intro.Welcome.Paragraph2": "We will have to setup a few things first before you can start using this plugin. Click on next to continue.",
|
||||
"Intro.Welcome.Note": "Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients might look broken because of this or others players mods might not apply on your end altogether. If you want to use this plugin you will have to move your mods to Penumbra.",
|
||||
"Intro.Welcome.Next": "Next",
|
||||
"Intro.Agreement.Title": "Agreement of Usage of Service",
|
||||
"Intro.Agreement.Callout": "READ THIS CAREFULLY",
|
||||
"Intro.Agreement.Timeout": "'I agree' button will be available in {0}s",
|
||||
"Intro.Agreement.Paragraph1": "To use Umbra, you must be over the age of 18, or 21 in some jurisdictions.",
|
||||
"Intro.Agreement.Paragraph2": "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod.",
|
||||
"Intro.Agreement.Paragraph3": "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again.",
|
||||
"Intro.Agreement.Paragraph4": "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.",
|
||||
"Intro.Agreement.Paragraph5": "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.",
|
||||
"Intro.Agreement.Paragraph6": "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted.",
|
||||
"Intro.Agreement.Paragraph7": "Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.",
|
||||
"Intro.Agreement.Paragraph8": "Umbra is operated from servers located in the European Union. You agree not to upload any content to the service that violates EU law; and more specifically, German law.",
|
||||
"Intro.Agreement.Paragraph9": "You may delete your account at any time from within the Settings panel of the plugin. Any mods unique to you will then be removed from the server within 14 days.",
|
||||
"Intro.Agreement.Paragraph10": "This service is provided as-is.",
|
||||
"Intro.Agreement.Accept": "I agree",
|
||||
"Intro.Storage.Title": "File Storage Setup",
|
||||
"Intro.Storage.Description": "To not unnecessarily download files already present on your computer, Umbra will have to scan your Penumbra mod directory. Additionally, a local storage folder must be set where Umbra will download other character files to. Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.",
|
||||
"Intro.Storage.ScanNote": "Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.",
|
||||
"Intro.Storage.Warning.FileCache": "Warning: once past this step you should not delete the FileCache.csv of Umbra in the Plugin Configurations folder of Dalamud. Otherwise on the next launch a full re-scan of the file cache database will be initiated.",
|
||||
"Intro.Storage.Warning.ScanHang": "Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.",
|
||||
"Intro.Storage.NoPenumbra": "You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.",
|
||||
"Intro.Storage.StartScan": "Start Scan",
|
||||
"Intro.Storage.UseCompactor": "Use File Compactor",
|
||||
"Intro.Storage.CompactorDescription": "The File Compactor can save a tremendous amount of space on the hard disk for downloads through Umbra. It will incur a minor CPU penalty on download but can speed up loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Umbra settings.",
|
||||
"Intro.Registration.Title": "Service Registration",
|
||||
"Intro.Registration.Description": "To be able to use Umbra you will have to register an account.",
|
||||
"Intro.Registration.Support": "Refer to the instructions at the location you obtained this plugin for more information or support.",
|
||||
"Intro.Registration.NewAccountInfo": "If you have not used Umbra before, click below to register a new account.",
|
||||
"Intro.Registration.RegisterButton": "Register a new Umbra account",
|
||||
"Intro.Registration.SendingRequest": "Sending request...",
|
||||
"Intro.Registration.Success": "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC.",
|
||||
"Intro.Registration.UnknownError": "An unknown error occured. Please try again later.",
|
||||
"Intro.Registration.SecretKeyLabel": "Enter Secret Key",
|
||||
"Intro.Registration.SecretKeyLabelRegistered": "Secret Key",
|
||||
"Intro.Registration.SecretKeyInstructions": "If you already have a registered account, you can enter its secret key below to use it instead.",
|
||||
"Intro.Registration.SecretKeyLength": "Your secret key must be exactly 64 characters long.",
|
||||
"Intro.Registration.SecretKeyCharacters": "Your secret key can only contain ABCDEF and the numbers 0-9.",
|
||||
"Intro.Registration.SaveAndConnect": "Save and Connect",
|
||||
"Intro.Registration.SavedKeyRegistered": "(registered {0})",
|
||||
"Intro.Registration.SavedKeySetup": "Secret Key added on Setup ({0})",
|
||||
"Intro.ConnectionStatus.Connected": "Connected",
|
||||
"AutoDetect.Disabled": "Nearby detection is disabled. Enable it in Settings to start detecting nearby Umbra users.",
|
||||
"AutoDetect.MaxDistance": "Max distance (m)",
|
||||
"AutoDetect.Table.Name": "Name",
|
||||
"AutoDetect.Table.World": "World",
|
||||
"AutoDetect.Table.Distance": "Distance",
|
||||
"AutoDetect.Table.Status": "Status",
|
||||
"AutoDetect.Table.Action": "Action",
|
||||
"AutoDetect.World.Unknown": "-",
|
||||
"AutoDetect.Distance.Unknown": "-",
|
||||
"AutoDetect.Distance.Format": "{0:0.0} m",
|
||||
"AutoDetect.Status.Paired": "Paired",
|
||||
"AutoDetect.Status.RequestsDisabled": "Requests disabled",
|
||||
"AutoDetect.Status.OnUmbra": "On Umbra",
|
||||
"AutoDetect.Action.AlreadySynced": "Already sync",
|
||||
"AutoDetect.Action.RequestsDisabled": "Requests disabled",
|
||||
"AutoDetect.Action.SendRequest": "Send request",
|
||||
"PairGroups.ResumeAll": "Resume pairing with all pairs in {0}",
|
||||
"PairGroups.PauseAll": "Pause pairing with all pairs in {0}",
|
||||
"PairGroups.Menu.Title": "Group Flyout Menu",
|
||||
"PairGroups.Menu.AddPeople": "Add people to {0}",
|
||||
"PairGroups.Menu.AddPeople.Tooltip": "Add more users to Group {0}",
|
||||
"PairGroups.Menu.Delete": "Delete {0}",
|
||||
"PairGroups.Menu.Delete.Tooltip": "Delete Group {0} (Will not delete the pairs)\nHold CTRL to delete",
|
||||
"PairGroups.Tag.Unpaired": "Unpaired",
|
||||
"PairGroups.Tag.Offline": "Offline",
|
||||
"PairGroups.Tag.Online": "Online",
|
||||
"PairGroups.Tag.Contacts": "Contacts",
|
||||
"PairGroups.Tag.Visible": "Visible",
|
||||
"PairGroups.Header.WithCounts": "{0} ({1}/{2}/{3} Pairs)",
|
||||
"PairGroups.Header.Special": "{0} ({1} Pairs)",
|
||||
"PairGroups.Tooltip.Title": "Group {0}",
|
||||
"PairGroups.Tooltip.Visible": "{0} Pairs visible",
|
||||
"PairGroups.Tooltip.Online": "{0} Pairs online/paused",
|
||||
"PairGroups.Tooltip.Total": "{0} Pairs total",
|
||||
"GroupPanel.Join.InputHint": "Syncshell GID/Alias (leave empty to create)",
|
||||
"GroupPanel.Join.PasswordPopup": "Enter Syncshell Password",
|
||||
"GroupPanel.Create.PopupTitle": "Create Syncshell",
|
||||
"GroupPanel.Create.Tooltip": "Create Syncshell",
|
||||
"GroupPanel.Create.TooMany": "You cannot create more than {0} Syncshells",
|
||||
"GroupPanel.Join.Tooltip": "Join Syncshell {0}",
|
||||
"GroupPanel.Join.TooMany": "You cannot join more than {0} Syncshells",
|
||||
"GroupPanel.Join.Warning": "Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell.",
|
||||
"GroupPanel.Join.EnterPassword": "Enter the password for Syncshell {0}:",
|
||||
"GroupPanel.Join.PasswordHint": "{0} Password",
|
||||
"GroupPanel.Join.Error": "An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({0}), it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({1} users) or the Syncshell has closed invites.",
|
||||
"GroupPanel.Join.Button": "Join {0}",
|
||||
"GroupPanel.Create.ChooseType": "Choisissez le type de Syncshell \u00e0 cr\u00e9er.",
|
||||
"GroupPanel.Create.Permanent": "Permanente",
|
||||
"GroupPanel.Create.Temporary": "Temporaire",
|
||||
"GroupPanel.Create.AliasPrompt": "Donnez un nom \u00e0 votre Syncshell (optionnel) puis cr\u00e9ez-la.",
|
||||
"GroupPanel.Create.AliasHint": "Nom du Syncshell",
|
||||
"GroupPanel.Create.TempMaxDuration": "Dur\u00e9e maximale d'une Syncshell temporaire : 7 jours.",
|
||||
"GroupPanel.Create.TempExpires": "Expiration le {0:g} (heure locale).",
|
||||
"GroupPanel.Create.Instruction": "Appuyez sur le bouton ci-dessous pour cr\u00e9er une nouvelle Syncshell.",
|
||||
"GroupPanel.Create.Button": "Create Syncshell",
|
||||
"GroupPanel.Create.Error.NameInUse": "Le nom de la Syncshell est d\u00e9j\u00e0 utilis\u00e9.",
|
||||
"GroupPanel.Create.Result.Name": "Syncshell Name: {0}",
|
||||
"GroupPanel.Create.Result.Id": "Syncshell ID: {0}",
|
||||
"GroupPanel.Create.Result.Password": "Syncshell Password: {0}",
|
||||
"GroupPanel.Create.Result.ChangeLater": "You can change the Syncshell password later at any time.",
|
||||
"GroupPanel.Create.Result.TempExpires": "Cette Syncshell expirera le {0:g} (heure locale).",
|
||||
"GroupPanel.Create.Error.Generic": "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.",
|
||||
"GroupPanel.CommentHint": "Comment/Notes",
|
||||
"GroupPanel.CommentTooltip": "Hit ENTER to save\\nRight click to cancel",
|
||||
"GroupPanel.Banlist.Title": "Manage Banlist for {0}",
|
||||
"GroupPanel.Banlist.Refresh": "Refresh Banlist from Server",
|
||||
"GroupPanel.Banlist.Column.Uid": "UID",
|
||||
"GroupPanel.Banlist.Column.Alias": "Alias",
|
||||
"GroupPanel.Banlist.Column.By": "By",
|
||||
"GroupPanel.Banlist.Column.Date": "Date",
|
||||
"GroupPanel.Banlist.Column.Reason": "Reason",
|
||||
"GroupPanel.Banlist.Column.Actions": "Actions",
|
||||
"GroupPanel.Banlist.Unban": "Unban",
|
||||
"GroupPanel.Password.Title": "Change Syncshell Password",
|
||||
"GroupPanel.Password.Description": "Enter the new Syncshell password for Syncshell {0} here.",
|
||||
"GroupPanel.Password.Warning": "This action is irreversible",
|
||||
"GroupPanel.Password.Hint": "New password for {0}",
|
||||
"GroupPanel.Password.Button": "Change password",
|
||||
"GroupPanel.Password.Error.TooShort": "The selected password is too short. It must be at least 10 characters.",
|
||||
"GroupPanel.Invites.Title": "Create Bulk One-Time Invites",
|
||||
"GroupPanel.Invites.Description": "This allows you to create up to 100 one-time invites at once for the Syncshell {0}.\\nThe invites are valid for 24h after creation and will automatically expire.",
|
||||
"GroupPanel.Invites.CreateButton": "Create invites",
|
||||
"GroupPanel.Invites.Result": "A total of {0} invites have been created.",
|
||||
"GroupPanel.Invites.Copy": "Copy invites to clipboard",
|
||||
"GroupPanel.List.Visible": "Visible",
|
||||
"GroupPanel.List.Online": "Online",
|
||||
"GroupPanel.List.Offline": "Offline/Unknown",
|
||||
"GroupPanel.List.OfflineOmitted": "{0} offline users omitted from display.",
|
||||
"GroupPanel.Permissions.Header": "Syncshell permissions",
|
||||
"GroupPanel.Permissions.InvitesDisabled": "Syncshell is closed for joining",
|
||||
"GroupPanel.Permissions.SoundDisabledOwner": "Sound sync disabled through owner",
|
||||
"GroupPanel.Permissions.AnimationDisabledOwner": "Animation sync disabled through owner",
|
||||
"GroupPanel.Permissions.VfxDisabledOwner": "VFX sync disabled through owner",
|
||||
"GroupPanel.Permissions.OwnHeader": "Your permissions",
|
||||
"GroupPanel.Permissions.SoundDisabledSelf": "Sound sync disabled through you",
|
||||
"GroupPanel.Permissions.AnimationDisabledSelf": "Animation sync disabled through you",
|
||||
"GroupPanel.Permissions.VfxDisabledSelf": "VFX sync disabled through you",
|
||||
"GroupPanel.Permissions.NotePriority": "Note that syncshell permissions for disabling take precedence over your own set permissions",
|
||||
"GroupPanel.PauseToggle.Tooltip": "{0} pairing with all users in this Syncshell",
|
||||
"GroupPanel.PauseToggle.Resume": "Resume",
|
||||
"GroupPanel.PauseToggle.Pause": "Pause",
|
||||
"GroupPanel.Popup.Leave": "Leave Syncshell",
|
||||
"GroupPanel.Popup.LeaveTooltip": "Hold CTRL and click to leave this Syncshell{0}",
|
||||
"GroupPanel.Popup.LeaveWarning": "WARNING: This action is irreversible\\nLeaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.",
|
||||
"GroupPanel.Popup.CopyId": "Copy ID",
|
||||
"GroupPanel.Popup.CopyIdTooltip": "Copy Syncshell ID to Clipboard",
|
||||
"GroupPanel.Popup.CopyNotes": "Copy Notes",
|
||||
"GroupPanel.Popup.CopyNotesTooltip": "Copies all your notes for all users in this Syncshell to the clipboard.\\nThey can be imported via Settings -> General -> Notes -> Import notes from clipboard",
|
||||
"GroupPanel.Popup.EnableSound": "Enable sound sync",
|
||||
"GroupPanel.Popup.DisableSound": "Disable sound sync",
|
||||
"GroupPanel.Popup.SoundTooltip": "Sets your allowance for sound synchronization for users of this syncshell.\\nDisabling the synchronization will stop applying sound modifications for users of this syncshell.\\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\\nNote: this setting does not apply to individual pairs that are also in the syncshell.",
|
||||
"GroupPanel.Popup.EnableAnimations": "Enable animations sync",
|
||||
"GroupPanel.Popup.DisableAnimations": "Disable animations sync",
|
||||
"GroupPanel.Popup.AnimTooltip": "Sets your allowance for animations synchronization for users of this syncshell.\\nDisabling the synchronization will stop applying animations modifications for users of this syncshell.\\nNote: this setting might also affect sound synchronization\\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\\nNote: this setting does not apply to individual pairs that are also in the syncshell.",
|
||||
"GroupPanel.Popup.EnableVfx": "Enable VFX sync",
|
||||
"GroupPanel.Popup.DisableVfx": "Disable VFX sync",
|
||||
"GroupPanel.Popup.VfxTooltip": "Sets your allowance for VFX synchronization for users of this syncshell.\\nDisabling the synchronization will stop applying VFX modifications for users of this syncshell.\\nNote: this setting might also affect animation synchronization to some degree\\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\\nNote: this setting does not apply to individual pairs that are also in the syncshell.",
|
||||
"GroupPanel.Syncshell.OwnerTooltip": "You are the owner of Syncshell {0}",
|
||||
"GroupPanel.Syncshell.ModeratorTooltip": "You are a moderator of Syncshell {0}",
|
||||
"GroupPanel.Syncshell.MemberCount": "{0}/{1}",
|
||||
"GroupPanel.Syncshell.MemberCountTooltip": "Membres connect\u00e9s / membres totaux\\nCapacit\u00e9 maximale : {0}\\nSyncshell ID: {1}",
|
||||
"GroupPanel.Syncshell.NameTooltip": "Left click to switch between GID display and comment\\nRight click to change comment for {0}\\n\\nUsers: {1}, Owner: {2}",
|
||||
"GroupPanel.Syncshell.TempTag": "(Temp)",
|
||||
"GroupPanel.Syncshell.TempExpires": "Expire le {0:g}",
|
||||
"GroupPanel.Syncshell.TempTooltip": "Syncshell temporaire",
|
||||
"GroupPanel.Create.Duration.SingleDay": "24h",
|
||||
"GroupPanel.Create.Duration.Days": "{0}j",
|
||||
"GroupPanel.Create.Duration.Hours": "{0}h",
|
||||
"GroupPanel.Invites.AmountLabel": "Amount",
|
||||
"GroupPanel.Popup.OpenAdmin": "Open Admin Panel"
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
{
|
||||
"Language.DisplayName.French": "Français",
|
||||
"Language.DisplayName.English": "Anglais",
|
||||
"Settings.Plugins.MandatoryHeading": "",
|
||||
"Settings.Plugins.MandatoryLabel": "",
|
||||
"Settings.Plugins.OptionalHeading": "",
|
||||
"Settings.Plugins.OptionalLabel": "",
|
||||
"Settings.Plugins.OptionalDescription": "",
|
||||
"Settings.Plugins.Tooltip.Available": "",
|
||||
"Settings.Plugins.Tooltip.Unavailable": "",
|
||||
"Settings.Plugins.WarningMandatoryMissing": "",
|
||||
"Settings.General.LocalizationHeading": "Langue du plugin",
|
||||
"Settings.General.Language": "Langue",
|
||||
"Settings.General.Language.Description": "Sélectionnez la langue du plugin. Les traductions manquantes seront affichées en anglais.",
|
||||
"Settings.General.NotesHeading": "",
|
||||
"Settings.General.Notes.Export": "",
|
||||
"Settings.General.Notes.Import": "",
|
||||
"Settings.General.Notes.Overwrite": "",
|
||||
"Settings.General.Notes.Overwrite.Description": "",
|
||||
"Settings.General.Notes.Import.Success": "",
|
||||
"Settings.General.Notes.Import.Failure": "",
|
||||
"Settings.General.Notes.OpenPopup": "",
|
||||
"Settings.General.Notes.OpenPopup.Description": "",
|
||||
"Settings.Transfers.Blocked.Description": "",
|
||||
"Settings.Transfers.Blocked.Column.Hash": "",
|
||||
"Settings.Transfers.Blocked.Column.ForbiddenBy": "",
|
||||
"Settings.Transfers.Blocked.Tab": "",
|
||||
"Settings.Transfers.Heading": "",
|
||||
"Settings.Transfers.GlobalLimit.Label": "",
|
||||
"Settings.Transfers.GlobalLimit.Unit.BytePerSec": "",
|
||||
"Settings.Transfers.GlobalLimit.Unit.KiloBytePerSec": "",
|
||||
"Settings.Transfers.GlobalLimit.Unit.MegaBytePerSec": "",
|
||||
"Settings.Transfers.GlobalLimit.Hint": "",
|
||||
"Settings.Transfers.MaxParallelDownloads": "",
|
||||
"Settings.Transfers.AutoDetect.Heading": "",
|
||||
"Settings.Transfers.AutoDetect.EnableNearby": "",
|
||||
"Settings.Transfers.AutoDetect.AllowRequests": "",
|
||||
"Settings.Transfers.AutoDetect.Notification.Title": "",
|
||||
"Settings.Transfers.AutoDetect.Notification.Enabled": "",
|
||||
"Settings.Transfers.AutoDetect.Notification.Disabled": "",
|
||||
"Settings.Transfers.AutoDetect.MaxDistance": "",
|
||||
"Settings.Transfers.UI.Heading": "",
|
||||
"Settings.Transfers.UI.ShowWindow": "",
|
||||
"Settings.Transfers.UI.ShowWindow.Description": "",
|
||||
"Settings.Transfers.UI.EditWindowPosition": "",
|
||||
"Settings.Transfers.UI.ShowTransferBars": "",
|
||||
"Settings.Transfers.UI.ShowTransferBars.Description": "",
|
||||
"Settings.Transfers.UI.ShowDownloadText": "",
|
||||
"Settings.Transfers.UI.ShowDownloadText.Description": "",
|
||||
"Settings.Transfers.UI.BarWidth": "",
|
||||
"Settings.Transfers.UI.BarWidth.Description": "",
|
||||
"Settings.Transfers.UI.BarHeight": "",
|
||||
"Settings.Transfers.UI.BarHeight.Description": "",
|
||||
"Settings.Transfers.UI.ShowUploading": "",
|
||||
"Settings.Transfers.UI.ShowUploading.Description": "",
|
||||
"Settings.Transfers.UI.ShowUploadingBigText": "",
|
||||
"Settings.Transfers.UI.ShowUploadingBigText.Description": "",
|
||||
"Settings.Transfers.Current.Heading": "",
|
||||
"Settings.Transfers.Current.Tab": "",
|
||||
"Settings.Transfers.Current.Uploads": "",
|
||||
"Settings.Transfers.Current.Uploads.Column.File": "",
|
||||
"Settings.Transfers.Current.Uploads.Column.Uploaded": "",
|
||||
"Settings.Transfers.Current.Uploads.Column.Size": "",
|
||||
"Settings.Transfers.Current.Downloads": "",
|
||||
"Settings.Transfers.Current.Downloads.Column.User": "",
|
||||
"Settings.Transfers.Current.Downloads.Column.Server": "",
|
||||
"Settings.Transfers.Current.Downloads.Column.Files": "",
|
||||
"Settings.Transfers.Current.Downloads.Column.Download": "",
|
||||
"Settings.Storage.Heading": "",
|
||||
"Settings.Storage.Description": "",
|
||||
"Settings.Service.ActionsHeading": "",
|
||||
"Settings.Service.Actions.DeleteAccount": "",
|
||||
"Settings.Service.Actions.DeleteAccountPopup": "",
|
||||
"Settings.Service.Actions.DeleteAccount.Description": "",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Body1": "",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Body2": "",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Confirm": "",
|
||||
"Settings.Service.Actions.DeleteAccount.Popup.Cancel": "",
|
||||
"Settings.Service.SettingsHeading": "",
|
||||
"Settings.Service.ReconnectWarning": "",
|
||||
"Settings.Service.Tabs.CharacterAssignments": "",
|
||||
"Settings.Service.Tabs.SecretKey": "",
|
||||
"Settings.Service.Tabs.ServiceSettings": "",
|
||||
"Settings.Service.Character.Assignments.Description": "",
|
||||
"Settings.Service.Character.Assignments.TooltipCurrent": "",
|
||||
"Settings.Service.Character.Assignments.DeleteTooltip": "",
|
||||
"Settings.Service.Character.Assignments.AddCurrent": "",
|
||||
"Settings.Service.Character.Assignments.NoKeys": "",
|
||||
"Settings.Service.SecretKey.DisplayName": "",
|
||||
"Settings.Service.SecretKey.Value": "",
|
||||
"Settings.Service.SecretKey.AssignCurrent": "",
|
||||
"Settings.Service.SecretKey.AssignTooltip": "",
|
||||
"Settings.Service.SecretKey.Delete": "",
|
||||
"Settings.Service.SecretKey.DeleteTooltip": "",
|
||||
"Settings.Service.SecretKey.InUse": "",
|
||||
"Settings.Service.SecretKey.Add": "",
|
||||
"Settings.Service.SecretKey.NewFriendlyName": "",
|
||||
"Settings.Service.SecretKey.RegisterAccount": "",
|
||||
"Settings.Service.SecretKey.RegisterFailed": "",
|
||||
"Settings.Service.SecretKey.RegisterSuccess": "",
|
||||
"Settings.Service.SecretKey.RegisteredFriendlyName": "",
|
||||
"Settings.Service.SecretKey.Registering": "",
|
||||
"Settings.Service.ServiceTab.Uri": "",
|
||||
"Settings.Service.ServiceTab.UriReadOnlyHint": "",
|
||||
"Settings.Service.ServiceTab.Name": "",
|
||||
"Settings.Service.ServiceTab.NameReadOnlyHint": "",
|
||||
"Settings.Service.ServiceTab.Delete": "",
|
||||
"Settings.Service.ServiceTab.DeleteHint": "",
|
||||
"Settings.Advanced.Heading": "",
|
||||
"Settings.Advanced.Tab": "",
|
||||
"Settings.Advanced.Api.Enable": "",
|
||||
"Settings.Advanced.Api.Description": "",
|
||||
"Settings.Advanced.Api.Status.Active": "",
|
||||
"Settings.Advanced.Api.Status.Disabled": "",
|
||||
"Settings.Advanced.Api.Status.PluginLoaded": "",
|
||||
"Settings.Advanced.Api.Status.Unknown": "",
|
||||
"Settings.Advanced.EventViewer.LogToDisk": "",
|
||||
"Settings.Advanced.EventViewer.Open": "",
|
||||
"Settings.Advanced.HoldCombat": "",
|
||||
"Settings.Advanced.SerializedApplications": "",
|
||||
"Settings.Advanced.SerializedApplications.Description": "",
|
||||
"Settings.Advanced.DebugHeading": "",
|
||||
"Settings.Advanced.Debug.LastCreatedTree": "",
|
||||
"Settings.Advanced.Debug.CopyButton": "",
|
||||
"Settings.Advanced.Debug.CopyError": "",
|
||||
"Settings.Advanced.Debug.CopyTooltip": "",
|
||||
"Settings.Advanced.LogLevel": "",
|
||||
"Settings.Advanced.Performance.LogCounters": "",
|
||||
"Settings.Advanced.Performance.LogCounters.Description": "",
|
||||
"Settings.Advanced.Performance.PrintStats": "",
|
||||
"Settings.Advanced.Performance.PrintStatsRecent": "",
|
||||
"Settings.Advanced.ActiveBlocks": "",
|
||||
"Settings.UI.Heading": "",
|
||||
"Settings.UI.EnableRightClick": "",
|
||||
"Settings.UI.EnableRightClick.Description": "",
|
||||
"Settings.UI.EnableDtrEntry": "",
|
||||
"Settings.UI.EnableDtrEntry.Description": "",
|
||||
"Settings.UI.Dtr.ShowUid": "",
|
||||
"Settings.UI.Dtr.PreferNotes": "",
|
||||
"Settings.UI.Dtr.UseColors": "",
|
||||
"Settings.UI.Dtr.ColorDefault": "",
|
||||
"Settings.UI.Dtr.ColorNotConnected": "",
|
||||
"Settings.UI.Dtr.ColorPairsInRange": "",
|
||||
"Settings.UI.NameColors.Enable": "",
|
||||
"Settings.UI.NameColors.Character": "",
|
||||
"Settings.UI.NameColors.Blocked": "",
|
||||
"Settings.UI.VisibleGroup": "",
|
||||
"Settings.UI.VisibleGroup.Description": "",
|
||||
"Settings.UI.OfflineGroup": "",
|
||||
"Settings.UI.OfflineGroup.Description": "",
|
||||
"Settings.UI.ShowPlayerNames": "",
|
||||
"Settings.UI.ShowPlayerNames.Description": "",
|
||||
"Settings.UI.Profiles.Show": "",
|
||||
"Settings.UI.Profiles.Show.Description": "",
|
||||
"Settings.UI.Profiles.PopoutRight": "",
|
||||
"Settings.UI.Profiles.PopoutRight.Description": "",
|
||||
"Settings.UI.Profiles.HoverDelay": "",
|
||||
"Settings.UI.Profiles.HoverDelay.Description": "",
|
||||
"Settings.UI.Profiles.ShowNsfw": "",
|
||||
"Settings.UI.Profiles.ShowNsfw.Description": "",
|
||||
"Settings.Notifications.Heading": "",
|
||||
"Settings.Notifications.InfoDisplay": "",
|
||||
"Settings.Notifications.InfoDisplay.Description": "",
|
||||
"Settings.Notifications.WarningDisplay": "",
|
||||
"Settings.Notifications.WarningDisplay.Description": "",
|
||||
"Settings.Notifications.ErrorDisplay": "",
|
||||
"Settings.Notifications.ErrorDisplay.Description": "",
|
||||
"Settings.Notifications.Location.Nowhere": "",
|
||||
"Settings.Notifications.Location.Chat": "",
|
||||
"Settings.Notifications.Location.Toast": "",
|
||||
"Settings.Notifications.Location.Both": "",
|
||||
"Settings.Notifications.DisableOptionalWarnings": "",
|
||||
"Settings.Notifications.DisableOptionalWarnings.Description": "",
|
||||
"Settings.Notifications.EnableOnlineNotifications": "",
|
||||
"Settings.Notifications.EnableOnlineNotifications.Description": "",
|
||||
"Settings.Notifications.IndividualPairsOnly": "",
|
||||
"Settings.Notifications.IndividualPairsOnly.Description": "",
|
||||
"Settings.Notifications.NamedPairsOnly": "",
|
||||
"Settings.Notifications.NamedPairsOnly.Description": "",
|
||||
"Compact.Version.UnsupportedTitle": "",
|
||||
"Compact.Version.Outdated": "",
|
||||
"Compact.Toggle.IndividualPairs": "",
|
||||
"Compact.Toggle.Syncshells": "",
|
||||
"Compact.AddUser.ModalTitle": "",
|
||||
"Compact.AddUser.Description": "",
|
||||
"Compact.AddUser.NoteHint": "",
|
||||
"Compact.AddUser.Save": "",
|
||||
"Compact.AddCharacter.Button": "",
|
||||
"Compact.AddCharacter.SecretKeyLabel": "",
|
||||
"Compact.AddCharacter.NoKeys": "",
|
||||
"Compact.AddPair.Hint": "",
|
||||
"Compact.AddPair.Tooltip": "",
|
||||
"Compact.AddPair.Tooltip.DefaultUser": "",
|
||||
"Compact.Filter.Hint": "",
|
||||
"Compact.Filter.ToggleTooltip": "",
|
||||
"Compact.Filter.ToggleTooltip.Resume": "",
|
||||
"Compact.Filter.ToggleTooltip.Pause": "",
|
||||
"Compact.Filter.CooldownTooltip": "",
|
||||
"Compact.Nearby.Title": "",
|
||||
"Compact.Nearby.Button": "",
|
||||
"Compact.Nearby.None": "",
|
||||
"Compact.Nearby.Tooltip.AlreadyPaired": "",
|
||||
"Compact.Nearby.Tooltip.RequestsDisabled": "",
|
||||
"Compact.Nearby.Tooltip.SendInvite": "",
|
||||
"Compact.Nearby.Tooltip.CannotInvite": "",
|
||||
"Compact.Nearby.Incoming": "",
|
||||
"Compact.Nearby.Incoming.Entry": "",
|
||||
"Compact.Nearby.Incoming.Accept": "",
|
||||
"Compact.Nearby.Incoming.Dismiss": "",
|
||||
"Compact.Header.SettingsTooltip": "",
|
||||
"Compact.Header.CopyUid": "",
|
||||
"Compact.ServerStatus.UsersOnline": "",
|
||||
"Compact.ServerStatus.Shard": "",
|
||||
"Compact.ServerStatus.NotConnected": "",
|
||||
"Compact.ServerStatus.EditProfile": "",
|
||||
"Compact.ServerStatus.Disconnect": "",
|
||||
"Compact.ServerStatus.Connect": "",
|
||||
"Compact.ServerError.Connecting": "",
|
||||
"Compact.ServerError.Reconnecting": "",
|
||||
"Compact.ServerError.Disconnected": "",
|
||||
"Compact.ServerError.Disconnecting": "",
|
||||
"Compact.ServerError.Unauthorized": "",
|
||||
"Compact.ServerError.Offline": "",
|
||||
"Compact.ServerError.VersionMismatch": "",
|
||||
"Compact.ServerError.RateLimited": "",
|
||||
"Compact.ServerError.NoSecretKey": "",
|
||||
"Compact.ServerError.MultiChara": "",
|
||||
"Compact.Transfers.CharacterAnalysis": "",
|
||||
"Compact.Transfers.CharacterDataHub": "",
|
||||
"Compact.UidText.Reconnecting": "",
|
||||
"Compact.UidText.Connecting": "",
|
||||
"Compact.UidText.Disconnected": "",
|
||||
"Compact.UidText.Disconnecting": "",
|
||||
"Compact.UidText.Unauthorized": "",
|
||||
"Compact.UidText.VersionMismatch": "",
|
||||
"Compact.UidText.Offline": "",
|
||||
"Compact.UidText.RateLimited": "",
|
||||
"Compact.UidText.NoSecretKey": "",
|
||||
"Compact.UidText.MultiChara": "",
|
||||
"UserPair.Status.Online": "",
|
||||
"UserPair.Status.Offline": "",
|
||||
"UserPair.Tooltip.NotAddedBack": "",
|
||||
"UserPair.Tooltip.Paused": "",
|
||||
"UserPair.Tooltip.Visible": "",
|
||||
"UserPair.Tooltip.Visible.LastPrefix": "",
|
||||
"UserPair.Tooltip.Visible.ModsInfo": "",
|
||||
"UserPair.Tooltip.Visible.FilesSize": "",
|
||||
"UserPair.Tooltip.Visible.Vram": "",
|
||||
"UserPair.Tooltip.Visible.Tris": "",
|
||||
"UserPair.Tooltip.Pause": "",
|
||||
"UserPair.Tooltip.Resume": "",
|
||||
"UserPair.Tooltip.Permission.Header": "",
|
||||
"UserPair.Tooltip.Permission.Sound": "",
|
||||
"UserPair.Tooltip.Permission.Animation": "",
|
||||
"UserPair.Tooltip.Permission.Vfx": "",
|
||||
"UserPair.Tooltip.Permission.Status": "",
|
||||
"UserPair.Tooltip.Permission.State.Disabled": "",
|
||||
"UserPair.Tooltip.Permission.State.Enabled": "",
|
||||
"UserPair.Tooltip.SharedData": "",
|
||||
"UserPair.Tooltip.SharedData.OpenHub": "",
|
||||
"UserPair.Menu.Target": "",
|
||||
"UserPair.Menu.OpenProfile": "",
|
||||
"UserPair.Menu.OpenProfile.Tooltip": "",
|
||||
"UserPair.Menu.OpenAnalysis": "",
|
||||
"UserPair.Menu.ReloadData": "",
|
||||
"UserPair.Menu.ReloadData.Tooltip": "",
|
||||
"UserPair.Menu.CyclePause": "",
|
||||
"UserPair.Menu.PairGroups": "",
|
||||
"UserPair.Menu.PairGroups.Tooltip": "",
|
||||
"UserPair.Menu.EnableSoundSync": "",
|
||||
"UserPair.Menu.DisableSoundSync": "",
|
||||
"UserPair.Menu.EnableAnimationSync": "",
|
||||
"UserPair.Menu.DisableAnimationSync": "",
|
||||
"UserPair.Menu.EnableVfxSync": "",
|
||||
"UserPair.Menu.DisableVfxSync": "",
|
||||
"UserPair.Menu.Unpair": "",
|
||||
"UserPair.Menu.Unpair.Tooltip": "",
|
||||
"Popup.Generic.Close": "",
|
||||
"Popup.BanUser.Description": "",
|
||||
"Popup.BanUser.ReasonHint": "",
|
||||
"Popup.BanUser.Button": "",
|
||||
"Popup.BanUser.ReasonNote": "",
|
||||
"Popup.Report.Title": "",
|
||||
"Popup.Report.Note": "",
|
||||
"Popup.Report.Warning": "",
|
||||
"Popup.Report.Scope": "",
|
||||
"Popup.Report.Button": "",
|
||||
"PairGroups.Popup.Title": "",
|
||||
"PairGroups.Popup.SelectPrompt": "",
|
||||
"PairGroups.Popup.CreatePrompt": "",
|
||||
"PairGroups.Popup.NewGroupHint": "",
|
||||
"PairGroups.SelectPairs.Title": "",
|
||||
"PairGroups.SelectPairs.SelectPrompt": "",
|
||||
"PairGroups.SelectPairs.FilterHint": "",
|
||||
"UidDisplay.Tooltip": "",
|
||||
"UidDisplay.EditNotes.Hint": "",
|
||||
"DataAnalysis.WindowTitle": "",
|
||||
"DataAnalysis.Bc7.ModalTitle": "",
|
||||
"DataAnalysis.Bc7.Status": "",
|
||||
"DataAnalysis.Bc7.CurrentFile": "",
|
||||
"DataAnalysis.Bc7.Cancel": "",
|
||||
"DataAnalysis.Description": "",
|
||||
"DataAnalysis.Analyzing": "",
|
||||
"DataAnalysis.Button.CancelAnalysis": "",
|
||||
"DataAnalysis.Analyze.MissingNotice": "",
|
||||
"DataAnalysis.Button.StartMissing": "",
|
||||
"DataAnalysis.Button.StartAll": "",
|
||||
"DataAnalysis.TotalFiles": "",
|
||||
"DataAnalysis.Tooltip.FileSummary": "",
|
||||
"DataAnalysis.TotalSizeActual": "",
|
||||
"DataAnalysis.TotalSizeDownload": "",
|
||||
"DataAnalysis.Tooltip.CalculateDownloadSize": "",
|
||||
"DataAnalysis.TotalTriangles": "",
|
||||
"DataAnalysis.FilesFor": "",
|
||||
"DataAnalysis.Object.SizeActual": "",
|
||||
"DataAnalysis.Object.SizeDownload": "",
|
||||
"DataAnalysis.Object.Vram": "",
|
||||
"DataAnalysis.Object.Triangles": "",
|
||||
"DataAnalysis.FileGroup.Count": "",
|
||||
"DataAnalysis.FileGroup.SizeActual": "",
|
||||
"DataAnalysis.FileGroup.SizeDownload": "",
|
||||
"DataAnalysis.Bc7.EnableMode": "",
|
||||
"DataAnalysis.Bc7.WarningTitle": "",
|
||||
"DataAnalysis.Bc7.WarningIrreversible": "",
|
||||
"DataAnalysis.Bc7.WarningDetails": "",
|
||||
"DataAnalysis.Bc7.StartConversion": "",
|
||||
"DataAnalysis.Table.Hash": "",
|
||||
"DataAnalysis.Table.Filepaths": "",
|
||||
"DataAnalysis.Table.Gamepaths": "",
|
||||
"DataAnalysis.Table.FileSize": "",
|
||||
"DataAnalysis.Table.DownloadSize": "",
|
||||
"DataAnalysis.Table.Format": "",
|
||||
"DataAnalysis.Table.ConvertToBc7": "",
|
||||
"DataAnalysis.Table.Triangles": "",
|
||||
"DataAnalysis.SelectedFile": "",
|
||||
"DataAnalysis.LocalFilePath": "",
|
||||
"DataAnalysis.MoreCount": "",
|
||||
"DataAnalysis.GamePath": "",
|
||||
"DownloadUi.WindowTitle": "",
|
||||
"DownloadUi.UploadStatus": "",
|
||||
"DownloadUi.DownloadStatus": "",
|
||||
"DownloadUi.UploadingLabel": "",
|
||||
"EventViewer.WindowTitle": "",
|
||||
"EventViewer.Button.Unfreeze": "",
|
||||
"EventViewer.Button.Freeze": "",
|
||||
"EventViewer.Tooltip.NewEvents": "",
|
||||
"EventViewer.FilterLabel": "",
|
||||
"EventViewer.Button.OpenLog": "",
|
||||
"EventViewer.Column.Time": "",
|
||||
"EventViewer.Column.Source": "",
|
||||
"EventViewer.Column.Uid": "",
|
||||
"EventViewer.Column.Character": "",
|
||||
"EventViewer.Column.Event": "",
|
||||
"EventViewer.Severity.Informational": "",
|
||||
"EventViewer.Severity.Warning": "",
|
||||
"EventViewer.Severity.Error": "",
|
||||
"EventViewer.NoValue": "",
|
||||
"DtrEntry.EntryName": "",
|
||||
"DtrEntry.Tooltip.Connected": "",
|
||||
"DtrEntry.Tooltip.Disconnected": "",
|
||||
"PermissionWindow.Title": "",
|
||||
"PermissionWindow.Pause.Label": "",
|
||||
"PermissionWindow.Pause.HelpMain": "",
|
||||
"PermissionWindow.Pause.HelpNote": "",
|
||||
"PermissionWindow.OtherPaused.True": "",
|
||||
"PermissionWindow.OtherPaused.False": "",
|
||||
"PermissionWindow.Sounds.Label": "",
|
||||
"PermissionWindow.Sounds.HelpMain": "",
|
||||
"PermissionWindow.Sounds.HelpNote": "",
|
||||
"PermissionWindow.OtherSoundDisabled.True": "",
|
||||
"PermissionWindow.OtherSoundDisabled.False": "",
|
||||
"PermissionWindow.Animations.Label": "",
|
||||
"PermissionWindow.Animations.HelpMain": "",
|
||||
"PermissionWindow.Animations.HelpNote": "",
|
||||
"PermissionWindow.OtherAnimationDisabled.True": "",
|
||||
"PermissionWindow.OtherAnimationDisabled.False": "",
|
||||
"PermissionWindow.Vfx.Label": "",
|
||||
"PermissionWindow.Vfx.HelpMain": "",
|
||||
"PermissionWindow.Vfx.HelpNote": "",
|
||||
"PermissionWindow.OtherVfxDisabled.True": "",
|
||||
"PermissionWindow.OtherVfxDisabled.False": "",
|
||||
"PermissionWindow.Button.Save": "",
|
||||
"PermissionWindow.Tooltip.Save": "",
|
||||
"PermissionWindow.Button.Revert": "",
|
||||
"PermissionWindow.Tooltip.Revert": "",
|
||||
"PermissionWindow.Button.Reset": "",
|
||||
"PermissionWindow.Tooltip.Reset": "",
|
||||
"EditProfile.WindowTitle": "",
|
||||
"EditProfile.CurrentProfile": "",
|
||||
"EditProfile.Button.UploadPicture": "",
|
||||
"EditProfile.Dialog.PictureTitle": "",
|
||||
"EditProfile.Tooltip.UploadPicture": "",
|
||||
"EditProfile.Button.ClearPicture": "",
|
||||
"EditProfile.Tooltip.ClearPicture": "",
|
||||
"EditProfile.Error.PictureTooLarge": "",
|
||||
"EditProfile.Checkbox.Nsfw": "",
|
||||
"EditProfile.Help.Nsfw": "",
|
||||
"EditProfile.DescriptionCounter": "",
|
||||
"EditProfile.PreviewLabel": "",
|
||||
"EditProfile.Button.SaveDescription": "",
|
||||
"EditProfile.Tooltip.SaveDescription": "",
|
||||
"EditProfile.Button.ClearDescription": "",
|
||||
"EditProfile.Tooltip.ClearDescription": "",
|
||||
"Intro.Welcome.Title": "",
|
||||
"Intro.Welcome.Paragraph1": "",
|
||||
"Intro.Welcome.Paragraph2": "",
|
||||
"Intro.Welcome.Note": "",
|
||||
"Intro.Welcome.Next": "",
|
||||
"Intro.Agreement.Title": "",
|
||||
"Intro.Agreement.Callout": "",
|
||||
"Intro.Agreement.Timeout": "",
|
||||
"Intro.Agreement.Paragraph1": "",
|
||||
"Intro.Agreement.Paragraph2": "",
|
||||
"Intro.Agreement.Paragraph3": "",
|
||||
"Intro.Agreement.Paragraph4": "",
|
||||
"Intro.Agreement.Paragraph5": "",
|
||||
"Intro.Agreement.Paragraph6": "",
|
||||
"Intro.Agreement.Paragraph7": "",
|
||||
"Intro.Agreement.Paragraph8": "",
|
||||
"Intro.Agreement.Paragraph9": "",
|
||||
"Intro.Agreement.Paragraph10": "",
|
||||
"Intro.Agreement.Accept": "",
|
||||
"Intro.Storage.Title": "",
|
||||
"Intro.Storage.Description": "",
|
||||
"Intro.Storage.ScanNote": "",
|
||||
"Intro.Storage.Warning.FileCache": "",
|
||||
"Intro.Storage.Warning.ScanHang": "",
|
||||
"Intro.Storage.NoPenumbra": "",
|
||||
"Intro.Storage.StartScan": "",
|
||||
"Intro.Storage.UseCompactor": "",
|
||||
"Intro.Storage.CompactorDescription": "",
|
||||
"Intro.Registration.Title": "",
|
||||
"Intro.Registration.Description": "",
|
||||
"Intro.Registration.Support": "",
|
||||
"Intro.Registration.NewAccountInfo": "",
|
||||
"Intro.Registration.RegisterButton": "",
|
||||
"Intro.Registration.SendingRequest": "",
|
||||
"Intro.Registration.Success": "",
|
||||
"Intro.Registration.UnknownError": "",
|
||||
"Intro.Registration.SecretKeyLabel": "",
|
||||
"Intro.Registration.SecretKeyLabelRegistered": "",
|
||||
"Intro.Registration.SecretKeyInstructions": "",
|
||||
"Intro.Registration.SecretKeyLength": "",
|
||||
"Intro.Registration.SecretKeyCharacters": "",
|
||||
"Intro.Registration.SaveAndConnect": "",
|
||||
"Intro.Registration.SavedKeyRegistered": "",
|
||||
"Intro.Registration.SavedKeySetup": "",
|
||||
"Intro.ConnectionStatus.Connected": "",
|
||||
"AutoDetect.Disabled": "",
|
||||
"AutoDetect.MaxDistance": "",
|
||||
"AutoDetect.Table.Name": "",
|
||||
"AutoDetect.Table.World": "",
|
||||
"AutoDetect.Table.Distance": "",
|
||||
"AutoDetect.Table.Status": "",
|
||||
"AutoDetect.Table.Action": "",
|
||||
"AutoDetect.World.Unknown": "",
|
||||
"AutoDetect.Distance.Unknown": "",
|
||||
"AutoDetect.Distance.Format": "",
|
||||
"AutoDetect.Status.Paired": "",
|
||||
"AutoDetect.Status.RequestsDisabled": "",
|
||||
"AutoDetect.Status.OnUmbra": "",
|
||||
"AutoDetect.Action.AlreadySynced": "",
|
||||
"AutoDetect.Action.RequestsDisabled": "",
|
||||
"AutoDetect.Action.SendRequest": "",
|
||||
"PairGroups.ResumeAll": "",
|
||||
"PairGroups.PauseAll": "",
|
||||
"PairGroups.Menu.Title": "",
|
||||
"PairGroups.Menu.AddPeople": "",
|
||||
"PairGroups.Menu.AddPeople.Tooltip": "",
|
||||
"PairGroups.Menu.Delete": "",
|
||||
"PairGroups.Menu.Delete.Tooltip": "",
|
||||
"PairGroups.Tag.Unpaired": "",
|
||||
"PairGroups.Tag.Offline": "",
|
||||
"PairGroups.Tag.Online": "",
|
||||
"PairGroups.Tag.Contacts": "",
|
||||
"PairGroups.Tag.Visible": "",
|
||||
"PairGroups.Header.WithCounts": "",
|
||||
"PairGroups.Header.Special": "",
|
||||
"PairGroups.Tooltip.Title": "",
|
||||
"PairGroups.Tooltip.Visible": "",
|
||||
"PairGroups.Tooltip.Online": "",
|
||||
"PairGroups.Tooltip.Total": "",
|
||||
"GroupPanel.Join.InputHint": "",
|
||||
"GroupPanel.Join.PasswordPopup": "",
|
||||
"GroupPanel.Create.PopupTitle": "",
|
||||
"GroupPanel.Create.Tooltip": "",
|
||||
"GroupPanel.Create.TooMany": "",
|
||||
"GroupPanel.Join.Tooltip": "",
|
||||
"GroupPanel.Join.TooMany": "",
|
||||
"GroupPanel.Join.Warning": "",
|
||||
"GroupPanel.Join.EnterPassword": "",
|
||||
"GroupPanel.Join.PasswordHint": "",
|
||||
"GroupPanel.Join.Error": "",
|
||||
"GroupPanel.Join.Button": "",
|
||||
"GroupPanel.Create.ChooseType": "",
|
||||
"GroupPanel.Create.Permanent": "",
|
||||
"GroupPanel.Create.Temporary": "",
|
||||
"GroupPanel.Create.AliasPrompt": "",
|
||||
"GroupPanel.Create.AliasHint": "",
|
||||
"GroupPanel.Create.TempMaxDuration": "",
|
||||
"GroupPanel.Create.TempExpires": "",
|
||||
"GroupPanel.Create.Instruction": "",
|
||||
"GroupPanel.Create.Button": "",
|
||||
"GroupPanel.Create.Error.NameInUse": "",
|
||||
"GroupPanel.Create.Result.Name": "",
|
||||
"GroupPanel.Create.Result.Id": "",
|
||||
"GroupPanel.Create.Result.Password": "",
|
||||
"GroupPanel.Create.Result.ChangeLater": "",
|
||||
"GroupPanel.Create.Result.TempExpires": "",
|
||||
"GroupPanel.Create.Error.Generic": "",
|
||||
"GroupPanel.CommentHint": "",
|
||||
"GroupPanel.CommentTooltip": "",
|
||||
"GroupPanel.Banlist.Title": "",
|
||||
"GroupPanel.Banlist.Refresh": "",
|
||||
"GroupPanel.Banlist.Column.Uid": "",
|
||||
"GroupPanel.Banlist.Column.Alias": "",
|
||||
"GroupPanel.Banlist.Column.By": "",
|
||||
"GroupPanel.Banlist.Column.Date": "",
|
||||
"GroupPanel.Banlist.Column.Reason": "",
|
||||
"GroupPanel.Banlist.Column.Actions": "",
|
||||
"GroupPanel.Banlist.Unban": "",
|
||||
"GroupPanel.Password.Title": "",
|
||||
"GroupPanel.Password.Description": "",
|
||||
"GroupPanel.Password.Warning": "",
|
||||
"GroupPanel.Password.Hint": "",
|
||||
"GroupPanel.Password.Button": "",
|
||||
"GroupPanel.Password.Error.TooShort": "",
|
||||
"GroupPanel.Invites.Title": "",
|
||||
"GroupPanel.Invites.Description": "",
|
||||
"GroupPanel.Invites.CreateButton": "",
|
||||
"GroupPanel.Invites.Result": "",
|
||||
"GroupPanel.Invites.Copy": "",
|
||||
"GroupPanel.List.Visible": "",
|
||||
"GroupPanel.List.Online": "",
|
||||
"GroupPanel.List.Offline": "",
|
||||
"GroupPanel.List.OfflineOmitted": "",
|
||||
"GroupPanel.Permissions.Header": "",
|
||||
"GroupPanel.Permissions.InvitesDisabled": "",
|
||||
"GroupPanel.Permissions.SoundDisabledOwner": "",
|
||||
"GroupPanel.Permissions.AnimationDisabledOwner": "",
|
||||
"GroupPanel.Permissions.VfxDisabledOwner": "",
|
||||
"GroupPanel.Permissions.OwnHeader": "",
|
||||
"GroupPanel.Permissions.SoundDisabledSelf": "",
|
||||
"GroupPanel.Permissions.AnimationDisabledSelf": "",
|
||||
"GroupPanel.Permissions.VfxDisabledSelf": "",
|
||||
"GroupPanel.Permissions.NotePriority": "",
|
||||
"GroupPanel.PauseToggle.Tooltip": "",
|
||||
"GroupPanel.PauseToggle.Resume": "",
|
||||
"GroupPanel.PauseToggle.Pause": "",
|
||||
"GroupPanel.Popup.Leave": "",
|
||||
"GroupPanel.Popup.LeaveTooltip": "",
|
||||
"GroupPanel.Popup.LeaveWarning": "",
|
||||
"GroupPanel.Popup.CopyId": "",
|
||||
"GroupPanel.Popup.CopyIdTooltip": "",
|
||||
"GroupPanel.Popup.CopyNotes": "",
|
||||
"GroupPanel.Popup.CopyNotesTooltip": "",
|
||||
"GroupPanel.Popup.EnableSound": "",
|
||||
"GroupPanel.Popup.DisableSound": "",
|
||||
"GroupPanel.Popup.SoundTooltip": "",
|
||||
"GroupPanel.Popup.EnableAnimations": "",
|
||||
"GroupPanel.Popup.DisableAnimations": "",
|
||||
"GroupPanel.Popup.AnimTooltip": "",
|
||||
"GroupPanel.Popup.EnableVfx": "",
|
||||
"GroupPanel.Popup.DisableVfx": "",
|
||||
"GroupPanel.Popup.VfxTooltip": "",
|
||||
"GroupPanel.Syncshell.OwnerTooltip": "",
|
||||
"GroupPanel.Syncshell.ModeratorTooltip": "",
|
||||
"GroupPanel.Syncshell.MemberCount": "",
|
||||
"GroupPanel.Syncshell.MemberCountTooltip": "",
|
||||
"GroupPanel.Syncshell.NameTooltip": "",
|
||||
"GroupPanel.Syncshell.TempTag": "",
|
||||
"GroupPanel.Syncshell.TempExpires": "",
|
||||
"GroupPanel.Syncshell.TempTooltip": "",
|
||||
"GroupPanel.Create.Duration.SingleDay": "",
|
||||
"GroupPanel.Create.Duration.Days": "",
|
||||
"GroupPanel.Create.Duration.Hours": "",
|
||||
"GroupPanel.Invites.AmountLabel": "",
|
||||
"GroupPanel.Popup.OpenAdmin": ""
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class CharaDataConfig : IMareConfiguration
|
||||
public bool NearbyOwnServerOnly { get; set; } = false;
|
||||
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
||||
public bool NearbyDrawWisps { get; set; } = true;
|
||||
public int NearbyMaxWisps { get; set; } = 20;
|
||||
public int NearbyDistanceFilter { get; set; } = 100;
|
||||
public bool NearbyShowOwnData { get; set; } = false;
|
||||
public bool ShowHelpTexts { get; set; } = true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using System.Collections.Generic;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -12,7 +13,6 @@ public class MareConfig : IMareConfiguration
|
||||
public bool AcceptedAgreement { get; set; } = false;
|
||||
public string CacheFolder { get; set; } = string.Empty;
|
||||
public bool DisableOptionalPluginWarnings { get; set; } = false;
|
||||
public LocalizationLanguage Language { get; set; } = LocalizationLanguage.French;
|
||||
public bool EnableDtrEntry { get; set; } = true;
|
||||
public int DtrStyle { get; set; } = 0;
|
||||
public bool ShowUidInDtrTooltip { get; set; } = true;
|
||||
@@ -20,7 +20,7 @@ public class MareConfig : IMareConfiguration
|
||||
public bool UseColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0x8D37C0u);
|
||||
public bool UseNameColors { get; set; } = false;
|
||||
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
||||
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
||||
@@ -38,6 +38,7 @@ public class MareConfig : IMareConfiguration
|
||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||
public bool OpenPopupOnAdd { get; set; } = true;
|
||||
public int ParallelDownloads { get; set; } = 10;
|
||||
public bool EnableDownloadQueue { get; set; } = false;
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
@@ -60,8 +61,14 @@ public class MareConfig : IMareConfiguration
|
||||
public bool ShowUploading { get; set; } = true;
|
||||
public bool ShowUploadingBigText { get; set; } = true;
|
||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||
public bool EnableAutoDetectDiscovery { get; set; } = false;
|
||||
public bool AllowAutoDetectPairRequests { get; set; } = false;
|
||||
public string LastChangelogVersionSeen { get; set; } = string.Empty;
|
||||
public bool DefaultDisableSounds { get; set; } = false;
|
||||
public bool DefaultDisableAnimations { get; set; } = false;
|
||||
public bool DefaultDisableVfx { get; set; } = false;
|
||||
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||
public bool EnableAutoDetectDiscovery { get; set; } = true;
|
||||
public bool AllowAutoDetectPairRequests { get; set; } = true;
|
||||
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
||||
public int AutoDetectMuteMinutes { get; set; } = 5;
|
||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||
@@ -78,6 +85,9 @@ public class MareConfig : IMareConfiguration
|
||||
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
||||
public bool ExtraChatAPI { get; set; } = false;
|
||||
public bool ExtraChatTags { get; set; } = false;
|
||||
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
|
||||
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||
|
||||
public bool MareAPI { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class PlayerPerformanceConfig : IMareConfiguration
|
||||
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
||||
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
||||
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
||||
public bool ShowSelfAnalysisWarnings { get; set; } = true;
|
||||
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
||||
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
||||
public bool IgnoreDirectPairs { get; set; } = true;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
public enum LocalizationLanguage
|
||||
{
|
||||
French = 0,
|
||||
English = 1,
|
||||
}
|
||||
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
[Serializable]
|
||||
public class SyncOverrideEntry
|
||||
{
|
||||
public bool? DisableSounds { get; set; }
|
||||
public bool? DisableAnimations { get; set; }
|
||||
public bool? DisableVfx { get; set; }
|
||||
|
||||
public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
public enum TypingIndicatorBubbleSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using MareSynchronos.PlayerData.Services;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Interop;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -150,8 +151,12 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatTypingDetectionService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
||||
var characterAnalyzer = _runtimeServiceScope.ServiceProvider.GetRequiredService<CharacterAnalyzer>();
|
||||
_ = characterAnalyzer.ComputeAnalysis(print: false);
|
||||
|
||||
#if !DEBUG
|
||||
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyName>UmbraSync</AssemblyName>
|
||||
<RootNamespace>UmbraSync</RootNamespace>
|
||||
<Version>0.1.8.0</Version>
|
||||
<Version>0.1.9.6</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -60,14 +60,4 @@
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Localization\\*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Localization\\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
|
||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mareMediator = mareMediator;
|
||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
_mareConfigService = mareConfigService;
|
||||
}
|
||||
|
||||
public FileDownloadManager Create()
|
||||
{
|
||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void AddGroupPair(GroupPairFullInfoDto dto)
|
||||
public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||
@@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
var group = _allGroups[dto.Group];
|
||||
_allClientPairs[dto.User].GroupPair[group] = dto;
|
||||
RecreateLazy();
|
||||
|
||||
if (!isInitialLoad)
|
||||
{
|
||||
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
|
||||
}
|
||||
}
|
||||
|
||||
public Pair? GetPairByUID(string uid)
|
||||
@@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
LastAddedUser = _allClientPairs[dto.User];
|
||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||
RecreateLazy();
|
||||
|
||||
if (addToLastAddedUser)
|
||||
{
|
||||
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearPairs()
|
||||
|
||||
@@ -6,7 +6,6 @@ using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Interop;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Configurations;
|
||||
using MareSynchronos.PlayerData.Factories;
|
||||
@@ -92,7 +91,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<MareMediator>();
|
||||
collection.AddSingleton<FileCacheManager>();
|
||||
collection.AddSingleton<ServerConfigurationManager>();
|
||||
collection.AddSingleton<LocalizationService>();
|
||||
collection.AddSingleton<ApiController>();
|
||||
collection.AddSingleton<PerformanceCollectorService>();
|
||||
collection.AddSingleton<HubFactory>();
|
||||
@@ -103,6 +101,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
|
||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
|
||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
|
||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
|
||||
collection.AddSingleton<MarePlugin>();
|
||||
collection.AddSingleton<MareProfileManager>();
|
||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||
@@ -117,6 +116,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<PluginWarningNotificationService>();
|
||||
collection.AddSingleton<FileCompactor>();
|
||||
collection.AddSingleton<TagHandler>();
|
||||
collection.AddSingleton<SyncDefaultsService>();
|
||||
collection.AddSingleton<UidDisplayHandler>();
|
||||
collection.AddSingleton<PluginWatcherService>();
|
||||
collection.AddSingleton<PlayerPerformanceService>();
|
||||
@@ -148,6 +148,9 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<IpcManager>();
|
||||
collection.AddSingleton<NotificationService>();
|
||||
collection.AddSingleton<TemporarySyncshellNotificationService>();
|
||||
collection.AddSingleton<PartyListTypingService>();
|
||||
collection.AddSingleton<TypingIndicatorStateService>();
|
||||
collection.AddSingleton<ChatTwoCompatibilityService>();
|
||||
|
||||
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
@@ -184,12 +187,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, AutoDetectUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, ChangelogUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, TypingIndicatorOverlay>();
|
||||
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
|
||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||
collection.AddScoped<CacheCreationService>();
|
||||
@@ -201,6 +206,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddScoped<UiSharedService>();
|
||||
collection.AddScoped<ChatService>();
|
||||
collection.AddScoped<GuiHookService>();
|
||||
collection.AddScoped<ChatTypingDetectionService>();
|
||||
|
||||
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||
@@ -216,9 +222,21 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
|
||||
})
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var partyListTypingService = _host.Services.GetRequiredService<PartyListTypingService>();
|
||||
pluginInterface.UiBuilder.Draw += partyListTypingService.Draw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook");
|
||||
}
|
||||
|
||||
_ = Task.Run(async () => {
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.WebAPI.AutoDetect;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
namespace MareSynchronos.Services.AutoDetect;
|
||||
|
||||
@@ -15,8 +23,15 @@ public class AutoDetectRequestService
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamud;
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
|
||||
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
|
||||
private readonly ApiController _apiController;
|
||||
|
||||
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService)
|
||||
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_configProvider = configProvider;
|
||||
@@ -24,9 +39,10 @@ public class AutoDetectRequestService
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_dalamud = dalamudUtilService;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public async Task<bool> SendRequestAsync(string token, CancellationToken ct = default)
|
||||
public async Task<bool> SendRequestAsync(string? token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
|
||||
{
|
||||
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||
{
|
||||
@@ -34,6 +50,54 @@ public class AutoDetectRequestService
|
||||
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(uid))
|
||||
{
|
||||
_logger.LogDebug("Nearby request blocked: no token or UID provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetKey = BuildTargetKey(uid, token, targetDisplayName);
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||
{
|
||||
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||
{
|
||||
tracker.LockUntil = null;
|
||||
tracker.Count = 0;
|
||||
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||
{
|
||||
_refusalTrackers.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||
{
|
||||
var elapsed = now - lastSent;
|
||||
if (elapsed < RequestCooldown)
|
||||
{
|
||||
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elapsed >= RequestCooldown)
|
||||
{
|
||||
_activeCooldowns.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var endpoint = _configProvider.RequestEndpoint;
|
||||
if (string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
@@ -49,14 +113,64 @@ public class AutoDetectRequestService
|
||||
}
|
||||
catch { }
|
||||
|
||||
var requestToken = string.IsNullOrEmpty(token) ? null : token;
|
||||
var requestUid = requestToken == null ? uid : null;
|
||||
|
||||
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
|
||||
var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false);
|
||||
var ok = await _client.SendRequestAsync(endpoint!, requestToken, requestUid, displayName, ct).ConfigureAwait(false);
|
||||
if (ok)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
tracker.Count = 0;
|
||||
tracker.LockUntil = null;
|
||||
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||
{
|
||||
_refusalTrackers.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||
var pendingKey = EnsureTargetKey(targetKey);
|
||||
var label = !string.IsNullOrWhiteSpace(targetDisplayName)
|
||||
? targetDisplayName!
|
||||
: (!string.IsNullOrEmpty(uid) ? uid : (!string.IsNullOrEmpty(token) ? token : pendingKey));
|
||||
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, token, label, DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_activeCooldowns.Remove(targetKey);
|
||||
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
tracker = new RefusalTracker();
|
||||
_refusalTrackers[targetKey] = tracker;
|
||||
}
|
||||
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||
{
|
||||
tracker.LockUntil = null;
|
||||
tracker.Count = 0;
|
||||
}
|
||||
|
||||
tracker.Count++;
|
||||
if (tracker.Count >= 3)
|
||||
{
|
||||
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||
}
|
||||
}
|
||||
_pendingRequests.TryRemove(targetKey, out _);
|
||||
}
|
||||
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||
}
|
||||
return ok;
|
||||
@@ -80,4 +194,193 @@ public class AutoDetectRequestService
|
||||
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
|
||||
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SendDirectUidRequestAsync(string uid, string? targetDisplayName = null, CancellationToken ct = default)
|
||||
{
|
||||
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||
{
|
||||
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
|
||||
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
{
|
||||
_logger.LogDebug("Direct pair request aborted: UID is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetKey = BuildTargetKey(uid, null, targetDisplayName);
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||
{
|
||||
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||
{
|
||||
tracker.LockUntil = null;
|
||||
tracker.Count = 0;
|
||||
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||
{
|
||||
_refusalTrackers.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||
{
|
||||
var elapsed = now - lastSent;
|
||||
if (elapsed < RequestCooldown)
|
||||
{
|
||||
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elapsed >= RequestCooldown)
|
||||
{
|
||||
_activeCooldowns.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _apiController.UserAddPair(new UserDto(new UserData(uid))).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
tracker.Count = 0;
|
||||
tracker.LockUntil = null;
|
||||
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||
{
|
||||
_refusalTrackers.Remove(targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||
var pendingKey = EnsureTargetKey(targetKey);
|
||||
var label = !string.IsNullOrWhiteSpace(targetDisplayName) ? targetDisplayName! : uid;
|
||||
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, null, label, DateTime.UtcNow);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Direct pair request failed for {uid}", uid);
|
||||
if (!string.IsNullOrEmpty(targetKey))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_activeCooldowns.Remove(targetKey);
|
||||
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||
{
|
||||
tracker = new RefusalTracker();
|
||||
_refusalTrackers[targetKey] = tracker;
|
||||
}
|
||||
|
||||
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||
{
|
||||
tracker.LockUntil = null;
|
||||
tracker.Count = 0;
|
||||
}
|
||||
|
||||
tracker.Count++;
|
||||
if (tracker.Count >= 3)
|
||||
{
|
||||
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||
}
|
||||
}
|
||||
_pendingRequests.TryRemove(targetKey, out _);
|
||||
}
|
||||
|
||||
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BuildTargetKey(string? uid, string? token, string? displayName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;
|
||||
if (!string.IsNullOrEmpty(token)) return "token:" + token;
|
||||
if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void PublishCooldownNotification(TimeSpan remaining)
|
||||
{
|
||||
var durationText = FormatDuration(remaining);
|
||||
_mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
private void PublishLockNotification(TimeSpan remaining)
|
||||
{
|
||||
var durationText = FormatDuration(remaining);
|
||||
_mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan remaining)
|
||||
{
|
||||
if (remaining.TotalMinutes >= 1)
|
||||
{
|
||||
var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes));
|
||||
return minutes == 1 ? "1 minute" : minutes + " minutes";
|
||||
}
|
||||
|
||||
var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds));
|
||||
return seconds == 1 ? "1 seconde" : seconds + " secondes";
|
||||
}
|
||||
|
||||
private sealed class RefusalTracker
|
||||
{
|
||||
public int Count;
|
||||
public DateTime? LockUntil;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<PendingRequestInfo> GetPendingRequestsSnapshot()
|
||||
{
|
||||
return _pendingRequests.Values.OrderByDescending(v => v.SentAt).ToList();
|
||||
}
|
||||
|
||||
public void RemovePendingRequestByUid(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) return;
|
||||
foreach (var kvp in _pendingRequests)
|
||||
{
|
||||
if (string.Equals(kvp.Value.Uid, uid, StringComparison.Ordinal))
|
||||
{
|
||||
_pendingRequests.TryRemove(kvp.Key, out _);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePendingRequestByKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
_pendingRequests.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
private static string EnsureTargetKey(string? targetKey)
|
||||
{
|
||||
return !string.IsNullOrEmpty(targetKey) ? targetKey! : "target:" + Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
public sealed record PendingRequestInfo(string Key, string? Uid, string? Token, string TargetDisplayName, DateTime SentAt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.Sheets;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.AutoDetect;
|
||||
|
||||
public sealed class AutoDetectSuppressionService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private static readonly string[] ContentTypeKeywords =
|
||||
[
|
||||
"dungeon",
|
||||
"donjon",
|
||||
"raid",
|
||||
"trial",
|
||||
"défi",
|
||||
"front",
|
||||
"frontline",
|
||||
"pvp",
|
||||
"jcj",
|
||||
"conflict",
|
||||
"conflit"
|
||||
];
|
||||
|
||||
private readonly ILogger<AutoDetectSuppressionService> _logger;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IDataManager _dataManager;
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
private bool _isSuppressed;
|
||||
private bool _hasSavedState;
|
||||
private bool _savedDiscoveryEnabled;
|
||||
private bool _savedAllowRequests;
|
||||
private bool _suppressionWarningShown;
|
||||
public bool IsSuppressed => _isSuppressed;
|
||||
|
||||
public AutoDetectSuppressionService(ILogger<AutoDetectSuppressionService> logger,
|
||||
MareConfigService configService, IClientState clientState,
|
||||
IDataManager dataManager, DalamudUtilService dalamudUtilService, MareMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_clientState = clientState;
|
||||
_dataManager = dataManager;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public MareMediator Mediator => _mediator;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => UpdateSuppressionState());
|
||||
_mediator.Subscribe<DalamudLoginMessage>(this, _ => UpdateSuppressionState());
|
||||
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearSuppression());
|
||||
UpdateSuppressionState();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void UpdateSuppressionState()
|
||||
{
|
||||
_ = _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
|
||||
{
|
||||
ClearSuppression();
|
||||
return;
|
||||
}
|
||||
|
||||
uint territoryId = _clientState.TerritoryType;
|
||||
bool shouldSuppress = ShouldSuppressForTerritory(territoryId);
|
||||
ApplySuppression(shouldSuppress, territoryId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to update AutoDetect suppression state");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplySuppression(bool shouldSuppress, uint territoryId)
|
||||
{
|
||||
if (shouldSuppress)
|
||||
{
|
||||
if (!_isSuppressed)
|
||||
{
|
||||
_savedDiscoveryEnabled = _configService.Current.EnableAutoDetectDiscovery;
|
||||
_savedAllowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
||||
_hasSavedState = true;
|
||||
_isSuppressed = true;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (_configService.Current.EnableAutoDetectDiscovery)
|
||||
{
|
||||
_configService.Current.EnableAutoDetectDiscovery = false;
|
||||
changed = true;
|
||||
}
|
||||
if (_configService.Current.AllowAutoDetectPairRequests)
|
||||
{
|
||||
_configService.Current.AllowAutoDetectPairRequests = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
_logger.LogInformation("AutoDetect temporarily disabled in instanced content (territory {territoryId}).", territoryId);
|
||||
if (!_suppressionWarningShown)
|
||||
{
|
||||
_suppressionWarningShown = true;
|
||||
const string warningText = "Zone instanciée détectée : les fonctions AutoDetect/Nearby sont coupées pour économiser de la bande passante.";
|
||||
_mediator.Publish(new DualNotificationMessage("AutoDetect désactivé",
|
||||
warningText,
|
||||
NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_isSuppressed) return;
|
||||
|
||||
bool changed = false;
|
||||
bool wasSuppressed = _suppressionWarningShown;
|
||||
if (_hasSavedState)
|
||||
{
|
||||
if (_configService.Current.EnableAutoDetectDiscovery != _savedDiscoveryEnabled)
|
||||
{
|
||||
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||
changed = true;
|
||||
}
|
||||
if (_configService.Current.AllowAutoDetectPairRequests != _savedAllowRequests)
|
||||
{
|
||||
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
_isSuppressed = false;
|
||||
_hasSavedState = false;
|
||||
_suppressionWarningShown = false;
|
||||
|
||||
if (changed || wasSuppressed)
|
||||
{
|
||||
_logger.LogInformation("AutoDetect restored after leaving instanced content (territory {territoryId}).", territoryId);
|
||||
const string restoredText = "Vous avez quitté la zone instanciée : AutoDetect/Nearby fonctionnent de nouveau.";
|
||||
_mediator.Publish(new DualNotificationMessage("AutoDetect réactivé",
|
||||
restoredText,
|
||||
NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSuppression()
|
||||
{
|
||||
if (!_isSuppressed) return;
|
||||
_isSuppressed = false;
|
||||
if (_hasSavedState)
|
||||
{
|
||||
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||
}
|
||||
_hasSavedState = false;
|
||||
_suppressionWarningShown = false;
|
||||
}
|
||||
|
||||
private bool ShouldSuppressForTerritory(uint territoryId)
|
||||
{
|
||||
if (territoryId == 0) return false;
|
||||
|
||||
var cfcSheet = _dataManager.GetExcelSheet<ContentFinderCondition>();
|
||||
if (cfcSheet == null) return false;
|
||||
|
||||
var cfc = cfcSheet.FirstOrDefault(c => c.TerritoryType.RowId == territoryId);
|
||||
if (cfc.RowId == 0) return false;
|
||||
|
||||
if (MatchesSuppressionKeyword(cfc.Name.ToString())) return true;
|
||||
|
||||
var contentType = cfc.ContentType.Value;
|
||||
if (contentType.RowId == 0) return false;
|
||||
|
||||
return MatchesSuppressionKeyword(contentType.Name.ToString());
|
||||
}
|
||||
|
||||
private static bool MatchesSuppressionKeyword(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
return ContentTypeKeywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -23,6 +25,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
_api = api;
|
||||
_requestService = requestService;
|
||||
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
||||
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
|
||||
}
|
||||
|
||||
public MareMediator Mediator => _mediator;
|
||||
@@ -41,6 +44,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
{
|
||||
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
||||
_pending.TryRemove(uidA, out _);
|
||||
_requestService.RemovePendingRequestByUid(uidA);
|
||||
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
||||
}
|
||||
return;
|
||||
@@ -64,9 +68,24 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
||||
}
|
||||
|
||||
private void OnManualPairInvite(ManualPairInviteMessage msg)
|
||||
{
|
||||
if (!string.Equals(msg.TargetUid, _api.UID, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var display = !string.IsNullOrWhiteSpace(msg.DisplayName)
|
||||
? msg.DisplayName!
|
||||
: (!string.IsNullOrWhiteSpace(msg.SourceAlias) ? msg.SourceAlias : msg.SourceUid);
|
||||
|
||||
_pending[msg.SourceUid] = display;
|
||||
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
|
||||
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
public void Remove(string uid)
|
||||
{
|
||||
_pending.TryRemove(uid, out _);
|
||||
_requestService.RemovePendingRequestByUid(uid);
|
||||
}
|
||||
|
||||
public async Task<bool> AcceptAsync(string uid)
|
||||
@@ -75,6 +94,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
{
|
||||
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
|
||||
_pending.TryRemove(uid, out _);
|
||||
_requestService.RemovePendingRequestByUid(uid);
|
||||
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
||||
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
||||
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
||||
|| (_serverConfigurationManager.GetNoteForUid(d.Key.UID) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToDictionary(k => k.Key, k => k.Value))
|
||||
{
|
||||
// filter all poses based on territory, that always must be correct
|
||||
@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
||||
{
|
||||
foreach (var data in _nearbyData.Keys)
|
||||
int maxWisps = _charaDataConfigService.Current.NearbyMaxWisps;
|
||||
if (maxWisps <= 0)
|
||||
{
|
||||
if (_poseVfx.TryGetValue(data, out var _)) continue;
|
||||
ClearAllVfx();
|
||||
return;
|
||||
}
|
||||
|
||||
Guid? vfxGuid;
|
||||
if (data.MetaInfo.IsOwnData)
|
||||
const int hardLimit = 200;
|
||||
if (maxWisps > hardLimit) maxWisps = hardLimit;
|
||||
|
||||
var orderedAllowedPoses = _nearbyData
|
||||
.OrderBy(kvp => kvp.Value.Distance)
|
||||
.Take(maxWisps)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
var allowedPoseSet = orderedAllowedPoses.ToHashSet();
|
||||
|
||||
foreach (var data in orderedAllowedPoses)
|
||||
{
|
||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
|
||||
}
|
||||
else
|
||||
{
|
||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
||||
}
|
||||
if (_poseVfx.TryGetValue(data, out _)) continue;
|
||||
|
||||
Guid? vfxGuid = data.MetaInfo.IsOwnData
|
||||
? _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f)
|
||||
: _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
||||
|
||||
if (vfxGuid != null)
|
||||
{
|
||||
_poseVfx[data] = vfxGuid.Value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var data in previousPoses.Except(_nearbyData.Keys))
|
||||
foreach (var data in previousPoses.Except(allowedPoseSet))
|
||||
{
|
||||
if (_poseVfx.Remove(data, out var guid))
|
||||
{
|
||||
_vfxSpawnManager.DespawnObject(guid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var data in _poseVfx.Keys.Except(allowedPoseSet).ToList())
|
||||
{
|
||||
if (_poseVfx.Remove(data, out var guid))
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Lumina.Data.Files;
|
||||
using System;
|
||||
using Lumina.Data.Files;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.UI;
|
||||
using MareSynchronos.Utils;
|
||||
@@ -13,11 +16,22 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private CancellationTokenSource? _analysisCts;
|
||||
private CancellationTokenSource _baseAnalysisCts = new();
|
||||
private string _lastDataHash = string.Empty;
|
||||
private CharacterAnalysisSummary _previousSummary = CharacterAnalysisSummary.Empty;
|
||||
private DateTime _lastAutoAnalysis = DateTime.MinValue;
|
||||
private string _lastAutoAnalysisHash = string.Empty;
|
||||
private const int AutoAnalysisFileDeltaThreshold = 25;
|
||||
private const long AutoAnalysisSizeDeltaThreshold = 50L * 1024 * 1024;
|
||||
private static readonly TimeSpan AutoAnalysisCooldown = TimeSpan.FromMinutes(2);
|
||||
private const long NotificationSizeThreshold = 300L * 1024 * 1024;
|
||||
private const long NotificationTriangleThreshold = 150_000;
|
||||
private bool _sizeWarningShown;
|
||||
private bool _triangleWarningShown;
|
||||
|
||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer, PlayerPerformanceConfigService playerPerformanceConfigService)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||
@@ -28,11 +42,14 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
||||
});
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_xivDataAnalyzer = modelAnalyzer;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
}
|
||||
|
||||
public int CurrentFile { get; internal set; }
|
||||
public bool IsAnalysisRunning => _analysisCts != null;
|
||||
public int TotalFiles { get; internal set; }
|
||||
public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty;
|
||||
public DateTime? LastCompletedAnalysis { get; private set; }
|
||||
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
||||
|
||||
public void CancelAnalyze()
|
||||
@@ -80,8 +97,15 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
RefreshSummary(false, _lastDataHash);
|
||||
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
|
||||
if (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
LastCompletedAnalysis = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_analysisCts.CancelDispose();
|
||||
_analysisCts = null;
|
||||
|
||||
@@ -142,9 +166,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
||||
LastAnalysis[obj.Key] = data;
|
||||
}
|
||||
|
||||
_lastDataHash = charaData.DataHash.Value;
|
||||
RefreshSummary(true, _lastDataHash);
|
||||
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
|
||||
_lastDataHash = charaData.DataHash.Value;
|
||||
}
|
||||
|
||||
private void PrintAnalysis()
|
||||
@@ -193,6 +219,168 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||
}
|
||||
|
||||
private void RefreshSummary(bool evaluateAutoAnalysis, string dataHash)
|
||||
{
|
||||
var summary = CalculateSummary();
|
||||
CurrentSummary = summary;
|
||||
|
||||
if (evaluateAutoAnalysis)
|
||||
{
|
||||
EvaluateAutoAnalysis(summary, dataHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_previousSummary = summary;
|
||||
|
||||
if (!summary.HasUncomputedEntries && string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||
{
|
||||
_lastAutoAnalysisHash = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
EvaluateThresholdNotifications(summary);
|
||||
}
|
||||
|
||||
private CharacterAnalysisSummary CalculateSummary()
|
||||
{
|
||||
if (LastAnalysis.Count == 0)
|
||||
{
|
||||
return CharacterAnalysisSummary.Empty;
|
||||
}
|
||||
|
||||
long original = 0;
|
||||
long compressed = 0;
|
||||
long triangles = 0;
|
||||
int files = 0;
|
||||
bool hasUncomputed = false;
|
||||
|
||||
foreach (var obj in LastAnalysis.Values)
|
||||
{
|
||||
foreach (var entry in obj.Values)
|
||||
{
|
||||
files++;
|
||||
original += entry.OriginalSize;
|
||||
compressed += entry.CompressedSize;
|
||||
triangles += entry.Triangles;
|
||||
hasUncomputed |= !entry.IsComputed;
|
||||
}
|
||||
}
|
||||
|
||||
return new CharacterAnalysisSummary(files, original, compressed, triangles, hasUncomputed);
|
||||
}
|
||||
|
||||
private void EvaluateAutoAnalysis(CharacterAnalysisSummary newSummary, string dataHash)
|
||||
{
|
||||
var previous = _previousSummary;
|
||||
_previousSummary = newSummary;
|
||||
|
||||
if (newSummary.TotalFiles == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAnalysisRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - _lastAutoAnalysis < AutoAnalysisCooldown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool firstSummary = previous.TotalFiles == 0;
|
||||
bool filesIncreased = newSummary.TotalFiles - previous.TotalFiles >= AutoAnalysisFileDeltaThreshold;
|
||||
bool sizeIncreased = newSummary.TotalCompressedSize - previous.TotalCompressedSize >= AutoAnalysisSizeDeltaThreshold;
|
||||
bool needsCompute = newSummary.HasUncomputedEntries;
|
||||
|
||||
if (!firstSummary && !filesIncreased && !sizeIncreased && !needsCompute)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastAutoAnalysis = now;
|
||||
_lastAutoAnalysisHash = dataHash;
|
||||
_ = ComputeAnalysis(print: false);
|
||||
}
|
||||
|
||||
private void EvaluateThresholdNotifications(CharacterAnalysisSummary summary)
|
||||
{
|
||||
if (summary.IsEmpty || summary.HasUncomputedEntries)
|
||||
{
|
||||
ResetThresholdFlagsIfNeeded(summary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings)
|
||||
{
|
||||
ResetThresholdFlagsIfNeeded(summary);
|
||||
return;
|
||||
}
|
||||
|
||||
bool sizeExceeded = summary.TotalCompressedSize >= NotificationSizeThreshold;
|
||||
bool trianglesExceeded = summary.TotalTriangles >= NotificationTriangleThreshold;
|
||||
List<string> exceededReasons = new();
|
||||
|
||||
if (sizeExceeded && !_sizeWarningShown)
|
||||
{
|
||||
exceededReasons.Add($"un poids partagé de {UiSharedService.ByteToString(summary.TotalCompressedSize)} (≥ 300 MiB)");
|
||||
_sizeWarningShown = true;
|
||||
}
|
||||
else if (!sizeExceeded && _sizeWarningShown)
|
||||
{
|
||||
_sizeWarningShown = false;
|
||||
}
|
||||
|
||||
if (trianglesExceeded && !_triangleWarningShown)
|
||||
{
|
||||
exceededReasons.Add($"un total de {UiSharedService.TrisToString(summary.TotalTriangles)} triangles (≥ 150k)");
|
||||
_triangleWarningShown = true;
|
||||
}
|
||||
else if (!trianglesExceeded && _triangleWarningShown)
|
||||
{
|
||||
_triangleWarningShown = false;
|
||||
}
|
||||
|
||||
if (exceededReasons.Count == 0) return;
|
||||
|
||||
string combined = string.Join(" et ", exceededReasons);
|
||||
string message = $"Attention : votre self-analysis indique {combined}. Des joueurs risquent de ne pas vous voir et UmbraSync peut activer un auto-pause. Pensez à réduire textures ou modèles lourds.";
|
||||
Mediator.Publish(new DualNotificationMessage("Self Analysis", message, NotificationType.Warning));
|
||||
}
|
||||
|
||||
private void ResetThresholdFlagsIfNeeded(CharacterAnalysisSummary summary)
|
||||
{
|
||||
if (summary.IsEmpty)
|
||||
{
|
||||
_sizeWarningShown = false;
|
||||
_triangleWarningShown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.TotalCompressedSize < NotificationSizeThreshold)
|
||||
{
|
||||
_sizeWarningShown = false;
|
||||
}
|
||||
|
||||
if (summary.TotalTriangles < NotificationTriangleThreshold)
|
||||
{
|
||||
_triangleWarningShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct CharacterAnalysisSummary(int TotalFiles, long TotalOriginalSize, long TotalCompressedSize, long TotalTriangles, bool HasUncomputedEntries)
|
||||
{
|
||||
public static CharacterAnalysisSummary Empty => new();
|
||||
public bool IsEmpty => TotalFiles == 0 && TotalOriginalSize == 0 && TotalCompressedSize == 0 && TotalTriangles == 0;
|
||||
}
|
||||
|
||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||
{
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
@@ -18,6 +21,7 @@ namespace MareSynchronos.Services;
|
||||
public class ChatService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
public const int DefaultColor = 710;
|
||||
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
|
||||
public const int CommandMaxNumber = 50;
|
||||
|
||||
private readonly ILogger<ChatService> _logger;
|
||||
@@ -30,6 +34,13 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
|
||||
private readonly Lazy<GameChatHooks> _gameChatHooks;
|
||||
|
||||
private readonly object _typingLock = new();
|
||||
private CancellationTokenSource? _typingCts;
|
||||
private bool _isTypingAnnounced;
|
||||
private DateTime _lastTypingSent = DateTime.MinValue;
|
||||
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
||||
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
||||
|
||||
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
|
||||
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
|
||||
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
||||
@@ -46,13 +57,12 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
|
||||
|
||||
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
|
||||
|
||||
// Initialize chat hooks in advance
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = _gameChatHooks.Value;
|
||||
_isTypingAnnounced = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -64,9 +74,80 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_typingCts?.Cancel();
|
||||
_typingCts?.Dispose();
|
||||
if (_gameChatHooks.IsValueCreated)
|
||||
_gameChatHooks.Value!.Dispose();
|
||||
}
|
||||
public void NotifyTypingKeystroke()
|
||||
{
|
||||
lock (_typingLock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (!_isTypingAnnounced || (now - _lastTypingSent) >= TypingResendInterval)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
|
||||
});
|
||||
_isTypingAnnounced = true;
|
||||
_lastTypingSent = now;
|
||||
}
|
||||
|
||||
_typingCts?.Cancel();
|
||||
_typingCts?.Dispose();
|
||||
_typingCts = new CancellationTokenSource();
|
||||
var token = _typingCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
||||
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// reset timer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=false");
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_typingLock)
|
||||
{
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
_isTypingAnnounced = false;
|
||||
_lastTypingSent = DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
public void ClearTypingState()
|
||||
{
|
||||
lock (_typingLock)
|
||||
{
|
||||
_typingCts?.Cancel();
|
||||
_typingCts?.Dispose();
|
||||
_typingCts = null;
|
||||
if (_isTypingAnnounced)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
|
||||
});
|
||||
_isTypingAnnounced = false;
|
||||
_lastTypingSent = DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUserChat(UserChatMsgMessage message)
|
||||
{
|
||||
@@ -113,6 +194,10 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
var extraChatTags = _mareConfig.Current.ExtraChatTags;
|
||||
var logKind = ResolveShellLogKind(shellConfig.LogKind);
|
||||
|
||||
var payload = SeString.Parse(message.ChatMsg.PayloadContent);
|
||||
if (TryHandleManualPairInvite(message, payload))
|
||||
return;
|
||||
|
||||
var msg = new SeStringBuilder();
|
||||
if (extraChatTags)
|
||||
{
|
||||
@@ -124,7 +209,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
msg.AddText($"[SS{shellNumber}]<");
|
||||
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
|
||||
{
|
||||
// Don't link to your own character
|
||||
msg.AddText(chatMsg.SenderName);
|
||||
}
|
||||
else
|
||||
@@ -132,7 +216,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
|
||||
}
|
||||
msg.AddText("> ");
|
||||
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent));
|
||||
msg.Append(payload);
|
||||
if (color != 0)
|
||||
msg.AddUiForegroundOff();
|
||||
|
||||
@@ -143,7 +227,51 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
// Print an example message to the configured global chat channel
|
||||
private bool TryHandleManualPairInvite(GroupChatMsgMessage message, SeString payload)
|
||||
{
|
||||
var textValue = payload.TextValue;
|
||||
if (string.IsNullOrEmpty(textValue) || !textValue.StartsWith(ManualPairInvitePrefix, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var content = textValue[ManualPairInvitePrefix.Length..];
|
||||
if (content.EndsWith("]", StringComparison.Ordinal))
|
||||
{
|
||||
content = content[..^1];
|
||||
}
|
||||
|
||||
var parts = content.Split('|');
|
||||
if (parts.Length < 4)
|
||||
return true;
|
||||
|
||||
var sourceUid = parts[0];
|
||||
var sourceAlias = DecodeInviteField(parts[1]);
|
||||
var targetUid = parts[2];
|
||||
var displayName = DecodeInviteField(parts[3]);
|
||||
var inviteId = parts.Length > 4 ? parts[4] : Guid.NewGuid().ToString("N");
|
||||
|
||||
if (!string.Equals(targetUid, _apiController.UID, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
Mediator.Publish(new ManualPairInviteMessage(sourceUid, sourceAlias, targetUid, string.IsNullOrEmpty(displayName) ? null : displayName, inviteId));
|
||||
_logger.LogDebug("Received manual pair invite from {source} via syncshell", sourceUid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string DecodeInviteField(string encoded)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encoded)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(encoded);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return encoded;
|
||||
}
|
||||
}
|
||||
public void PrintChannelExample(string message, string gid = "")
|
||||
{
|
||||
int chatType = _mareConfig.Current.ChatLogKind;
|
||||
@@ -164,8 +292,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
Type = (XivChatType)chatType
|
||||
});
|
||||
}
|
||||
|
||||
// Called to update the active chat shell name if its renamed
|
||||
public void MaybeUpdateShellName(int shellNumber)
|
||||
{
|
||||
if (_mareConfig.Current.DisableSyncshellChat)
|
||||
@@ -178,7 +304,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
|
||||
{
|
||||
// Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later
|
||||
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
|
||||
SwitchChatShell(shellNumber);
|
||||
}
|
||||
@@ -197,7 +322,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||
{
|
||||
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
|
||||
// BUG: This doesn't always update the chat window e.g. when renaming a group
|
||||
_gameChatHooks.Value.ChatChannelOverride = new()
|
||||
{
|
||||
ChannelName = $"SS [{shellNumber}]: {name}",
|
||||
@@ -221,7 +345,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||
{
|
||||
_ = Task.Run(async () => {
|
||||
// Should cache the name and home world instead of fetching it every time
|
||||
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
|
||||
return new ChatMessage()
|
||||
{
|
||||
@@ -230,6 +353,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
PayloadContent = chatBytes
|
||||
};
|
||||
}).ConfigureAwait(false);
|
||||
ClearTypingState();
|
||||
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
return;
|
||||
|
||||
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Plugin;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class ChatTwoCompatibilityService : MediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private const string ChatTwoInternalName = "ChatTwo";
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private bool _warningShown;
|
||||
|
||||
public ChatTwoCompatibilityService(ILogger<ChatTwoCompatibilityService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, ChatTwoInternalName, OnChatTwoStateChanged);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var initialState = PluginWatcherService.GetInitialPluginState(_pluginInterface, ChatTwoInternalName);
|
||||
if (initialState?.IsLoaded == true)
|
||||
{
|
||||
ShowWarning();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to inspect ChatTwo initial state");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnChatTwoStateChanged(PluginChangeMessage message)
|
||||
{
|
||||
if (message.IsLoaded)
|
||||
{
|
||||
ShowWarning();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWarning()
|
||||
{
|
||||
if (_warningShown) return;
|
||||
_warningShown = true;
|
||||
|
||||
const string warningTitle = "ChatTwo détecté";
|
||||
const string warningBody = "Actuellement, le plugin ChatTwo n'est pas compatible avec la bulle d'écriture d'UmbraSync. Désactivez ChatTwo si vous souhaitez conserver l'indicateur de saisie.";
|
||||
|
||||
Mediator.Publish(new NotificationMessage(warningTitle, warningBody, NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
294
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
294
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Game.Text;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.WebAPI;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class ChatTypingDetectionService : IDisposable
|
||||
{
|
||||
private readonly ILogger<ChatTypingDetectionService> _logger;
|
||||
private readonly IFramework _framework;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly TypingIndicatorStateService _typingStateService;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly IPartyList _partyList;
|
||||
|
||||
private string _lastChatText = string.Empty;
|
||||
private bool _isTyping;
|
||||
private bool _notifyingRemote;
|
||||
private bool _serverSupportWarnLogged;
|
||||
private bool _remoteNotificationsEnabled;
|
||||
|
||||
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
||||
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
||||
TypingIndicatorStateService typingStateService, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_chatService = chatService;
|
||||
_pairManager = pairManager;
|
||||
_partyList = partyList;
|
||||
_typingStateService = typingStateService;
|
||||
_apiController = apiController;
|
||||
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
_logger.LogInformation("ChatTypingDetectionService initialized");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
ResetTypingState();
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_clientState.IsLoggedIn)
|
||||
{
|
||||
ResetTypingState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
||||
{
|
||||
ResetTypingState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsIgnoredCommand(chatText))
|
||||
{
|
||||
ResetTypingState();
|
||||
return;
|
||||
}
|
||||
|
||||
var notifyRemote = ShouldNotifyRemote();
|
||||
UpdateRemoteNotificationLogState(notifyRemote);
|
||||
if (!notifyRemote && _notifyingRemote)
|
||||
{
|
||||
_chatService.ClearTypingState();
|
||||
_notifyingRemote = false;
|
||||
}
|
||||
|
||||
if (!_isTyping || !string.Equals(chatText, _lastChatText, StringComparison.Ordinal))
|
||||
{
|
||||
if (notifyRemote)
|
||||
{
|
||||
_chatService.NotifyTypingKeystroke();
|
||||
_notifyingRemote = true;
|
||||
}
|
||||
|
||||
_typingStateService.SetSelfTypingLocal(true);
|
||||
_isTyping = true;
|
||||
}
|
||||
|
||||
_lastChatText = chatText;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "ChatTypingDetectionService tick failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetTypingState()
|
||||
{
|
||||
if (!_isTyping)
|
||||
{
|
||||
_lastChatText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
_isTyping = false;
|
||||
_lastChatText = string.Empty;
|
||||
_chatService.ClearTypingState();
|
||||
_notifyingRemote = false;
|
||||
_typingStateService.SetSelfTypingLocal(false);
|
||||
}
|
||||
|
||||
private static bool IsIgnoredCommand(string chatText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chatText))
|
||||
return false;
|
||||
|
||||
var trimmed = chatText.TrimStart();
|
||||
if (!trimmed.StartsWith('/'))
|
||||
return false;
|
||||
|
||||
var firstTokenEnd = trimmed.IndexOf(' ');
|
||||
var command = firstTokenEnd >= 0 ? trimmed[..firstTokenEnd] : trimmed;
|
||||
command = command.TrimEnd();
|
||||
|
||||
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||
return command.StartsWith("/tell", comparison)
|
||||
|| command.StartsWith("/t", comparison)
|
||||
|| command.StartsWith("/xllog", comparison)
|
||||
|| command.StartsWith("/umbra", comparison)
|
||||
|| command.StartsWith("/fc", comparison)
|
||||
|| command.StartsWith("/freecompany", comparison);
|
||||
}
|
||||
|
||||
private unsafe bool ShouldNotifyRemote()
|
||||
{
|
||||
try
|
||||
{
|
||||
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
||||
var connected = _apiController.IsConnected;
|
||||
if (!connected || !supportsTypingState)
|
||||
{
|
||||
if (!_serverSupportWarnLogged)
|
||||
{
|
||||
_logger.LogDebug("TypingDetection: server support unavailable (connected={connected}, supports={supports})", connected, supportsTypingState);
|
||||
_serverSupportWarnLogged = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_serverSupportWarnLogged = false;
|
||||
|
||||
var shellModule = RaptureShellModule.Instance();
|
||||
if (shellModule == null)
|
||||
{
|
||||
_logger.LogDebug("TypingDetection: shell module null");
|
||||
return true;
|
||||
}
|
||||
|
||||
var chatType = (XivChatType)shellModule->ChatType;
|
||||
switch (chatType)
|
||||
{
|
||||
case XivChatType.Say:
|
||||
case XivChatType.Shout:
|
||||
case XivChatType.Yell:
|
||||
return true;
|
||||
case XivChatType.Party:
|
||||
case XivChatType.CrossParty:
|
||||
var eligible = PartyContainsPairedMember();
|
||||
return eligible;
|
||||
case XivChatType.Debug:
|
||||
return true;
|
||||
default:
|
||||
_logger.LogTrace("TypingDetection: channel {type} rejected", chatType);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "ChatTypingDetectionService: failed to evaluate chat channel");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool PartyContainsPairedMember()
|
||||
{
|
||||
try
|
||||
{
|
||||
var pairedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pair in _pairManager.GetOnlineUserPairs())
|
||||
{
|
||||
if (!string.IsNullOrEmpty(pair.PlayerName))
|
||||
pairedNames.Add(pair.PlayerName);
|
||||
}
|
||||
|
||||
if (pairedNames.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("TypingDetection: no paired names online");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var member in _partyList)
|
||||
{
|
||||
var name = member?.Name?.TextValue;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
|
||||
if (pairedNames.Contains(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "ChatTypingDetectionService: failed to check party composition");
|
||||
}
|
||||
|
||||
_logger.LogDebug("TypingDetection: no paired members in party");
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe bool TryGetChatInput(out string chatText)
|
||||
{
|
||||
chatText = string.Empty;
|
||||
|
||||
var addon = _gameGui.GetAddonByName("ChatLog", 1);
|
||||
if (addon.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
var chatLog = (AtkUnitBase*)addon.Address;
|
||||
if (chatLog == null || !chatLog->IsVisible)
|
||||
return false;
|
||||
|
||||
var textInputNode = chatLog->UldManager.NodeList[16];
|
||||
if (textInputNode == null)
|
||||
return false;
|
||||
|
||||
var componentNode = textInputNode->GetAsAtkComponentNode();
|
||||
if (componentNode == null || componentNode->Component == null)
|
||||
return false;
|
||||
|
||||
var cursorNode = componentNode->Component->UldManager.NodeList[14];
|
||||
if (cursorNode == null)
|
||||
return false;
|
||||
|
||||
var cursorVisible = cursorNode->IsVisible();
|
||||
if (!cursorVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var chatInputNode = componentNode->Component->UldManager.NodeList[1];
|
||||
if (chatInputNode == null)
|
||||
return false;
|
||||
|
||||
var textNode = chatInputNode->GetAsAtkTextNode();
|
||||
if (textNode == null)
|
||||
return false;
|
||||
|
||||
var rawText = textNode->GetText();
|
||||
if (rawText == null)
|
||||
return false;
|
||||
|
||||
chatText = rawText.AsDalamudSeString().ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateRemoteNotificationLogState(bool notifyRemote)
|
||||
{
|
||||
if (notifyRemote && !_remoteNotificationsEnabled)
|
||||
{
|
||||
_remoteNotificationsEnabled = true;
|
||||
_logger.LogInformation("TypingDetection: remote notifications enabled");
|
||||
}
|
||||
else if (!notifyRemote && _remoteNotificationsEnabled)
|
||||
{
|
||||
_remoteNotificationsEnabled = false;
|
||||
_logger.LogInformation("TypingDetection: remote notifications disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,14 @@ using MareSynchronos.UI;
|
||||
using MareSynchronos.WebAPI;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Numerics;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
private const string _commandName = "/usync";
|
||||
private const string _autoDetectCommand = "/autodetect";
|
||||
private const string _ssCommandPrefix = "/ums";
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
@@ -43,6 +45,11 @@ public sealed class CommandManagerService : IDisposable
|
||||
HelpMessage = "Opens the UmbraSync UI"
|
||||
});
|
||||
|
||||
_commandManager.AddHandler(_autoDetectCommand, new CommandInfo(OnAutoDetectCommand)
|
||||
{
|
||||
HelpMessage = "Opens the AutoDetect window"
|
||||
});
|
||||
|
||||
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||
{
|
||||
@@ -56,12 +63,21 @@ public sealed class CommandManagerService : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_commandManager.RemoveHandler(_commandName);
|
||||
|
||||
_commandManager.RemoveHandler(_autoDetectCommand);
|
||||
|
||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
||||
}
|
||||
|
||||
private void OnAutoDetectCommand(string command, string args)
|
||||
{
|
||||
UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
|
||||
UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||
UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
|
||||
_mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||
}
|
||||
|
||||
|
||||
private void OnCommand(string command, string args)
|
||||
{
|
||||
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
@@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
||||
|
||||
@@ -52,6 +54,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<DalamudUtilService> _logger;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly Dictionary<string, ConditionFlag> _conditionLookup = new(StringComparer.OrdinalIgnoreCase);
|
||||
private uint? _classJobId = 0;
|
||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
private string _lastGlobalBlockPlayer = string.Empty;
|
||||
@@ -172,6 +175,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public bool IsInCombatOrPerforming { get; private set; } = false;
|
||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||
|
||||
public bool IsConditionActive(string flagName)
|
||||
{
|
||||
if (_conditionLookup.TryGetValue(flagName, out var cachedFlag))
|
||||
return _condition[cachedFlag];
|
||||
|
||||
if (Enum.TryParse<ConditionFlag>(flagName, true, out var flag))
|
||||
{
|
||||
_conditionLookup[flagName] = flag;
|
||||
return _condition[flag];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using Dalamud.Game.Gui.NamePlate;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.UI;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Dalamud.Game.Text;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
@@ -19,12 +23,18 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
private readonly IGameConfig _gameConfig;
|
||||
private readonly IPartyList _partyList;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly TypingIndicatorStateService _typingStateService;
|
||||
|
||||
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
private bool _isModified = false;
|
||||
private bool _namePlateRoleColorsEnabled = false;
|
||||
|
||||
public GuiHookService(ILogger<GuiHookService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, MareConfigService configService,
|
||||
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager)
|
||||
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager, ApiController apiController,
|
||||
IClientState clientState, TypingIndicatorStateService typingStateService)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -34,6 +44,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
_gameConfig = gameConfig;
|
||||
_partyList = partyList;
|
||||
_pairManager = pairManager;
|
||||
_apiController = apiController;
|
||||
_clientState = clientState;
|
||||
_typingStateService = typingStateService;
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
@@ -41,16 +54,24 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
|
||||
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
|
||||
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
|
||||
Mediator.Subscribe<UserTypingStateMessage>(this, (_) => RequestRedraw());
|
||||
}
|
||||
|
||||
public void RequestRedraw(bool force = false)
|
||||
{
|
||||
if (!_configService.Current.UseNameColors)
|
||||
var useColors = _configService.Current.UseNameColors;
|
||||
var showTyping = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||
|
||||
if (!useColors && !showTyping)
|
||||
{
|
||||
if (!_isModified && !force)
|
||||
return;
|
||||
_isModified = false;
|
||||
}
|
||||
else if (!useColors)
|
||||
{
|
||||
_isModified = false;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () => {
|
||||
await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw()).ConfigureAwait(false);
|
||||
@@ -69,7 +90,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
{
|
||||
if (!_configService.Current.UseNameColors)
|
||||
var applyColors = _configService.Current.UseNameColors;
|
||||
var showTypingIndicator = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||
if (!applyColors && !showTypingIndicator)
|
||||
return;
|
||||
|
||||
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
|
||||
@@ -82,6 +105,11 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
for (int i = 0; i < _partyList.Count; ++i)
|
||||
partyMembers[i] = _partyList[i]?.GameObject?.Address ?? nint.MaxValue;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||
var selfTypingActive = showTypingIndicator && _typingStateService.TryGetSelfTyping(TypingDisplayTime, out _, out _);
|
||||
var localPlayerAddress = selfTypingActive ? _clientState.LocalPlayer?.Address ?? nint.Zero : nint.Zero;
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
if (handler != null && visibleUsersIds.Contains(handler.GameObjectId))
|
||||
@@ -89,6 +117,8 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
|
||||
continue;
|
||||
var pair = visibleUsersDict[handler.GameObjectId];
|
||||
if (applyColors)
|
||||
{
|
||||
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
|
||||
handler.NameParts.TextWrap = (
|
||||
BuildColorStartSeString(colors),
|
||||
@@ -96,6 +126,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
||||
);
|
||||
_isModified = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
@@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record NotificationMessage
|
||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||
public record DualNotificationMessage(string Title, string Message, NotificationType Type, TimeSpan? ToastDuration = null) : MessageBase;
|
||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||
@@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas
|
||||
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
||||
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
||||
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
||||
public record UserTypingStateMessage(TypingStateDto Typing) : MessageBase;
|
||||
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
||||
public record NameplateRedrawMessage : MessageBase;
|
||||
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
||||
@@ -112,6 +115,12 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
|
||||
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
||||
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
||||
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
|
||||
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
|
||||
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
||||
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
|
||||
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
|
||||
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||
|
||||
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
||||
#pragma warning restore S2094
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using System;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
@@ -31,6 +32,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -81,41 +83,103 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
|
||||
switch (msg.Type)
|
||||
bool appendInstruction;
|
||||
bool forceChat = ShouldForceChat(msg, out appendInstruction);
|
||||
var effectiveMessage = forceChat && appendInstruction ? AppendAutoDetectInstruction(msg.Message) : msg.Message;
|
||||
var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg;
|
||||
|
||||
switch (adjustedMsg.Type)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
||||
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.InfoNotification, forceChat);
|
||||
break;
|
||||
|
||||
case NotificationType.Warning:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
||||
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.WarningNotification, forceChat);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
||||
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.ErrorNotification, forceChat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
private void ShowDualNotification(DualNotificationMessage message)
|
||||
{
|
||||
switch (location)
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
|
||||
var baseMsg = new NotificationMessage(message.Title, message.Message, message.Type, message.ToastDuration);
|
||||
ShowToast(baseMsg);
|
||||
ShowChat(baseMsg);
|
||||
}
|
||||
|
||||
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
|
||||
{
|
||||
appendInstruction = false;
|
||||
|
||||
bool IsNearbyRequestText(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return false;
|
||||
return text.Contains("Nearby request", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("Nearby Request", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
bool IsNearbyAcceptText(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return false;
|
||||
return text.Contains("Nearby Accept", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
bool isAccept = IsNearbyAcceptText(msg.Title) || IsNearbyAcceptText(msg.Message);
|
||||
if (isAccept)
|
||||
return false;
|
||||
|
||||
bool isRequest = IsNearbyRequestText(msg.Title) || IsNearbyRequestText(msg.Message);
|
||||
if (isRequest)
|
||||
{
|
||||
appendInstruction = !IsRequestSentConfirmation(msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsRequestSentConfirmation(NotificationMessage msg)
|
||||
{
|
||||
if (string.Equals(msg.Title, "Nearby request sent", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrEmpty(msg.Message) && msg.Message.Contains("The other user will receive a request notification.", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string AppendAutoDetectInstruction(string? message)
|
||||
{
|
||||
const string suffix = " | Ouvrez /autodetect pour gérer l'invitation.";
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return suffix.TrimStart(' ', '|');
|
||||
|
||||
if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase))
|
||||
return message;
|
||||
|
||||
return message.TrimEnd() + suffix;
|
||||
}
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location, bool forceChat)
|
||||
{
|
||||
bool showToast = location is NotificationLocation.Toast or NotificationLocation.Both;
|
||||
bool showChat = forceChat || location is NotificationLocation.Chat or NotificationLocation.Both;
|
||||
|
||||
if (showToast)
|
||||
{
|
||||
case NotificationLocation.Toast:
|
||||
ShowToast(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case NotificationLocation.Chat:
|
||||
if (showChat)
|
||||
{
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Both:
|
||||
ShowToast(msg);
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
MareSynchronos/Services/PartyListTypingService.cs
Normal file
78
MareSynchronos/Services/PartyListTypingService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using System.Collections.Generic;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public class PartyListTypingService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<PartyListTypingService> _logger;
|
||||
private readonly IPartyList _partyList;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly TypingIndicatorStateService _typingStateService;
|
||||
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||
private static readonly TimeSpan TypingDisplayDelay = TimeSpan.FromMilliseconds(500);
|
||||
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
|
||||
|
||||
public PartyListTypingService(ILogger<PartyListTypingService> logger,
|
||||
MareMediator mediator,
|
||||
IPartyList partyList,
|
||||
PairManager pairManager,
|
||||
MareConfigService configService,
|
||||
TypingIndicatorStateService typingStateService)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_partyList = partyList;
|
||||
_pairManager = pairManager;
|
||||
_configService = configService;
|
||||
_typingStateService = typingStateService;
|
||||
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
||||
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
|
||||
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
var visibleUsers = _pairManager.GetVisibleUsers();
|
||||
foreach (var u in visibleUsers)
|
||||
{
|
||||
var alias = string.IsNullOrEmpty(u.AliasOrUID) ? u.UID : u.AliasOrUID;
|
||||
if (!visibleByAlias.ContainsKey(alias)) visibleByAlias[alias] = u.UID;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "PartyListTypingService: failed to get visible users");
|
||||
}
|
||||
|
||||
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var member in _partyList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(member.Name?.TextValue)) continue;
|
||||
|
||||
var displayName = member.Name.TextValue;
|
||||
if (visibleByAlias.TryGetValue(displayName, out var uid)
|
||||
&& activeTypers.TryGetValue(uid, out var entry)
|
||||
&& (now - entry.LastUpdate) <= TypingDisplayFade)
|
||||
{
|
||||
_logger.LogDebug("PartyListTypingService: bubble would be shown for {name}", displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Configurations;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class SyncDefaultsService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly PairManager _pairManager;
|
||||
|
||||
public SyncDefaultsService(ILogger<SyncDefaultsService> logger, MareMediator mediator,
|
||||
MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator)
|
||||
{
|
||||
_configService = configService;
|
||||
_apiController = apiController;
|
||||
_pairManager = pairManager;
|
||||
|
||||
Mediator.Subscribe<ApplyDefaultPairPermissionsMessage>(this, OnApplyPairDefaults);
|
||||
Mediator.Subscribe<ApplyDefaultGroupPermissionsMessage>(this, OnApplyGroupDefaults);
|
||||
Mediator.Subscribe<ApplyDefaultsToAllSyncsMessage>(this, msg => ApplyDefaultsToAll(msg));
|
||||
Mediator.Subscribe<PairSyncOverrideChanged>(this, OnPairOverrideChanged);
|
||||
Mediator.Subscribe<GroupSyncOverrideChanged>(this, OnGroupOverrideChanged);
|
||||
}
|
||||
|
||||
private void OnApplyPairDefaults(ApplyDefaultPairPermissionsMessage message)
|
||||
{
|
||||
var config = _configService.Current;
|
||||
var permissions = message.Pair.OwnPermissions;
|
||||
var overrides = TryGetPairOverride(message.Pair.User.UID);
|
||||
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||
return;
|
||||
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(message.Pair.User, permissions));
|
||||
}
|
||||
|
||||
private void OnApplyGroupDefaults(ApplyDefaultGroupPermissionsMessage message)
|
||||
{
|
||||
if (!string.Equals(message.GroupPair.User.UID, _apiController.UID, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var config = _configService.Current;
|
||||
var permissions = message.GroupPair.GroupUserPermissions;
|
||||
var overrides = TryGetGroupOverride(message.GroupPair.Group.GID);
|
||||
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||
return;
|
||||
|
||||
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(message.GroupPair.Group, message.GroupPair.User, permissions));
|
||||
}
|
||||
|
||||
private async Task ApplyDefaultsToAllAsync(ApplyDefaultsToAllSyncsMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = _configService.Current;
|
||||
var tasks = new List<Task>();
|
||||
int updatedPairs = 0;
|
||||
int updatedGroups = 0;
|
||||
|
||||
foreach (var pair in _pairManager.DirectPairs.Where(p => p.UserPair != null).ToList())
|
||||
{
|
||||
var permissions = pair.UserPair!.OwnPermissions;
|
||||
var overrides = TryGetPairOverride(pair.UserData.UID);
|
||||
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||
continue;
|
||||
|
||||
updatedPairs++;
|
||||
tasks.Add(_apiController.UserSetPairPermissions(new UserPermissionsDto(pair.UserData, permissions)));
|
||||
}
|
||||
|
||||
var selfUser = new UserData(_apiController.UID);
|
||||
foreach (var groupInfo in _pairManager.Groups.Values.ToList())
|
||||
{
|
||||
var permissions = groupInfo.GroupUserPermissions;
|
||||
var overrides = TryGetGroupOverride(groupInfo.Group.GID);
|
||||
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||
continue;
|
||||
|
||||
updatedGroups++;
|
||||
tasks.Add(_apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupInfo.Group, selfUser, permissions)));
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed applying default sync settings to all pairs/groups");
|
||||
}
|
||||
}
|
||||
|
||||
var summary = BuildSummaryMessage(updatedPairs, updatedGroups);
|
||||
var primary = BuildPrimaryMessage(message);
|
||||
var combined = string.IsNullOrEmpty(primary) ? summary : string.Concat(primary, ' ', summary);
|
||||
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", combined, NotificationType.Info));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Unexpected error while applying default sync settings to all pairs/groups");
|
||||
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", "Une erreur est survenue lors de l'application des paramètres par défaut.", NotificationType.Error));
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDefaultsToAll(ApplyDefaultsToAllSyncsMessage message) => _ = ApplyDefaultsToAllAsync(message);
|
||||
|
||||
private static string? BuildPrimaryMessage(ApplyDefaultsToAllSyncsMessage message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.Context) || message.Disabled == null)
|
||||
return null;
|
||||
|
||||
var state = message.Disabled.Value ? "désactivée" : "activée";
|
||||
return $"Synchronisation {message.Context} par défaut {state}.";
|
||||
}
|
||||
|
||||
private static string BuildSummaryMessage(int pairs, int groups)
|
||||
{
|
||||
if (pairs == 0 && groups == 0)
|
||||
return "Aucun pair ou syncshell n'avait besoin d'être modifié.";
|
||||
|
||||
if (pairs > 0 && groups > 0)
|
||||
return $"Mise à jour de {pairs} pair(s) et {groups} syncshell(s).";
|
||||
|
||||
if (pairs > 0)
|
||||
return $"Mise à jour de {pairs} pair(s).";
|
||||
|
||||
return $"Mise à jour de {groups} syncshell(s).";
|
||||
}
|
||||
|
||||
private void OnPairOverrideChanged(PairSyncOverrideChanged message)
|
||||
{
|
||||
var overrides = _configService.Current.PairSyncOverrides ??= new(StringComparer.Ordinal);
|
||||
var entry = overrides.TryGetValue(message.Uid, out var existing) ? existing : new SyncOverrideEntry();
|
||||
bool changed = false;
|
||||
|
||||
if (message.DisableSounds.HasValue)
|
||||
{
|
||||
var val = message.DisableSounds.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableSounds != newValue)
|
||||
{
|
||||
entry.DisableSounds = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.DisableAnimations.HasValue)
|
||||
{
|
||||
var val = message.DisableAnimations.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableAnimations != newValue)
|
||||
{
|
||||
entry.DisableAnimations = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.DisableVfx.HasValue)
|
||||
{
|
||||
var val = message.DisableVfx.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableVfx != newValue)
|
||||
{
|
||||
entry.DisableVfx = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
if (entry.IsEmpty)
|
||||
overrides.Remove(message.Uid);
|
||||
else
|
||||
overrides[message.Uid] = entry;
|
||||
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
private void OnGroupOverrideChanged(GroupSyncOverrideChanged message)
|
||||
{
|
||||
var overrides = _configService.Current.GroupSyncOverrides ??= new(StringComparer.Ordinal);
|
||||
var entry = overrides.TryGetValue(message.Gid, out var existing) ? existing : new SyncOverrideEntry();
|
||||
bool changed = false;
|
||||
|
||||
if (message.DisableSounds.HasValue)
|
||||
{
|
||||
var val = message.DisableSounds.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableSounds != newValue)
|
||||
{
|
||||
entry.DisableSounds = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.DisableAnimations.HasValue)
|
||||
{
|
||||
var val = message.DisableAnimations.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableAnimations != newValue)
|
||||
{
|
||||
entry.DisableAnimations = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.DisableVfx.HasValue)
|
||||
{
|
||||
var val = message.DisableVfx.Value;
|
||||
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||
var newValue = val == defaultVal ? (bool?)null : val;
|
||||
if (entry.DisableVfx != newValue)
|
||||
{
|
||||
entry.DisableVfx = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
if (entry.IsEmpty)
|
||||
overrides.Remove(message.Gid);
|
||||
else
|
||||
overrides[message.Gid] = entry;
|
||||
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
private SyncOverrideEntry? TryGetPairOverride(string uid)
|
||||
{
|
||||
var overrides = _configService.Current.PairSyncOverrides;
|
||||
return overrides != null && overrides.TryGetValue(uid, out var entry) ? entry : null;
|
||||
}
|
||||
|
||||
private SyncOverrideEntry? TryGetGroupOverride(string gid)
|
||||
{
|
||||
var overrides = _configService.Current.GroupSyncOverrides;
|
||||
return overrides != null && overrides.TryGetValue(gid, out var entry) ? entry : null;
|
||||
}
|
||||
|
||||
private static bool ApplyDefaults(ref UserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||
{
|
||||
bool changed = false;
|
||||
if (overrides?.DisableSounds is bool overrideSounds)
|
||||
{
|
||||
if (permissions.IsDisableSounds() != overrideSounds)
|
||||
{
|
||||
permissions.SetDisableSounds(overrideSounds);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||
{
|
||||
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||
{
|
||||
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||
{
|
||||
permissions.SetDisableAnimations(overrideAnims);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||
{
|
||||
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (overrides?.DisableVfx is bool overrideVfx)
|
||||
{
|
||||
if (permissions.IsDisableVFX() != overrideVfx)
|
||||
{
|
||||
permissions.SetDisableVFX(overrideVfx);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||
{
|
||||
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool ApplyDefaults(ref GroupUserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||
{
|
||||
bool changed = false;
|
||||
if (overrides?.DisableSounds is bool overrideSounds)
|
||||
{
|
||||
if (permissions.IsDisableSounds() != overrideSounds)
|
||||
{
|
||||
permissions.SetDisableSounds(overrideSounds);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||
{
|
||||
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||
{
|
||||
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||
{
|
||||
permissions.SetDisableAnimations(overrideAnims);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||
{
|
||||
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (overrides?.DisableVfx is bool overrideVfx)
|
||||
{
|
||||
if (permissions.IsDisableVFX() != overrideVfx)
|
||||
{
|
||||
permissions.SetDisableVFX(overrideVfx);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||
{
|
||||
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
117
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
117
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
|
||||
{
|
||||
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate);
|
||||
|
||||
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ILogger<TypingIndicatorStateService> _logger;
|
||||
private DateTime _selfTypingLast = DateTime.MinValue;
|
||||
private DateTime _selfTypingStart = DateTime.MinValue;
|
||||
private bool _selfTypingActive;
|
||||
|
||||
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
Mediator = mediator;
|
||||
|
||||
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Mediator.UnsubscribeAll(this);
|
||||
}
|
||||
|
||||
public MareMediator Mediator { get; }
|
||||
|
||||
public void SetSelfTypingLocal(bool isTyping)
|
||||
{
|
||||
if (isTyping)
|
||||
{
|
||||
if (!_selfTypingActive)
|
||||
_selfTypingStart = DateTime.UtcNow;
|
||||
_selfTypingLast = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selfTypingStart = DateTime.MinValue;
|
||||
}
|
||||
|
||||
_selfTypingActive = isTyping;
|
||||
}
|
||||
|
||||
private void OnTypingState(UserTypingStateMessage msg)
|
||||
{
|
||||
var uid = msg.Typing.User.UID;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||
{
|
||||
_selfTypingActive = msg.Typing.IsTyping;
|
||||
if (_selfTypingActive)
|
||||
{
|
||||
if (_selfTypingStart == DateTime.MinValue)
|
||||
_selfTypingStart = now;
|
||||
_selfTypingLast = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selfTypingStart = DateTime.MinValue;
|
||||
}
|
||||
_logger.LogInformation("Typing state self -> {state}", _selfTypingActive);
|
||||
}
|
||||
else if (msg.Typing.IsTyping)
|
||||
{
|
||||
_typingUsers.AddOrUpdate(uid,
|
||||
_ => new TypingEntry(msg.Typing.User, now, now),
|
||||
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
|
||||
}
|
||||
else
|
||||
{
|
||||
_typingUsers.TryRemove(uid, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetSelfTyping(TimeSpan maxAge, out DateTime startTyping, out DateTime lastTyping)
|
||||
{
|
||||
startTyping = _selfTypingStart;
|
||||
lastTyping = _selfTypingLast;
|
||||
if (!_selfTypingActive)
|
||||
return false;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - _selfTypingLast) >= maxAge)
|
||||
{
|
||||
_selfTypingActive = false;
|
||||
_selfTypingStart = DateTime.MinValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> GetActiveTypers(TimeSpan maxAge)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var kvp in _typingUsers.ToArray())
|
||||
{
|
||||
if ((now - kvp.Value.LastUpdate) >= maxAge)
|
||||
{
|
||||
_typingUsers.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate), StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
@@ -21,17 +26,15 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamud;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly Services.AutoDetect.AutoDetectRequestService _requestService;
|
||||
private readonly AutoDetectRequestService _requestService;
|
||||
private readonly NearbyPendingService _pendingService;
|
||||
private readonly PairManager _pairManager;
|
||||
private List<Services.Mediator.NearbyEntry> _entries = new();
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
=> LocalizationService.Instance?.GetString(key, fallback, args)
|
||||
?? string.Format(CultureInfo.CurrentCulture, fallback, args);
|
||||
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
|
||||
|
||||
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
|
||||
MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable,
|
||||
Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager,
|
||||
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
|
||||
PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
_dalamud = dalamudUtilService;
|
||||
_objectTable = objectTable;
|
||||
_requestService = requestService;
|
||||
_pendingService = pendingService;
|
||||
_pairManager = pairManager;
|
||||
|
||||
Flags |= ImGuiWindowFlags.NoScrollbar;
|
||||
@@ -58,15 +62,147 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
using var idScope = ImRaii.PushId("autodetect-ui");
|
||||
|
||||
var incomingInvites = _pendingService.Pending.ToList();
|
||||
var outgoingInvites = _requestService.GetPendingRequestsSnapshot();
|
||||
|
||||
Vector4 accent = UiSharedService.AccentColor;
|
||||
if (accent.W <= 0f) accent = ImGuiColors.ParsedPurple;
|
||||
Vector4 inactiveTab = new(accent.X * 0.45f, accent.Y * 0.45f, accent.Z * 0.45f, Math.Clamp(accent.W + 0.15f, 0f, 1f));
|
||||
Vector4 hoverTab = UiSharedService.AccentHoverColor;
|
||||
|
||||
using var tabs = ImRaii.TabBar("AutoDetectTabs");
|
||||
if (!tabs.Success) return;
|
||||
|
||||
var incomingCount = incomingInvites.Count;
|
||||
DrawStyledTab($"Invitations ({incomingCount})", accent, inactiveTab, hoverTab, () =>
|
||||
{
|
||||
DrawInvitationsTab(incomingInvites, outgoingInvites);
|
||||
});
|
||||
|
||||
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
|
||||
|
||||
using (ImRaii.Disabled(true))
|
||||
{
|
||||
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, () =>
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Disponible prochainement.", ImGuiColors.DalamudGrey3);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawStyledTab(string label, Vector4 accent, Vector4 inactive, Vector4 hover, Action draw, bool disabled = false)
|
||||
{
|
||||
var tabColor = disabled ? ImGuiColors.DalamudGrey3 : inactive;
|
||||
var tabHover = disabled ? ImGuiColors.DalamudGrey3 : hover;
|
||||
var tabActive = disabled ? ImGuiColors.DalamudGrey2 : accent;
|
||||
using var baseColor = ImRaii.PushColor(ImGuiCol.Tab, tabColor);
|
||||
using var hoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, tabHover);
|
||||
using var activeColor = ImRaii.PushColor(ImGuiCol.TabActive, tabActive);
|
||||
using var activeText = ImRaii.PushColor(ImGuiCol.Text, disabled ? ImGuiColors.DalamudGrey2 : Vector4.One, false);
|
||||
using var tab = ImRaii.TabItem(label);
|
||||
if (tab.Success)
|
||||
{
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawInvitationsTab(List<KeyValuePair<string, string>> incomingInvites, IReadOnlyCollection<AutoDetectRequestService.PendingRequestInfo> outgoingInvites)
|
||||
{
|
||||
if (incomingInvites.Count == 0 && outgoingInvites.Count == 0)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Aucune invitation en attente. Cette page regroupera les demandes reçues et celles que vous avez envoyées.", ImGuiColors.DalamudGrey3);
|
||||
return;
|
||||
}
|
||||
|
||||
if (incomingInvites.Count == 0)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Vous n'avez aucune invitation de pair en attente pour le moment.", ImGuiColors.DalamudGrey3);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
float leftWidth = Math.Max(220f * ImGuiHelpers.GlobalScale, ImGui.CalcTextSize("Invitations reçues (00)").X + ImGui.GetStyle().FramePadding.X * 4f);
|
||||
var avail = ImGui.GetContentRegionAvail();
|
||||
|
||||
ImGui.BeginChild("incoming-requests", new Vector2(leftWidth, avail.Y), true);
|
||||
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations reçues ({incomingInvites.Count})");
|
||||
ImGui.Separator();
|
||||
if (incomingInvites.Count == 0)
|
||||
{
|
||||
ImGui.TextDisabled("Aucune invitation reçue.");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var (uid, name) in incomingInvites.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
using var id = ImRaii.PushId(uid);
|
||||
bool processing = _acceptInFlight.Contains(uid);
|
||||
ImGui.TextUnformatted(name);
|
||||
ImGui.TextDisabled(uid);
|
||||
if (processing)
|
||||
{
|
||||
ImGui.TextDisabled("Traitement en cours...");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGui.Button("Accepter"))
|
||||
{
|
||||
TriggerAccept(uid);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Refuser"))
|
||||
{
|
||||
_pendingService.Remove(uid);
|
||||
}
|
||||
}
|
||||
ImGui.Separator();
|
||||
}
|
||||
}
|
||||
ImGui.EndChild();
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.BeginChild("outgoing-requests", new Vector2(0, avail.Y), true);
|
||||
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations envoyées ({outgoingInvites.Count})");
|
||||
ImGui.Separator();
|
||||
if (outgoingInvites.Count == 0)
|
||||
{
|
||||
ImGui.TextDisabled("Aucune invitation envoyée en attente.");
|
||||
ImGui.EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var info in outgoingInvites.OrderByDescending(i => i.SentAt))
|
||||
{
|
||||
using var id = ImRaii.PushId(info.Key);
|
||||
ImGui.TextUnformatted(info.TargetDisplayName);
|
||||
if (!string.IsNullOrEmpty(info.Uid))
|
||||
{
|
||||
ImGui.TextDisabled(info.Uid);
|
||||
}
|
||||
|
||||
ImGui.TextDisabled($"Envoyée il y a {FormatDuration(DateTime.UtcNow - info.SentAt)}");
|
||||
if (ImGui.Button("Retirer"))
|
||||
{
|
||||
_requestService.RemovePendingRequestByKey(info.Key);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Retire uniquement cette entrée locale de suivi.");
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private void DrawNearbyTab()
|
||||
{
|
||||
if (!_configService.Current.EnableAutoDetectDiscovery)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("AutoDetect.Disabled", "Nearby detection is disabled. Enable it in Settings to start detecting nearby Umbra users."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("AutoDetect est désactivé. Activez-le dans les paramètres pour détecter les utilisateurs Umbra à proximité.", ImGuiColors.DalamudYellow);
|
||||
ImGuiHelpers.ScaledDummy(6);
|
||||
}
|
||||
|
||||
int maxDist = Math.Clamp(_configService.Current.AutoDetectMaxDistanceMeters, 5, 100);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(L("AutoDetect.MaxDistance", "Max distance (m)"));
|
||||
ImGui.TextUnformatted("Max distance (m)");
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(120 * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.SliderInt("##autodetect-dist", ref maxDist, 5, 100))
|
||||
@@ -80,11 +216,11 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
// Table header
|
||||
if (ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp))
|
||||
{
|
||||
ImGui.TableSetupColumn(L("AutoDetect.Table.Name", "Name"));
|
||||
ImGui.TableSetupColumn(L("AutoDetect.Table.World", "World"));
|
||||
ImGui.TableSetupColumn(L("AutoDetect.Table.Distance", "Distance"));
|
||||
ImGui.TableSetupColumn(L("AutoDetect.Table.Status", "Status"));
|
||||
ImGui.TableSetupColumn(L("AutoDetect.Table.Action", "Action"));
|
||||
ImGui.TableSetupColumn("Name");
|
||||
ImGui.TableSetupColumn("World");
|
||||
ImGui.TableSetupColumn("Distance");
|
||||
ImGui.TableSetupColumn("Status");
|
||||
ImGui.TableSetupColumn("Action");
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var data = _entries.Count > 0 ? _entries.Where(e => e.IsMatch).ToList() : new List<Services.Mediator.NearbyEntry>();
|
||||
@@ -93,33 +229,27 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(e.Name);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(e.WorldId == 0 ? L("AutoDetect.World.Unknown", "-") : (_dalamud.WorldData.Value.TryGetValue(e.WorldId, out var w) ? w : e.WorldId.ToString()));
|
||||
ImGui.TextUnformatted(e.WorldId == 0 ? "-" : (_dalamud.WorldData.Value.TryGetValue(e.WorldId, out var w) ? w : e.WorldId.ToString()));
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(float.IsNaN(e.Distance)
|
||||
? L("AutoDetect.Distance.Unknown", "-")
|
||||
: string.Format(CultureInfo.CurrentCulture, L("AutoDetect.Distance.Format", "{0:0.0} m"), e.Distance));
|
||||
ImGui.TextUnformatted(float.IsNaN(e.Distance) ? "-" : $"{e.Distance:0.0} m");
|
||||
ImGui.TableNextColumn();
|
||||
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(e);
|
||||
string status = alreadyPaired
|
||||
? L("AutoDetect.Status.Paired", "Paired")
|
||||
: (string.IsNullOrEmpty(e.Token)
|
||||
? L("AutoDetect.Status.RequestsDisabled", "Requests disabled")
|
||||
: L("AutoDetect.Status.OnUmbra", "On Umbra"));
|
||||
string status = alreadyPaired ? "Paired" : (string.IsNullOrEmpty(e.Token) ? "Requests disabled" : "On Umbra");
|
||||
ImGui.TextUnformatted(status);
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(alreadyPaired || string.IsNullOrEmpty(e.Token)))
|
||||
{
|
||||
if (alreadyPaired)
|
||||
{
|
||||
ImGui.Button(L("AutoDetect.Action.AlreadySynced", "Already sync") + "##" + e.Name);
|
||||
ImGui.Button($"Already sync##{e.Name}");
|
||||
}
|
||||
else if (string.IsNullOrEmpty(e.Token))
|
||||
{
|
||||
ImGui.Button(L("AutoDetect.Action.RequestsDisabled", "Requests disabled") + "##" + e.Name);
|
||||
ImGui.Button($"Requests disabled##{e.Name}");
|
||||
}
|
||||
else if (ImGui.Button(L("AutoDetect.Action.SendRequest", "Send request") + "##" + e.Name))
|
||||
else if (ImGui.Button($"Send request##{e.Name}"))
|
||||
{
|
||||
_ = _requestService.SendRequestAsync(e.Token!);
|
||||
_ = _requestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,26 +275,6 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
_entries = msg.Entries;
|
||||
}
|
||||
|
||||
private List<Services.Mediator.NearbyEntry> BuildLocalSnapshot(int maxDist)
|
||||
{
|
||||
var list = new List<Services.Mediator.NearbyEntry>();
|
||||
var local = _dalamud.GetPlayerCharacter();
|
||||
var localPos = local?.Position ?? Vector3.Zero;
|
||||
for (int i = 0; i < 200; i += 2)
|
||||
{
|
||||
var obj = _objectTable[i];
|
||||
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||
if (local != null && obj.Address == local.Address) continue;
|
||||
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
|
||||
if (!float.IsNaN(dist) && dist > maxDist) continue;
|
||||
string name = obj.Name.ToString();
|
||||
ushort worldId = 0;
|
||||
if (obj is IPlayerCharacter pc) worldId = (ushort)pc.HomeWorld.RowId;
|
||||
list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null, null, null));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
||||
{
|
||||
try
|
||||
@@ -205,4 +315,37 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void TriggerAccept(string uid)
|
||||
{
|
||||
if (!_acceptInFlight.Add(uid)) return;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
bool ok = await _pendingService.AcceptAsync(uid).ConfigureAwait(false);
|
||||
if (!ok)
|
||||
{
|
||||
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_acceptInFlight.Remove(uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan span)
|
||||
{
|
||||
if (span.TotalMinutes >= 1)
|
||||
{
|
||||
var minutes = Math.Max(1, (int)Math.Round(span.TotalMinutes));
|
||||
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||
}
|
||||
|
||||
var seconds = Math.Max(1, (int)Math.Round(span.TotalSeconds));
|
||||
return seconds == 1 ? "1 seconde" : $"{seconds} secondes";
|
||||
}
|
||||
}
|
||||
|
||||
249
MareSynchronos/UI/ChangelogUi.cs
Normal file
249
MareSynchronos/UI/ChangelogUi.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
public sealed class ChangelogUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private const int AlwaysExpandedEntryCount = 2;
|
||||
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly UiSharedService _uiShared;
|
||||
private readonly Version _currentVersion;
|
||||
private readonly string _currentVersionLabel;
|
||||
private readonly IReadOnlyList<ChangelogEntry> _entries;
|
||||
|
||||
private bool _showAllEntries;
|
||||
private bool _hasAcknowledgedVersion;
|
||||
|
||||
public ChangelogUi(ILogger<ChangelogUi> logger, UiSharedService uiShared, MareConfigService configService,
|
||||
MareMediator mediator, PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, "Umbra Sync - Notes de version", performanceCollectorService)
|
||||
{
|
||||
_uiShared = uiShared;
|
||||
_configService = configService;
|
||||
_currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0);
|
||||
_currentVersionLabel = _currentVersion.ToString();
|
||||
_entries = BuildEntries();
|
||||
_hasAcknowledgedVersion = string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal);
|
||||
|
||||
RespectCloseHotkey = true;
|
||||
SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new(520, 360),
|
||||
MaximumSize = new(900, 1200)
|
||||
};
|
||||
Flags |= ImGuiWindowFlags.NoResize;
|
||||
ShowCloseButton = true;
|
||||
|
||||
if (!string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal))
|
||||
{
|
||||
IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
MarkCurrentVersionAsReadIfNeeded();
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
_ = _uiShared.DrawOtherPluginState();
|
||||
|
||||
DrawHeader();
|
||||
DrawEntries();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
private void DrawHeader()
|
||||
{
|
||||
using (_uiShared.UidFont.Push())
|
||||
{
|
||||
ImGui.TextUnformatted("Notes de version");
|
||||
}
|
||||
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, $"Version chargée : {_currentVersionLabel}");
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
private void DrawEntries()
|
||||
{
|
||||
bool expandedOldVersions = false;
|
||||
for (int index = 0; index < _entries.Count; index++)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
if (!_showAllEntries && index >= AlwaysExpandedEntryCount)
|
||||
{
|
||||
if (!expandedOldVersions)
|
||||
{
|
||||
expandedOldVersions = ImGui.CollapsingHeader("Historique complet");
|
||||
}
|
||||
|
||||
if (!expandedOldVersions)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
DrawEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEntry(ChangelogEntry entry)
|
||||
{
|
||||
using (ImRaii.PushId(entry.VersionLabel))
|
||||
{
|
||||
ImGui.Spacing();
|
||||
UiSharedService.ColorText(entry.VersionLabel, entry.Version == _currentVersion
|
||||
? ImGuiColors.HealerGreen
|
||||
: ImGuiColors.DalamudWhite);
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
foreach (var line in entry.Lines)
|
||||
{
|
||||
DrawLine(line);
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawLine(ChangelogLine line)
|
||||
{
|
||||
using var indent = line.IndentLevel > 0 ? ImRaii.PushIndent(line.IndentLevel) : null;
|
||||
if (line.Color != null)
|
||||
{
|
||||
ImGui.TextColored(line.Color.Value, $"- {line.Text}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted($"- {line.Text}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
ImGui.Spacing();
|
||||
if (!_showAllEntries && _entries.Count > AlwaysExpandedEntryCount)
|
||||
{
|
||||
if (ImGui.Button("Tout afficher"))
|
||||
{
|
||||
_showAllEntries = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
if (ImGui.Button("Marquer comme lu"))
|
||||
{
|
||||
MarkCurrentVersionAsReadIfNeeded();
|
||||
IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkCurrentVersionAsReadIfNeeded()
|
||||
{
|
||||
if (_hasAcknowledgedVersion)
|
||||
return;
|
||||
|
||||
_configService.Current.LastChangelogVersionSeen = _currentVersionLabel;
|
||||
_configService.Save();
|
||||
_hasAcknowledgedVersion = true;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ChangelogEntry> BuildEntries()
|
||||
{
|
||||
return new List<ChangelogEntry>
|
||||
{
|
||||
new(new Version(0, 1, 9, 6), "0.1.9.6", new List<ChangelogLine>
|
||||
{
|
||||
new("Possibilité de désactiver l'alerte self-analysis (Settings => Performance)."),
|
||||
}),
|
||||
new(new Version(0, 1, 9, 5), "0.1.9.5", new List<ChangelogLine>
|
||||
{
|
||||
new("Fix l'affichage de la bulle dans la liste du groupe."),
|
||||
new("Amélioration de l'ajout des utilisateurs via le bouton +."),
|
||||
new("Possibilité de mettre en pause individuellement des utilisateurs d'une syncshell."),
|
||||
new("Amélioration de la stabilité du plugin en cas de petite connexion / petite configuration."),
|
||||
new("Divers fix de l'interface."),
|
||||
}),
|
||||
new(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
|
||||
{
|
||||
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."),
|
||||
new("Désactivation de l'AutoDetect en zone instanciée."),
|
||||
new("Réécriture interface AutoDetect pour acceuillir les invitations en attente et préparer les synchsells publiques."),
|
||||
new("Amélioration de la compréhension des activations / désactivations des préférences de synchronisation par défaut."),
|
||||
new("Mise en avant du Self Analyse avec une alerte lorsqu'un seuil de donnée a été atteint."),
|
||||
new("Ajout de l'alerte de la non-compatibilité du plugin Chat2."),
|
||||
new("Divers fix de l'interface."),
|
||||
}),
|
||||
new(new Version(0, 1, 9, 3), "0.1.9.3", new List<ChangelogLine>
|
||||
{
|
||||
new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."),
|
||||
}),
|
||||
new(new Version(0, 1, 9, 2), "0.1.9.2", new List<ChangelogLine>
|
||||
{
|
||||
new("Correctif de l'affichage de la bulle de frappe."),
|
||||
}),
|
||||
new(new Version(0, 1, 9, 1), "0.1.9.1", new List<ChangelogLine>
|
||||
{
|
||||
new("Début correctif pour la bulle de frappe."),
|
||||
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||
|
||||
}),
|
||||
new(new Version(0, 1, 9, 0), "0.1.9.0", new List<ChangelogLine>
|
||||
{
|
||||
new("Il est désormais possible de configurer par défaut nos choix de synchronisation (VFX, Music, Animation)."),
|
||||
new("La catégorie 'En attente' ne s'affiche uniquement que si une invitation est en attente"),
|
||||
new("(EN PRÉ VERSION) Il est désormais possible de voir quand une personne appairée est en train d'écrire avec une bulle qui s'affiche."),
|
||||
new("(EN PRÉ VERSION) La bulle de frappe s'affiche également sur votre propre plaque de nom lorsque vous écrivez."),
|
||||
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||
new("Correctif : Désormais, les invitation entrantes ne s'affichent qu'une seule fois au lieu de deux."),
|
||||
}),
|
||||
new(new Version(0, 1, 8, 2), "0.1.8.2", new List<ChangelogLine>
|
||||
{
|
||||
new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."),
|
||||
new("Sont filtrés automatiquement les personnes refusées ou déjà appairées."),
|
||||
new("Invitations Nearby : anti-spam de 5 minutes par personne, blocage 15 minutes après trois refus."),
|
||||
new("Affichage : Correction de l'affichage des notes par défaut plutôt que de l'ID si disponible."),
|
||||
new("Les notifications de blocage sont envoyées directement dans le tchat."),
|
||||
new("Overlay DTR : affiche le nombre d'invitations Nearby disponibles dans le titre et l'infobulle."),
|
||||
new("Poses Nearby : le filtre re-fonctionne avec vos notes locales pour retrouver les entrées correspondantes."),
|
||||
}),
|
||||
new(new Version(0, 1, 8, 1), "0.1.8.1", new List<ChangelogLine>
|
||||
{
|
||||
new("Correctif 'Vu sous' : l'infobulle affiche désormais le dernier personnage observé."),
|
||||
new("Invitations AutoDetect : triées en tête de liste pour mieux les repérer."),
|
||||
new("Invitations AutoDetect : conservées entre les redémarrages du plugin ou du jeu."),
|
||||
new("Barre de statut serveur : couleur violette adoptée par défaut."),
|
||||
}),
|
||||
new(new Version(0, 1, 8, 0), "0.1.8.0", new List<ChangelogLine>
|
||||
{
|
||||
new("AutoDetect : détection automatique des joueurs Umbra autour de vous et propositions d'appairage."),
|
||||
new("AutoDetect : désactivé par défaut pour préserver la confidentialité.", 1, ImGuiColors.DalamudGrey),
|
||||
new("AutoDetect : activez-le dans 'Transfers' avec les options Nearby detection et Allow pair requests.", 1, ImGuiColors.DalamudGrey),
|
||||
new("Syncshell temporaire : durée configurable de 1 h à 7 jours, expiration automatique."),
|
||||
new("Syncshell permanente : possibilité de nommer et d'organiser vos groupes sur la durée."),
|
||||
new("Interface : palette UmbraSync harmonisée et menus allégés pour l'usage RP."),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private readonly record struct ChangelogEntry(Version Version, string VersionLabel, IReadOnlyList<ChangelogLine> Lines);
|
||||
|
||||
private readonly record struct ChangelogLine(string Text, int IndentLevel = 0, System.Numerics.Vector4? Color = null);
|
||||
}
|
||||
@@ -15,14 +15,14 @@ internal sealed partial class CharaDataHubUi
|
||||
if (!_charaDataManager.BrioAvailable)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
|
||||
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", UiSharedService.AccentColor);
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
}
|
||||
|
||||
if (!_uiSharedService.ApiController.IsConnected)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
|
||||
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", UiSharedService.AccentColor);
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ internal sealed partial class CharaDataHubUi
|
||||
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
||||
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
||||
{
|
||||
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
||||
@@ -175,12 +175,12 @@ internal sealed partial class CharaDataHubUi
|
||||
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
||||
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
||||
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
||||
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
||||
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
||||
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
||||
@@ -217,7 +217,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
|
||||
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (canUpdate)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", UiSharedService.AccentColor);
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
||||
{
|
||||
@@ -216,7 +216,7 @@ internal sealed partial class CharaDataHubUi
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", UiSharedService.AccentColor);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(pos);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
|
||||
@@ -401,7 +401,7 @@ internal sealed partial class CharaDataHubUi
|
||||
else if (!_charaDataManager.BrioAvailable)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", UiSharedService.AccentColor);
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
}
|
||||
|
||||
@@ -685,7 +685,7 @@ internal sealed partial class CharaDataHubUi
|
||||
}
|
||||
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
||||
{
|
||||
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
|
||||
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : UiSharedService.AccentColor;
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@ internal partial class CharaDataHubUi
|
||||
_configService.Save();
|
||||
}
|
||||
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
|
||||
int maxWisps = _configService.Current.NearbyMaxWisps;
|
||||
ImGui.SetNextItemWidth(140);
|
||||
if (ImGui.SliderInt("Maximum wisps", ref maxWisps, 0, 200))
|
||||
{
|
||||
_configService.Current.NearbyMaxWisps = maxWisps;
|
||||
_configService.Save();
|
||||
}
|
||||
_uiSharedService.DrawHelpText("Limit how many wisps are active at once. Set to 0 to disable wisps even when enabled above.");
|
||||
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
|
||||
ImGui.SetNextItemWidth(100);
|
||||
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
|
||||
|
||||
@@ -170,7 +170,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
if (!_charaDataManager.BrioAvailable)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(3);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", UiSharedService.AccentColor);
|
||||
UiSharedService.DistanceSeparator();
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
if (_charaDataManager.DataApplicationTask != null)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", UiSharedService.AccentColor);
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
ImGui.Separator();
|
||||
}
|
||||
@@ -648,7 +648,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, UiSharedService.AccentColor);
|
||||
}
|
||||
|
||||
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
|
||||
@@ -848,7 +848,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
||||
ImGuiColors.DalamudRed);
|
||||
UiSharedService.AccentColor);
|
||||
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
||||
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Dalamud.Utility;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
@@ -47,6 +48,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly NearbyPendingService _nearbyPending;
|
||||
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private bool _buttonState;
|
||||
@@ -62,13 +64,14 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private bool _wasOpen;
|
||||
private bool _nearbyOpen = true;
|
||||
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiSharedService.Localize(key, fallback, args);
|
||||
private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024;
|
||||
private const long SelfAnalysisTriangleWarningThreshold = 150_000;
|
||||
|
||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
|
||||
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
|
||||
NearbyPendingService nearbyPendingService,
|
||||
AutoDetectRequestService autoDetectRequestService,
|
||||
CharacterAnalyzer characterAnalyzer,
|
||||
PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
|
||||
{
|
||||
@@ -82,11 +85,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_charaDataManager = charaDataManager;
|
||||
_nearbyPending = nearbyPendingService;
|
||||
_autoDetectRequestService = autoDetectRequestService;
|
||||
_characterAnalyzer = characterAnalyzer;
|
||||
var tagHandler = new TagHandler(_serverManager);
|
||||
|
||||
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager);
|
||||
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager, _autoDetectRequestService);
|
||||
_selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService);
|
||||
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler, _uiSharedService);
|
||||
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler);
|
||||
_pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService);
|
||||
|
||||
#if DEBUG
|
||||
@@ -104,7 +108,20 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||
Mediator.Subscribe<DiscoveryListUpdated>(this, (msg) => _nearbyEntries = msg.Entries);
|
||||
Mediator.Subscribe<DiscoveryListUpdated>(this, (msg) =>
|
||||
{
|
||||
_nearbyEntries = msg.Entries;
|
||||
// Update last-seen character names for matched entries
|
||||
foreach (var e in _nearbyEntries.Where(x => x.IsMatch))
|
||||
{
|
||||
var uid = e.Uid;
|
||||
var lastSeen = e.DisplayName ?? e.Name;
|
||||
if (!string.IsNullOrEmpty(uid) && !string.IsNullOrEmpty(lastSeen))
|
||||
{
|
||||
_serverManager.SetNameForUid(uid, lastSeen);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Flags |= ImGuiWindowFlags.NoDocking;
|
||||
|
||||
@@ -117,24 +134,30 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
UiSharedService.AccentColor = new Vector4(0.63f, 0.25f, 1f, 1f);
|
||||
UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
|
||||
UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||
UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
|
||||
var accent = UiSharedService.AccentColor;
|
||||
using var titleBg = ImRaii.PushColor(ImGuiCol.TitleBg, accent);
|
||||
using var titleBgActive = ImRaii.PushColor(ImGuiCol.TitleBgActive, accent);
|
||||
using var titleBgCollapsed = ImRaii.PushColor(ImGuiCol.TitleBgCollapsed, accent);
|
||||
using var buttonHover = ImRaii.PushColor(ImGuiCol.ButtonHovered, UiSharedService.AccentHoverColor);
|
||||
using var buttonActive = ImRaii.PushColor(ImGuiCol.ButtonActive, UiSharedService.AccentActiveColor);
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y);
|
||||
WindowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
||||
if (!_apiController.IsCurrentVersion)
|
||||
{
|
||||
var ver = _apiController.CurrentClientVersion;
|
||||
var unsupported = L("Compact.Version.UnsupportedTitle", "UNSUPPORTED VERSION");
|
||||
var unsupported = "UNSUPPORTED VERSION";
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
{
|
||||
var uidTextSize = ImGui.CalcTextSize(unsupported);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
|
||||
ImGui.TextColored(UiSharedService.AccentColor, unsupported);
|
||||
}
|
||||
UiSharedService.ColorTextWrapped(L("Compact.Version.Outdated",
|
||||
"Your UmbraSync installation is out of date, the current version is {0}.{1}.{2}. It is highly recommended to keep UmbraSync up to date. Open /xlplugins and update the plugin.",
|
||||
ver.Major, ver.Minor, ver.Build),
|
||||
ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped($"Your UmbraSync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
|
||||
$"It is highly recommended to keep UmbraSync up to date. Open /xlplugins and update the plugin.", UiSharedService.AccentColor);
|
||||
}
|
||||
|
||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||
@@ -145,42 +168,61 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
var hasShownSyncShells = _showSyncShells;
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
using (var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, UiSharedService.AccentHoverColor))
|
||||
using (var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, UiSharedService.AccentActiveColor))
|
||||
{
|
||||
if (!hasShownSyncShells)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]);
|
||||
}
|
||||
using var selectedColor = ImRaii.PushColor(ImGuiCol.Button, accent);
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
|
||||
{
|
||||
_showSyncShells = false;
|
||||
}
|
||||
if (!hasShownSyncShells)
|
||||
{
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("Compact.Toggle.IndividualPairs", "Individual pairs"));
|
||||
}
|
||||
else
|
||||
{
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
|
||||
{
|
||||
_showSyncShells = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Individual pairs");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
if (hasShownSyncShells)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]);
|
||||
}
|
||||
using var selectedColor = ImRaii.PushColor(ImGuiCol.Button, accent);
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
|
||||
{
|
||||
_showSyncShells = true;
|
||||
}
|
||||
if (hasShownSyncShells)
|
||||
{
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
ImGui.PopFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
|
||||
{
|
||||
_showSyncShells = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(L("Compact.Toggle.Syncshells", "Syncshells"));
|
||||
UiSharedService.AttachToolTip("Syncshells");
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
DrawDefaultSyncSettings();
|
||||
if (!hasShownSyncShells)
|
||||
{
|
||||
using (ImRaii.PushId("pairlist")) DrawPairList();
|
||||
@@ -200,14 +242,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
_lastAddedUser = _pairManager.LastAddedUser;
|
||||
_pairManager.LastAddedUser = null;
|
||||
var setNotesTitle = L("Compact.AddUser.ModalTitle", "Set Notes for New User");
|
||||
ImGui.OpenPopup(setNotesTitle);
|
||||
ImGui.OpenPopup("Set Notes for New User");
|
||||
_showModalForUserAddition = true;
|
||||
_lastAddedUserComment = string.Empty;
|
||||
}
|
||||
|
||||
var setNotesModalTitle = L("Compact.AddUser.ModalTitle", "Set Notes for New User");
|
||||
if (ImGui.BeginPopupModal(setNotesModalTitle, ref _showModalForUserAddition, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
if (_lastAddedUser == null)
|
||||
{
|
||||
@@ -215,9 +255,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.TextWrapped(L("Compact.AddUser.Description", "You have successfully added {0}. Set a local note for the user in the field below:", _lastAddedUser.UserData.AliasOrUID));
|
||||
ImGui.InputTextWithHint("##noteforuser", L("Compact.AddUser.NoteHint", "Note for {0}", _lastAddedUser.UserData.AliasOrUID), ref _lastAddedUserComment, 100);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, L("Compact.AddUser.Save", "Save Note")))
|
||||
UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:");
|
||||
ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note"))
|
||||
{
|
||||
_serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment);
|
||||
_lastAddedUser = null;
|
||||
@@ -245,6 +285,293 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
private void DrawDefaultSyncSettings()
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(4f);
|
||||
using (ImRaii.PushId("sync-defaults"))
|
||||
{
|
||||
const string soundLabel = "Audio";
|
||||
const string animLabel = "Anim";
|
||||
const string vfxLabel = "VFX";
|
||||
const string soundSubject = "de l'audio";
|
||||
const string animSubject = "des animations";
|
||||
const string vfxSubject = "des effets visuels";
|
||||
|
||||
bool soundsDisabled = _configService.Current.DefaultDisableSounds;
|
||||
bool animsDisabled = _configService.Current.DefaultDisableAnimations;
|
||||
bool vfxDisabled = _configService.Current.DefaultDisableVfx;
|
||||
bool showNearby = _configService.Current.EnableAutoDetectDiscovery;
|
||||
int pendingInvites = _nearbyPending.Pending.Count;
|
||||
|
||||
const string nearbyLabel = "AutoDetect";
|
||||
|
||||
|
||||
var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||
var animIcon = animsDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||
var vfxIcon = vfxDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||
|
||||
float spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel);
|
||||
float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel);
|
||||
float vfxWidth = _uiSharedService.GetIconTextButtonSize(vfxIcon, vfxLabel);
|
||||
float nearbyWidth = showNearby ? _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel) : 0f;
|
||||
int buttonCount = 3 + (showNearby ? 1 : 0);
|
||||
float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1);
|
||||
float available = ImGui.GetContentRegionAvail().X;
|
||||
float startCursorX = ImGui.GetCursorPosX();
|
||||
if (totalWidth < available)
|
||||
{
|
||||
ImGui.SetCursorPosX(startCursorX + (available - totalWidth) / 2f);
|
||||
}
|
||||
|
||||
DrawDefaultSyncButton(soundIcon, soundLabel, audioWidth, soundsDisabled,
|
||||
state =>
|
||||
{
|
||||
_configService.Current.DefaultDisableSounds = state;
|
||||
_configService.Save();
|
||||
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(soundSubject, state));
|
||||
},
|
||||
() => DisableStateTooltip(soundSubject, _configService.Current.DefaultDisableSounds));
|
||||
|
||||
DrawDefaultSyncButton(animIcon, animLabel, animWidth, animsDisabled,
|
||||
state =>
|
||||
{
|
||||
_configService.Current.DefaultDisableAnimations = state;
|
||||
_configService.Save();
|
||||
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(animSubject, state));
|
||||
},
|
||||
() => DisableStateTooltip(animSubject, _configService.Current.DefaultDisableAnimations), spacing);
|
||||
|
||||
DrawDefaultSyncButton(vfxIcon, vfxLabel, vfxWidth, vfxDisabled,
|
||||
state =>
|
||||
{
|
||||
_configService.Current.DefaultDisableVfx = state;
|
||||
_configService.Save();
|
||||
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(vfxSubject, state));
|
||||
},
|
||||
() => DisableStateTooltip(vfxSubject, _configService.Current.DefaultDisableVfx), spacing);
|
||||
|
||||
if (showNearby)
|
||||
{
|
||||
ImGui.SameLine(0, spacing);
|
||||
var autodetectLabel = pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel;
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, autodetectLabel, nearbyWidth))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||
}
|
||||
string tooltip = pendingInvites > 0
|
||||
? string.Format("Vous avez {0} invitation{1} reçue. Ouvrez l\'interface AutoDetect pour y répondre.", pendingInvites, pendingInvites > 1 ? "s" : string.Empty)
|
||||
: "Ouvrir les outils AutoDetect (invitations et proximité).\n\nLes demandes reçues sont listées dans l\'onglet 'Invitations'.";
|
||||
UiSharedService.AttachToolTip(tooltip);
|
||||
}
|
||||
|
||||
DrawSelfAnalysisPreview();
|
||||
}
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
private void DrawSelfAnalysisPreview()
|
||||
{
|
||||
using (ImRaii.PushId("self-analysis"))
|
||||
{
|
||||
if (!ImGui.CollapsingHeader("Self Analysis"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var summary = _characterAnalyzer.CurrentSummary;
|
||||
bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
|
||||
|
||||
if (isAnalyzing)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(
|
||||
$"Analyse en cours ({_characterAnalyzer.CurrentFile}/{System.Math.Max(_characterAnalyzer.TotalFiles, 1)})...",
|
||||
ImGuiColors.DalamudYellow);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Annuler l'analyse"))
|
||||
{
|
||||
_characterAnalyzer.CancelAnalyze();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Stopper l'analyse en cours.");
|
||||
}
|
||||
else
|
||||
{
|
||||
bool recalculate = !summary.HasUncomputedEntries && !summary.IsEmpty;
|
||||
var label = recalculate ? "Recalculer l'analyse" : "Lancer l'analyse";
|
||||
var icon = recalculate ? FontAwesomeIcon.Sync : FontAwesomeIcon.PlayCircle;
|
||||
if (_uiSharedService.IconTextButton(icon, label))
|
||||
{
|
||||
_ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: recalculate);
|
||||
}
|
||||
UiSharedService.AttachToolTip(recalculate
|
||||
? "Recalcule toutes les entrées pour mettre à jour les tailles partagées."
|
||||
: "Analyse vos fichiers actuels pour estimer le poids partagé.");
|
||||
}
|
||||
|
||||
if (summary.IsEmpty && !isAnalyzing)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Aucune donnée analysée pour l'instant. Lancez une analyse pour générer cet aperçu.",
|
||||
ImGuiColors.DalamudGrey2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.HasUncomputedEntries && !isAnalyzing)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Certaines entrées n'ont pas encore de taille calculée. Lancez l'analyse pour compléter les données.",
|
||||
ImGuiColors.DalamudYellow);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4f);
|
||||
|
||||
UiSharedService.DrawGrouped(() =>
|
||||
{
|
||||
if (ImGui.BeginTable("self-analysis-stats", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoSavedSettings))
|
||||
{
|
||||
ImGui.TableSetupColumn("label", ImGuiTableColumnFlags.WidthStretch, 0.55f);
|
||||
ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthStretch, 0.45f);
|
||||
|
||||
DrawSelfAnalysisStatRow("Fichiers moddés", summary.TotalFiles.ToString("N0", CultureInfo.CurrentCulture));
|
||||
|
||||
var compressedValue = UiSharedService.ByteToString(summary.TotalCompressedSize);
|
||||
Vector4? compressedColor = null;
|
||||
FontAwesomeIcon? compressedIcon = null;
|
||||
Vector4? compressedIconColor = null;
|
||||
string? compressedTooltip = null;
|
||||
if (summary.HasUncomputedEntries)
|
||||
{
|
||||
compressedColor = ImGuiColors.DalamudYellow;
|
||||
compressedTooltip = "Lancez l'analyse pour calculer la taille de téléchargement exacte.";
|
||||
}
|
||||
else if (summary.TotalCompressedSize >= SelfAnalysisSizeWarningThreshold)
|
||||
{
|
||||
compressedColor = ImGuiColors.DalamudYellow;
|
||||
compressedTooltip = "Au-delà de 300 MiB, certains joueurs peuvent ne pas voir toutes vos modifications.";
|
||||
compressedIcon = FontAwesomeIcon.ExclamationTriangle;
|
||||
compressedIconColor = ImGuiColors.DalamudYellow;
|
||||
}
|
||||
|
||||
DrawSelfAnalysisStatRow("Taille compressée", compressedValue, compressedColor, compressedTooltip, compressedIcon, compressedIconColor);
|
||||
DrawSelfAnalysisStatRow("Taille extraite", UiSharedService.ByteToString(summary.TotalOriginalSize));
|
||||
|
||||
Vector4? trianglesColor = null;
|
||||
FontAwesomeIcon? trianglesIcon = null;
|
||||
Vector4? trianglesIconColor = null;
|
||||
string? trianglesTooltip = null;
|
||||
if (summary.TotalTriangles >= SelfAnalysisTriangleWarningThreshold)
|
||||
{
|
||||
trianglesColor = ImGuiColors.DalamudYellow;
|
||||
trianglesTooltip = "Plus de 150k triangles peuvent entraîner un auto-pause et impacter les performances.";
|
||||
trianglesIcon = FontAwesomeIcon.ExclamationTriangle;
|
||||
trianglesIconColor = ImGuiColors.DalamudYellow;
|
||||
}
|
||||
DrawSelfAnalysisStatRow("Triangles moddés", UiSharedService.TrisToString(summary.TotalTriangles), trianglesColor, trianglesTooltip, trianglesIcon, trianglesIconColor);
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
}, rounding: 4f, expectedWidth: ImGui.GetContentRegionAvail().X);
|
||||
|
||||
string lastAnalysisText;
|
||||
Vector4 lastAnalysisColor = ImGuiColors.DalamudGrey2;
|
||||
if (isAnalyzing)
|
||||
{
|
||||
lastAnalysisText = "Dernière analyse : en cours...";
|
||||
lastAnalysisColor = ImGuiColors.DalamudYellow;
|
||||
}
|
||||
else if (_characterAnalyzer.LastCompletedAnalysis.HasValue)
|
||||
{
|
||||
var localTime = _characterAnalyzer.LastCompletedAnalysis.Value.ToLocalTime();
|
||||
lastAnalysisText = $"Dernière analyse : {localTime.ToString("g", CultureInfo.CurrentCulture)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
lastAnalysisText = "Dernière analyse : jamais";
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
UiSharedService.ColorTextWrapped(lastAnalysisText, lastAnalysisColor);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4f);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Ouvrir l'analyse détaillée"))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSelfAnalysisStatRow(string label, string value, Vector4? valueColor = null, string? tooltip = null, FontAwesomeIcon? icon = null, Vector4? iconColor = null)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(label);
|
||||
ImGui.TableNextColumn();
|
||||
if (icon.HasValue)
|
||||
{
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
if (iconColor.HasValue)
|
||||
{
|
||||
using var iconColorPush = ImRaii.PushColor(ImGuiCol.Text, iconColor.Value);
|
||||
ImGui.TextUnformatted(icon.Value.ToIconString());
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(icon.Value.ToIconString());
|
||||
}
|
||||
}
|
||||
ImGui.SameLine(0f, 4f);
|
||||
}
|
||||
|
||||
if (valueColor.HasValue)
|
||||
{
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, valueColor.Value);
|
||||
ImGui.TextUnformatted(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
{
|
||||
UiSharedService.AttachToolTip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDefaultSyncButton(FontAwesomeIcon icon, string label, float width, bool currentState,
|
||||
Action<bool> onToggle, Func<string> tooltipProvider, float spacingOverride = -1f)
|
||||
{
|
||||
if (spacingOverride >= 0f)
|
||||
{
|
||||
ImGui.SameLine(0, spacingOverride);
|
||||
}
|
||||
|
||||
var colorsPushed = 0;
|
||||
if (currentState)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.95f, 0.35f, 0.35f, 1f));
|
||||
colorsPushed++;
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(icon, label, width))
|
||||
{
|
||||
var newState = !currentState;
|
||||
onToggle(newState);
|
||||
}
|
||||
|
||||
if (colorsPushed > 0)
|
||||
{
|
||||
ImGui.PopStyleColor(colorsPushed);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(tooltipProvider());
|
||||
}
|
||||
|
||||
private static string DisableStateTooltip(string context, bool disabled)
|
||||
{
|
||||
var state = disabled ? "désactivée" : "activée";
|
||||
return $"Synchronisation {context} par défaut : {state}.\nCliquez pour modifier.";
|
||||
}
|
||||
|
||||
private void DrawAddCharacter()
|
||||
{
|
||||
ImGui.Dummy(new(10));
|
||||
@@ -252,7 +579,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
if (keys.Any())
|
||||
{
|
||||
if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key;
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, L("Compact.AddCharacter.Button", "Add current character with secret key")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key"))
|
||||
{
|
||||
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
|
||||
{
|
||||
@@ -266,11 +593,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_ = _apiController.CreateConnections();
|
||||
}
|
||||
|
||||
_uiSharedService.DrawCombo(L("Compact.AddCharacter.SecretKeyLabel", "Secret Key") + "##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
|
||||
_uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("Compact.AddCharacter.NoKeys", "No secret keys are configured for the current server."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +605,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus);
|
||||
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
|
||||
ImGui.InputTextWithHint("##otheruid", L("Compact.AddPair.Hint", "Other player's UID/Alias"), ref _pairToAdd, 20);
|
||||
ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20);
|
||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
||||
var canAdd = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal));
|
||||
using (ImRaii.Disabled(!canAdd))
|
||||
@@ -288,7 +615,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_ = _apiController.UserAddPair(new(new(_pairToAdd)));
|
||||
_pairToAdd = string.Empty;
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.AddPair.Tooltip", "Pair with {0}", _pairToAdd.IsNullOrEmpty() ? L("Compact.AddPair.Tooltip.DefaultUser", "other user") : _pairToAdd));
|
||||
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
@@ -306,7 +633,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
: 0;
|
||||
|
||||
ImGui.SetNextItemWidth(WindowContentWidth - spacing);
|
||||
ImGui.InputTextWithHint("##filter", L("Compact.Filter.Hint", "Filter for UID/notes"), ref _characterOrCommentFilter, 255);
|
||||
ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255);
|
||||
|
||||
if (userCount == 0) return;
|
||||
|
||||
@@ -355,12 +682,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_buttonState = !_buttonState;
|
||||
}
|
||||
if (!_timeout.IsRunning)
|
||||
UiSharedService.AttachToolTip(L("Compact.Filter.ToggleTooltip", "Hold Control to {0} pairing with {1} out of {2} displayed users.",
|
||||
button == FontAwesomeIcon.Play ? L("Compact.Filter.ToggleTooltip.Resume", "resume") : L("Compact.Filter.ToggleTooltip.Pause", "pause"),
|
||||
users.Count, userCount));
|
||||
UiSharedService.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users.");
|
||||
else
|
||||
UiSharedService.AttachToolTip(L("Compact.Filter.CooldownTooltip", "Next execution is available at {0} seconds",
|
||||
(5000 - _timeout.ElapsedMilliseconds) / 1000));
|
||||
UiSharedService.AttachToolTip($"Next execution is available at {(5000 - _timeout.ElapsedMilliseconds) / 1000} seconds");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,15 +703,21 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY();
|
||||
var users = GetFilteredUsers().OrderBy(u => u.GetPairSortKey(), StringComparer.Ordinal);
|
||||
|
||||
var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
|
||||
var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
|
||||
var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
|
||||
|
||||
ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false);
|
||||
|
||||
var pendingCount = _nearbyPending?.Pending.Count ?? 0;
|
||||
if (pendingCount > 0)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Invitation AutoDetect en attente. Ouvrez l\'interface AutoDetect pour gérer vos demandes.", ImGuiColors.DalamudYellow);
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
}
|
||||
|
||||
var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
|
||||
var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
|
||||
var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
|
||||
|
||||
_pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers);
|
||||
|
||||
// Always show a Nearby group when detection is enabled, even if empty
|
||||
if (_configService.Current.EnableAutoDetectDiscovery)
|
||||
{
|
||||
using (ImRaii.PushId("group-Nearby"))
|
||||
@@ -396,107 +726,59 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_uiSharedService.IconText(icon);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
|
||||
ImGui.SameLine();
|
||||
var onUmbra = _nearbyEntries?.Count(e => e.IsMatch) ?? 0;
|
||||
ImGui.TextUnformatted(L("Compact.Nearby.Title", "Nearby ({0})", onUmbra));
|
||||
var onUmbra = _nearbyEntries?.Count(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e)) ?? 0;
|
||||
ImGui.TextUnformatted($"Nearby ({onUmbra})");
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
|
||||
var nearbyButtonLabel = L("Compact.Nearby.Button", "Nearby");
|
||||
var btnWidth = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, nearbyButtonLabel);
|
||||
var headerRight = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(headerRight - btnWidth);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, nearbyButtonLabel, btnWidth))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||
}
|
||||
|
||||
if (_nearbyOpen)
|
||||
{
|
||||
ImGui.Indent();
|
||||
var nearby = _nearbyEntries == null
|
||||
? new List<Services.Mediator.NearbyEntry>()
|
||||
: _nearbyEntries.Where(e => e.IsMatch)
|
||||
: _nearbyEntries.Where(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e))
|
||||
.OrderBy(e => e.Distance)
|
||||
.ToList();
|
||||
if (nearby.Count == 0)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("Compact.Nearby.None", "No nearby players detected."), ImGuiColors.DalamudGrey3);
|
||||
UiSharedService.ColorTextWrapped("Aucun nouveau joueur detecté.", ImGuiColors.DalamudGrey3);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var e in nearby)
|
||||
{
|
||||
if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token))
|
||||
continue;
|
||||
|
||||
var name = e.DisplayName ?? e.Name;
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(name);
|
||||
var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||
ImGui.SameLine();
|
||||
|
||||
bool isPaired = false;
|
||||
try
|
||||
{
|
||||
isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal));
|
||||
}
|
||||
catch
|
||||
{
|
||||
var key = (e.DisplayName ?? e.Name) ?? string.Empty;
|
||||
isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
|
||||
ImGui.SetCursorPosX(right - statusButtonSize.X);
|
||||
|
||||
if (isPaired)
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.ParsedGreen);
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Tooltip.AlreadyPaired", "Already paired on Umbra"));
|
||||
}
|
||||
else if (!e.AcceptPairRequests)
|
||||
if (!e.AcceptPairRequests)
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Ban, ImGuiColors.DalamudGrey3);
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Tooltip.RequestsDisabled", "Pair requests are disabled for this player"));
|
||||
UiSharedService.AttachToolTip("Les demandes sont désactivées pour ce joueur");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(e.Token))
|
||||
{
|
||||
using (ImRaii.PushId(e.Token ?? e.Uid ?? e.Name ?? string.Empty))
|
||||
{
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
|
||||
{
|
||||
_ = _autoDetectRequestService.SendRequestAsync(e.Token!);
|
||||
_ = _autoDetectRequestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Tooltip.SendInvite", "Send Umbra invitation"));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige");
|
||||
}
|
||||
else
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Tooltip.CannotInvite", "Unable to invite this player"));
|
||||
UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
|
||||
}
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
var inbox = _nearbyPending;
|
||||
if (inbox != null && inbox.Pending.Count > 0)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(6);
|
||||
_uiSharedService.BigText(L("Compact.Nearby.Incoming", "Incoming requests"));
|
||||
foreach (var kv in inbox.Pending)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(L("Compact.Nearby.Incoming.Entry", "{0} [{1}]", kv.Value, kv.Key));
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
||||
{
|
||||
_ = inbox.AcceptAsync(kv.Key);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Incoming.Accept", "Accept and add as pair"));
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
||||
{
|
||||
inbox.Remove(kv.Key);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.Nearby.Incoming.Dismiss", "Dismiss request"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
ImGui.Unindent();
|
||||
ImGui.Separator();
|
||||
}
|
||||
@@ -506,16 +788,34 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(entry.Uid))
|
||||
{
|
||||
if (_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, entry.Uid, StringComparison.Ordinal)))
|
||||
return true;
|
||||
}
|
||||
|
||||
var key = (entry.DisplayName ?? entry.Name) ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(key)) return false;
|
||||
|
||||
return _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawServerStatus()
|
||||
{
|
||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
||||
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
|
||||
var userSize = ImGui.CalcTextSize(userCount);
|
||||
var usersOnlineText = L("Compact.ServerStatus.UsersOnline", "Users Online");
|
||||
var textSize = ImGui.CalcTextSize(usersOnlineText);
|
||||
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase)
|
||||
? string.Empty
|
||||
: L("Compact.ServerStatus.Shard", "Shard: {0}", _apiController.ServerInfo.ShardName);
|
||||
var textSize = ImGui.CalcTextSize("Users Online");
|
||||
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}";
|
||||
var shardTextSize = ImGui.CalcTextSize(shardConnection);
|
||||
var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty;
|
||||
|
||||
@@ -526,12 +826,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextColored(UiSharedService.AccentColor, userCount);
|
||||
ImGui.SameLine();
|
||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(usersOnlineText);
|
||||
ImGui.TextUnformatted("Users Online");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, L("Compact.ServerStatus.NotConnected", "Not connected to any server"));
|
||||
ImGui.TextColored(UiSharedService.AccentColor, "Not connected to any server");
|
||||
}
|
||||
|
||||
if (printShard)
|
||||
@@ -557,7 +857,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(EditProfileUi)));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.ServerStatus.EditProfile", "Edit your Profile"));
|
||||
UiSharedService.AttachToolTip("Edit your Profile");
|
||||
}
|
||||
|
||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
||||
@@ -576,9 +876,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_ = _apiController.CreateConnections();
|
||||
}
|
||||
ImGui.PopStyleColor();
|
||||
UiSharedService.AttachToolTip(!_serverManager.CurrentServer.FullPause
|
||||
? L("Compact.ServerStatus.Disconnect", "Disconnect from {0}", _serverManager.CurrentServer.ServerName)
|
||||
: L("Compact.ServerStatus.Connect", "Connect to {0}", _serverManager.CurrentServer.ServerName));
|
||||
UiSharedService.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,13 +925,13 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
var bottomButtonWidth = (WindowContentWidth - spacing) / 2f;
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, L("Compact.Transfers.CharacterAnalysis", "Character Analysis"), bottomButtonWidth))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, L("Compact.Transfers.CharacterDataHub", "Character Data Hub"), bottomButtonWidth))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
|
||||
}
|
||||
@@ -661,7 +959,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
Mediator.Publish(new OpenSettingsUiMessage());
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.Header.SettingsTooltip", "Open the UmbraSync settings"));
|
||||
UiSharedService.AttachToolTip("Open the UmbraSync Settings");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPos(originalPos);
|
||||
@@ -674,7 +972,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("Compact.Header.CopyUid", "Copy your UID to clipboard"));
|
||||
UiSharedService.AttachToolTip("Copy your UID to clipboard");
|
||||
ImGui.SameLine();
|
||||
}
|
||||
ImGui.SetWindowFontScale(1f);
|
||||
@@ -685,8 +983,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
|
||||
if (_apiController.ServerState is not ServerState.Connected)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
|
||||
{
|
||||
if (_apiController.ServerState is ServerState.NoSecretKey)
|
||||
{
|
||||
DrawAddCharacter();
|
||||
@@ -709,19 +1007,18 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
return _apiController.ServerState switch
|
||||
{
|
||||
ServerState.Connecting => L("Compact.ServerError.Connecting", "Attempting to connect to the server."),
|
||||
ServerState.Reconnecting => L("Compact.ServerError.Reconnecting", "Connection to server interrupted, attempting to reconnect to the server."),
|
||||
ServerState.Disconnected => L("Compact.ServerError.Disconnected", "You are currently disconnected from the sync server."),
|
||||
ServerState.Disconnecting => L("Compact.ServerError.Disconnecting", "Disconnecting from the server"),
|
||||
ServerState.Unauthorized => L("Compact.ServerError.Unauthorized", "Server Response: {0}", _apiController.AuthFailureMessage),
|
||||
ServerState.Offline => L("Compact.ServerError.Offline", "Your selected sync server is currently offline."),
|
||||
ServerState.Connecting => "Attempting to connect to the server.",
|
||||
ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.",
|
||||
ServerState.Disconnected => "You are currently disconnected from the sync server.",
|
||||
ServerState.Disconnecting => "Disconnecting from the server",
|
||||
ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage,
|
||||
ServerState.Offline => "Your selected sync server is currently offline.",
|
||||
ServerState.VersionMisMatch =>
|
||||
L("Compact.ServerError.VersionMismatch",
|
||||
"Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version."),
|
||||
ServerState.RateLimited => L("Compact.ServerError.RateLimited", "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again."),
|
||||
"Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.",
|
||||
ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.",
|
||||
ServerState.Connected => string.Empty,
|
||||
ServerState.NoSecretKey => L("Compact.ServerError.NoSecretKey", "You have no secret key set for this current character. Use the button below or open the settings and set a secret key for the current character. You can reuse the same secret key for multiple characters."),
|
||||
ServerState.MultiChara => L("Compact.ServerError.MultiChara", "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after."),
|
||||
ServerState.NoSecretKey => "You have no secret key set for this current character. Use the button below or open the settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.",
|
||||
ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
@@ -731,17 +1028,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
return _apiController.ServerState switch
|
||||
{
|
||||
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
||||
ServerState.Connected => new Vector4(0.63f, 0.25f, 1f, 1f), // custom violet
|
||||
ServerState.Reconnecting => UiSharedService.AccentColor,
|
||||
ServerState.Connected => UiSharedService.AccentColor,
|
||||
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
||||
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
||||
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
|
||||
ServerState.Offline => ImGuiColors.DalamudRed,
|
||||
ServerState.Unauthorized => UiSharedService.AccentColor,
|
||||
ServerState.VersionMisMatch => UiSharedService.AccentColor,
|
||||
ServerState.Offline => UiSharedService.AccentColor,
|
||||
ServerState.RateLimited => ImGuiColors.DalamudYellow,
|
||||
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
|
||||
ServerState.MultiChara => ImGuiColors.DalamudYellow,
|
||||
_ => ImGuiColors.DalamudRed
|
||||
_ => UiSharedService.AccentColor
|
||||
};
|
||||
}
|
||||
|
||||
@@ -749,16 +1046,16 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
return _apiController.ServerState switch
|
||||
{
|
||||
ServerState.Reconnecting => L("Compact.UidText.Reconnecting", "Reconnecting"),
|
||||
ServerState.Connecting => L("Compact.UidText.Connecting", "Connecting"),
|
||||
ServerState.Disconnected => L("Compact.UidText.Disconnected", "Disconnected"),
|
||||
ServerState.Disconnecting => L("Compact.UidText.Disconnecting", "Disconnecting"),
|
||||
ServerState.Unauthorized => L("Compact.UidText.Unauthorized", "Unauthorized"),
|
||||
ServerState.VersionMisMatch => L("Compact.UidText.VersionMismatch", "Version mismatch"),
|
||||
ServerState.Offline => L("Compact.UidText.Offline", "Unavailable"),
|
||||
ServerState.RateLimited => L("Compact.UidText.RateLimited", "Rate Limited"),
|
||||
ServerState.NoSecretKey => L("Compact.UidText.NoSecretKey", "No Secret Key"),
|
||||
ServerState.MultiChara => L("Compact.UidText.MultiChara", "Duplicate Characters"),
|
||||
ServerState.Reconnecting => "Reconnecting",
|
||||
ServerState.Connecting => "Connecting",
|
||||
ServerState.Disconnected => "Disconnected",
|
||||
ServerState.Disconnecting => "Disconnecting",
|
||||
ServerState.Unauthorized => "Unauthorized",
|
||||
ServerState.VersionMisMatch => "Version mismatch",
|
||||
ServerState.Offline => "Unavailable",
|
||||
ServerState.RateLimited => "Rate Limited",
|
||||
ServerState.NoSecretKey => "No Secret Key",
|
||||
ServerState.MultiChara => "Duplicate Characters",
|
||||
ServerState.Connected => _apiController.DisplayName,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.Localization;
|
||||
|
||||
namespace MareSynchronos.UI.Components;
|
||||
|
||||
@@ -23,23 +27,22 @@ public class DrawGroupPair : DrawPairBase
|
||||
private readonly GroupPairFullInfoDto _fullInfoDto;
|
||||
private readonly GroupFullInfoDto _group;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(System.Globalization.CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
|
||||
|
||||
public DrawGroupPair(string id, Pair entry, ApiController apiController,
|
||||
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto,
|
||||
UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager)
|
||||
UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager,
|
||||
AutoDetectRequestService autoDetectRequestService, ServerConfigurationManager serverConfigurationManager)
|
||||
: base(id, entry, apiController, handler, uiSharedService)
|
||||
{
|
||||
_group = group;
|
||||
_fullInfoDto = fullInfoDto;
|
||||
_mediator = mareMediator;
|
||||
_charaDataManager = charaDataManager;
|
||||
_autoDetectRequestService = autoDetectRequestService;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||
@@ -50,20 +53,20 @@ public class DrawGroupPair : DrawPairBase
|
||||
var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned();
|
||||
var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : FontAwesomeIcon.CloudMoon;
|
||||
var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? new Vector4(0.63f, 0.25f, 1f, 1f) : ImGuiColors.DalamudGrey;
|
||||
var presenceText = L("GroupPair.Presence.Offline", "{0} is offline", entryUID);
|
||||
var presenceText = entryUID + " is offline";
|
||||
|
||||
ImGui.SetCursorPosY(textPosY);
|
||||
bool drewPrefixIcon = false;
|
||||
|
||||
if (_pair.IsPaused)
|
||||
{
|
||||
presenceText = L("GroupPair.Presence.Paused", "{0} online status is unknown (paused)", entryUID);
|
||||
presenceText = entryUID + " online status is unknown (paused)";
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow);
|
||||
ImGui.PopFont();
|
||||
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.Paused", "Pairing status with {0} is paused", entryUID));
|
||||
UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused");
|
||||
drewPrefixIcon = true;
|
||||
}
|
||||
else
|
||||
@@ -75,7 +78,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), violet);
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.IndividuallyPaired", "You are individually paired with {0}", entryUID));
|
||||
UiSharedService.AttachToolTip("You are individually paired with " + entryUID);
|
||||
drewPrefixIcon = true;
|
||||
}
|
||||
}
|
||||
@@ -87,10 +90,8 @@ public class DrawGroupPair : DrawPairBase
|
||||
UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor);
|
||||
ImGui.PopFont();
|
||||
|
||||
if (_pair.IsOnline && !_pair.IsVisible)
|
||||
presenceText = L("GroupPair.Presence.Online", "{0} is online", entryUID);
|
||||
else if (_pair.IsOnline && _pair.IsVisible)
|
||||
presenceText = L("GroupPair.Presence.Visible", "{0} is visible: {1}\nClick to target this player", entryUID, _pair.PlayerName);
|
||||
if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online";
|
||||
else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
|
||||
|
||||
if (_pair.IsVisible)
|
||||
{
|
||||
@@ -101,19 +102,16 @@ public class DrawGroupPair : DrawPairBase
|
||||
if (_pair.LastAppliedDataBytes >= 0)
|
||||
{
|
||||
presenceText += UiSharedService.TooltipSeparator;
|
||||
presenceText += (!_pair.IsVisible ? L("GroupPair.Presence.LastPrefix", "(Last) ") : string.Empty)
|
||||
+ L("GroupPair.Presence.ModsInfo", "Mods Info") + Environment.NewLine;
|
||||
presenceText += L("GroupPair.Presence.FilesSize", "Files Size: {0}", UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true));
|
||||
presenceText += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
|
||||
presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true);
|
||||
if (_pair.LastAppliedApproximateVRAMBytes >= 0)
|
||||
{
|
||||
presenceText += Environment.NewLine + L("GroupPair.Presence.Vram", "Approx. VRAM Usage: {0}", UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true));
|
||||
presenceText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true);
|
||||
}
|
||||
if (_pair.LastAppliedDataTris >= 0)
|
||||
{
|
||||
var trisValue = _pair.LastAppliedDataTris > 1000
|
||||
? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'", System.Globalization.CultureInfo.CurrentCulture)
|
||||
: _pair.LastAppliedDataTris.ToString(System.Globalization.CultureInfo.CurrentCulture);
|
||||
presenceText += Environment.NewLine + L("GroupPair.Presence.Tris", "Triangle Count (excl. Vanilla): {0}", trisValue);
|
||||
presenceText += Environment.NewLine + "Triangle Count (excl. Vanilla): "
|
||||
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +125,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.Owner", "User is owner of this Syncshell"));
|
||||
UiSharedService.AttachToolTip("User is owner of this Syncshell");
|
||||
}
|
||||
else if (entryIsMod)
|
||||
{
|
||||
@@ -136,7 +134,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString());
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.Moderator", "User is moderator of this Syncshell"));
|
||||
UiSharedService.AttachToolTip("User is moderator of this Syncshell");
|
||||
}
|
||||
else if (entryIsPinned)
|
||||
{
|
||||
@@ -145,7 +143,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString());
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.Pinned", "User is pinned in this Syncshell"));
|
||||
UiSharedService.AttachToolTip("User is pinned in this Syncshell");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +165,9 @@ public class DrawGroupPair : DrawPairBase
|
||||
|
||||
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
|
||||
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled);
|
||||
bool showPlus = _pair.UserPair == null;
|
||||
bool showPlus = _pair.UserPair == null && _pair.IsOnline;
|
||||
bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
|
||||
bool showPause = true;
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
|
||||
@@ -176,12 +175,15 @@ public class DrawGroupPair : DrawPairBase
|
||||
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
|
||||
var infoIconWidth = UiSharedService.GetIconSize(permIcon).X;
|
||||
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
|
||||
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
|
||||
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||
|
||||
var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing
|
||||
- (showShared ? (runningIconWidth + spacing) : 0)
|
||||
- (showInfo ? (infoIconWidth + spacing) : 0)
|
||||
- (showPlus ? (plusButtonWidth + spacing) : 0)
|
||||
- (showPause ? (pauseButtonWidth + spacing) : 0)
|
||||
- (showBars ? (barButtonWidth + spacing) : 0);
|
||||
|
||||
ImGui.SameLine(pos);
|
||||
@@ -190,9 +192,8 @@ public class DrawGroupPair : DrawPairBase
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Running);
|
||||
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Tooltip.SharedData", "This user has shared {0} Character Data Sets with you.", sharedData!.Count)
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("GroupPair.Tooltip.SharedData.OpenHub", "Click to open the Character Data Hub and show the entries."));
|
||||
UiSharedService.AttachToolTip($"This user has shared {sharedData!.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
||||
+ "Click to open the Character Data Hub and show the entries.");
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
@@ -201,7 +202,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
||||
if (individualAnimDisabled || individualSoundsDisabled)
|
||||
{
|
||||
ImGui.SetCursorPosY(textPosY);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
|
||||
@@ -211,52 +212,46 @@ public class DrawGroupPair : DrawPairBase
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
ImGui.TextUnformatted(L("GroupPair.Tooltip.IndividualHeader", "Individual User permissions"));
|
||||
ImGui.TextUnformatted("Individual User permissions");
|
||||
|
||||
if (individualSoundsDisabled)
|
||||
{
|
||||
var userSoundsText = L("GroupPair.Tooltip.SoundWith", "Sound sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
||||
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userSoundsText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(L("GroupPair.Tooltip.YouThey", "You: {0}, They: {1}",
|
||||
_pair.UserPair!.OwnPermissions.IsDisableSounds() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled"),
|
||||
_pair.UserPair!.OtherPermissions.IsDisableSounds() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled")));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
if (individualAnimDisabled)
|
||||
{
|
||||
var userAnimText = L("GroupPair.Tooltip.AnimWith", "Animation sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
||||
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userAnimText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(L("GroupPair.Tooltip.YouThey", "You: {0}, They: {1}",
|
||||
_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled"),
|
||||
_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled")));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
if (individualVFXDisabled)
|
||||
{
|
||||
var userVFXText = L("GroupPair.Tooltip.VfxWith", "VFX sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
||||
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userVFXText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(L("GroupPair.Tooltip.YouThey", "You: {0}, They: {1}",
|
||||
_pair.UserPair!.OwnPermissions.IsDisableVFX() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled"),
|
||||
_pair.UserPair!.OtherPermissions.IsDisableVFX() ? L("GroupPair.Toggle.Disabled", "Disabled") : L("GroupPair.Toggle.Enabled", "Enabled")));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
ImGui.SameLine();
|
||||
}
|
||||
else if ((animDisabled || soundsDisabled || vfxDisabled))
|
||||
else if ((animDisabled || soundsDisabled))
|
||||
{
|
||||
ImGui.SetCursorPosY(textPosY);
|
||||
_uiSharedService.IconText(permIcon);
|
||||
@@ -264,28 +259,28 @@ public class DrawGroupPair : DrawPairBase
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
ImGui.TextUnformatted(L("GroupPair.Tooltip.SyncshellHeader", "Syncshell User permissions"));
|
||||
ImGui.TextUnformatted("Syncshell User permissions");
|
||||
|
||||
if (soundsDisabled)
|
||||
{
|
||||
var userSoundsText = L("GroupPair.Tooltip.SoundBy", "Sound sync disabled by {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
||||
var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userSoundsText);
|
||||
}
|
||||
|
||||
if (animDisabled)
|
||||
{
|
||||
var userAnimText = L("GroupPair.Tooltip.AnimBy", "Animation sync disabled by {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
||||
var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userAnimText);
|
||||
}
|
||||
|
||||
if (vfxDisabled)
|
||||
{
|
||||
var userVFXText = L("GroupPair.Tooltip.VfxBy", "VFX sync disabled by {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
||||
var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userVFXText);
|
||||
}
|
||||
@@ -301,9 +296,28 @@ public class DrawGroupPair : DrawPairBase
|
||||
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
||||
{
|
||||
_ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID)));
|
||||
var targetUid = _pair.UserData.UID;
|
||||
if (!string.IsNullOrEmpty(targetUid))
|
||||
{
|
||||
_ = SendGroupPairInviteAsync(targetUid, entryUID);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPair.Popup.PairIndividually", "Pair with {0} individually", entryUID));
|
||||
}
|
||||
UiSharedService.AttachToolTip(AppendSeenInfo("Send pairing invite to " + entryUID));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
if (showPause)
|
||||
{
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
|
||||
if (_uiSharedService.IconButton(pauseIcon))
|
||||
{
|
||||
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
||||
_fullInfoDto.GroupUserPermissions = newPermissions;
|
||||
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_group.Group, _pair.UserData, newPermissions));
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(AppendSeenInfo((_fullInfoDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " syncing with " + entryUID));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
@@ -404,4 +418,74 @@ public class DrawGroupPair : DrawPairBase
|
||||
|
||||
return pos - spacing;
|
||||
}
|
||||
|
||||
private string AppendSeenInfo(string tooltip)
|
||||
{
|
||||
if (_pair.IsVisible) return tooltip;
|
||||
|
||||
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
|
||||
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
|
||||
|
||||
return tooltip + " (Vu sous : " + lastSeen + ")";
|
||||
}
|
||||
|
||||
private async Task SendGroupPairInviteAsync(string targetUid, string displayName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await _autoDetectRequestService.SendDirectUidRequestAsync(targetUid, displayName).ConfigureAwait(false);
|
||||
if (!ok) return;
|
||||
|
||||
await SendManualInviteSignalAsync(targetUid, displayName).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// errors are logged within the request service; ignore here
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendManualInviteSignalAsync(string targetUid, string displayName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_apiController.UID)) return;
|
||||
|
||||
var senderAliasRaw = string.IsNullOrEmpty(_apiController.DisplayName) ? _apiController.UID : _apiController.DisplayName;
|
||||
var senderAlias = EncodeInviteField(senderAliasRaw);
|
||||
var targetDisplay = EncodeInviteField(displayName);
|
||||
var inviteId = Guid.NewGuid().ToString("N");
|
||||
var payloadText = new StringBuilder()
|
||||
.Append(ManualPairInvitePrefix)
|
||||
.Append(_apiController.UID)
|
||||
.Append('|')
|
||||
.Append(senderAlias)
|
||||
.Append('|')
|
||||
.Append(targetUid)
|
||||
.Append('|')
|
||||
.Append(targetDisplay)
|
||||
.Append('|')
|
||||
.Append(inviteId)
|
||||
.Append(']')
|
||||
.ToString();
|
||||
|
||||
var payload = new SeStringBuilder().AddText(payloadText).Build().Encode();
|
||||
var chatMessage = new ChatMessage
|
||||
{
|
||||
SenderName = senderAlias,
|
||||
PayloadContent = payload
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _apiController.GroupChatSendMsg(new GroupDto(_group.Group), chatMessage).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore - invite remains tracked locally even if group chat signal fails
|
||||
}
|
||||
}
|
||||
|
||||
private static string EncodeInviteField(string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,13 @@ using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using MareSynchronos.WebAPI;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using System;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
|
||||
namespace MareSynchronos.UI.Components;
|
||||
|
||||
@@ -23,17 +21,12 @@ public class DrawUserPair : DrawPairBase
|
||||
protected readonly MareMediator _mediator;
|
||||
private readonly SelectGroupForPairUi _selectGroupForPairUi;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
|
||||
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
|
||||
UiSharedService uiSharedService, CharaDataManager charaDataManager)
|
||||
UiSharedService uiSharedService, CharaDataManager charaDataManager,
|
||||
ServerConfigurationManager serverConfigurationManager)
|
||||
: base(id, entry, apiController, displayHandler, uiSharedService)
|
||||
{
|
||||
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
|
||||
@@ -41,6 +34,7 @@ public class DrawUserPair : DrawPairBase
|
||||
_selectGroupForPairUi = selectGroupForPairUi;
|
||||
_mediator = mareMediator;
|
||||
_charaDataManager = charaDataManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
public bool IsOnline => _pair.IsOnline;
|
||||
@@ -57,16 +51,16 @@ public class DrawUserPair : DrawPairBase
|
||||
UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? Violet : offlineGrey);
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(online
|
||||
? L("UserPair.Status.Online", "User is online")
|
||||
: L("UserPair.Status.Offline", "User is offline"));
|
||||
? "User is online"
|
||||
: "User is offline");
|
||||
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(textPosY);
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), UiSharedService.AccentColor);
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("UserPair.Tooltip.NotAddedBack", "{0} has not added you back", _pair.UserData.AliasOrUID));
|
||||
UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has not added you back");
|
||||
}
|
||||
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
|
||||
{
|
||||
@@ -75,7 +69,7 @@ public class DrawUserPair : DrawPairBase
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow);
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("UserPair.Tooltip.Paused", "Pairing with {0} is paused", _pair.UserData.AliasOrUID));
|
||||
UiSharedService.AttachToolTip("Pairing with " + _pair.UserData.AliasOrUID + " is paused");
|
||||
}
|
||||
if (_pair is { IsOnline: true, IsVisible: true })
|
||||
{
|
||||
@@ -88,23 +82,20 @@ public class DrawUserPair : DrawPairBase
|
||||
_mediator.Publish(new TargetPairMessage(_pair));
|
||||
}
|
||||
ImGui.PopFont();
|
||||
var visibleTooltip = L("UserPair.Tooltip.Visible", "{0} is visible: {1}\nClick to target this player", _pair.UserData.AliasOrUID, _pair.PlayerName!);
|
||||
var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName! + Environment.NewLine + "Click to target this player";
|
||||
if (_pair.LastAppliedDataBytes >= 0)
|
||||
{
|
||||
visibleTooltip += UiSharedService.TooltipSeparator;
|
||||
visibleTooltip += (!_pair.IsVisible ? L("UserPair.Tooltip.Visible.LastPrefix", "(Last) ") : string.Empty)
|
||||
+ L("UserPair.Tooltip.Visible.ModsInfo", "Mods Info") + Environment.NewLine;
|
||||
visibleTooltip += L("UserPair.Tooltip.Visible.FilesSize", "Files Size: {0}", UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true));
|
||||
visibleTooltip += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
|
||||
visibleTooltip += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true);
|
||||
if (_pair.LastAppliedApproximateVRAMBytes >= 0)
|
||||
{
|
||||
visibleTooltip += Environment.NewLine + L("UserPair.Tooltip.Visible.Vram", "Approx. VRAM Usage: {0}", UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true));
|
||||
visibleTooltip += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true);
|
||||
}
|
||||
if (_pair.LastAppliedDataTris >= 0)
|
||||
{
|
||||
var trisValue = _pair.LastAppliedDataTris > 1000
|
||||
? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'", CultureInfo.CurrentCulture)
|
||||
: _pair.LastAppliedDataTris.ToString(CultureInfo.CurrentCulture);
|
||||
visibleTooltip += Environment.NewLine + L("UserPair.Tooltip.Visible.Tris", "Triangle Count (excl. Vanilla): {0}", trisValue);
|
||||
visibleTooltip += Environment.NewLine + "Triangle Count (excl. Vanilla): "
|
||||
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,9 +139,9 @@ public class DrawUserPair : DrawPairBase
|
||||
perm.SetPaused(!perm.IsPaused());
|
||||
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
|
||||
? L("UserPair.Tooltip.Pause", "Pause pairing with {0}", entryUID)
|
||||
: L("UserPair.Tooltip.Resume", "Resume pairing with {0}", entryUID));
|
||||
UiSharedService.AttachToolTip(AppendSeenInfo(!_pair.UserPair!.OwnPermissions.IsPaused()
|
||||
? "Pause pairing with " + entryUID
|
||||
: "Resume pairing with " + entryUID));
|
||||
|
||||
|
||||
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
||||
@@ -171,57 +162,39 @@ public class DrawUserPair : DrawPairBase
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
ImGui.TextUnformatted(L("UserPair.Tooltip.Permission.Header", "Individual user permissions"));
|
||||
ImGui.TextUnformatted("Individual User permissions");
|
||||
|
||||
if (individualSoundsDisabled)
|
||||
{
|
||||
var userSoundsText = L("UserPair.Tooltip.Permission.Sound", "Sound sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
||||
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userSoundsText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
var youStatus = _pair.UserPair!.OwnPermissions.IsDisableSounds()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
var theyStatus = _pair.UserPair!.OtherPermissions.IsDisableSounds()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
ImGui.TextUnformatted(L("UserPair.Tooltip.Permission.Status", "You: {0}, They: {1}", youStatus, theyStatus));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
if (individualAnimDisabled)
|
||||
{
|
||||
var userAnimText = L("UserPair.Tooltip.Permission.Animation", "Animation sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
||||
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userAnimText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
var youStatus = _pair.UserPair!.OwnPermissions.IsDisableAnimations()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
var theyStatus = _pair.UserPair!.OtherPermissions.IsDisableAnimations()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
ImGui.TextUnformatted(L("UserPair.Tooltip.Permission.Status", "You: {0}, They: {1}", youStatus, theyStatus));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
if (individualVFXDisabled)
|
||||
{
|
||||
var userVFXText = L("UserPair.Tooltip.Permission.Vfx", "VFX sync disabled with {0}", _pair.UserData.AliasOrUID);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
||||
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
||||
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userVFXText);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
var youStatus = _pair.UserPair!.OwnPermissions.IsDisableVFX()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
var theyStatus = _pair.UserPair!.OtherPermissions.IsDisableVFX()
|
||||
? L("UserPair.Tooltip.Permission.State.Disabled", "Disabled")
|
||||
: L("UserPair.Tooltip.Permission.State.Enabled", "Enabled");
|
||||
ImGui.TextUnformatted(L("UserPair.Tooltip.Permission.Status", "You: {0}, They: {1}", youStatus, theyStatus));
|
||||
ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled"));
|
||||
}
|
||||
|
||||
ImGui.EndTooltip();
|
||||
@@ -236,9 +209,8 @@ public class DrawUserPair : DrawPairBase
|
||||
ImGui.SameLine(rightSidePos);
|
||||
_uiSharedService.IconText(icon);
|
||||
|
||||
UiSharedService.AttachToolTip(L("UserPair.Tooltip.SharedData", "This user has shared {0} Character Data Sets with you.", sharedData.Count)
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("UserPair.Tooltip.SharedData.OpenHub", "Click to open the Character Data Hub and show the entries."));
|
||||
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
||||
+ "Click to open the Character Data Hub and show the entries.");
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
@@ -253,7 +225,7 @@ public class DrawUserPair : DrawPairBase
|
||||
{
|
||||
if (entry.IsVisible)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eye, L("UserPair.Menu.Target", "Target player")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eye, "Target player"))
|
||||
{
|
||||
_mediator.Publish(new TargetPairMessage(entry));
|
||||
ImGui.CloseCurrentPopup();
|
||||
@@ -261,82 +233,89 @@ public class DrawUserPair : DrawPairBase
|
||||
}
|
||||
if (!entry.IsPaused)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, L("UserPair.Menu.OpenProfile", "Open Profile")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile"))
|
||||
{
|
||||
_displayHandler.OpenProfile(entry);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("UserPair.Menu.OpenProfile.Tooltip", "Opens the profile for this user in a new window"));
|
||||
UiSharedService.AttachToolTip("Opens the profile for this user in a new window");
|
||||
}
|
||||
if (entry.IsVisible)
|
||||
{
|
||||
#if DEBUG
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, L("UserPair.Menu.OpenAnalysis", "Open Analysis")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis"))
|
||||
{
|
||||
_displayHandler.OpenAnalysis(_pair);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
#endif
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, L("UserPair.Menu.ReloadData", "Reload last data")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data"))
|
||||
{
|
||||
entry.ApplyLastReceivedData(forced: true);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("UserPair.Menu.ReloadData.Tooltip", "This reapplies the last received character data to this character"));
|
||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, L("UserPair.Menu.CyclePause", "Cycle pause state")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state"))
|
||||
{
|
||||
_ = _apiController.CyclePause(entry.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
var entryUID = entry.UserData.AliasOrUID;
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, L("UserPair.Menu.PairGroups", "Pair Groups")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups"))
|
||||
{
|
||||
_selectGroupForPairUi.Open(entry);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("UserPair.Menu.PairGroups.Tooltip", "Choose pair groups for {0}", entryUID));
|
||||
UiSharedService.AttachToolTip(AppendSeenInfo("Choose pair groups for " + entryUID));
|
||||
|
||||
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
|
||||
string disableSoundsText = isDisableSounds
|
||||
? L("UserPair.Menu.EnableSoundSync", "Enable sound sync")
|
||||
: L("UserPair.Menu.DisableSoundSync", "Disable sound sync");
|
||||
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute;
|
||||
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
|
||||
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||
if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText))
|
||||
{
|
||||
var permissions = entry.UserPair.OwnPermissions;
|
||||
permissions.SetDisableSounds(!isDisableSounds);
|
||||
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, permissions.IsDisableSounds(), null, null));
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||
}
|
||||
|
||||
var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations();
|
||||
string disableAnimsText = isDisableAnims
|
||||
? L("UserPair.Menu.EnableAnimationSync", "Enable animation sync")
|
||||
: L("UserPair.Menu.DisableAnimationSync", "Disable animation sync");
|
||||
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop;
|
||||
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
|
||||
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||
if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText))
|
||||
{
|
||||
var permissions = entry.UserPair.OwnPermissions;
|
||||
permissions.SetDisableAnimations(!isDisableAnims);
|
||||
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, permissions.IsDisableAnimations(), null));
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||
}
|
||||
|
||||
var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX();
|
||||
string disableVFXText = isDisableVFX
|
||||
? L("UserPair.Menu.EnableVfxSync", "Enable VFX sync")
|
||||
: L("UserPair.Menu.DisableVfxSync", "Disable VFX sync");
|
||||
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle;
|
||||
string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync";
|
||||
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||
if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText))
|
||||
{
|
||||
var permissions = entry.UserPair.OwnPermissions;
|
||||
permissions.SetDisableVFX(!isDisableVFX);
|
||||
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, null, permissions.IsDisableVFX()));
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, L("UserPair.Menu.Unpair", "Unpair Permanently")) && UiSharedService.CtrlPressed())
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_ = _apiController.UserRemovePair(new(entry.UserData));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("UserPair.Menu.Unpair.Tooltip", "Hold CTRL and click to unpair permanently from {0}", entryUID));
|
||||
UiSharedService.AttachToolTip(AppendSeenInfo("Hold CTRL and click to unpair permanently from " + entryUID));
|
||||
}
|
||||
|
||||
private string AppendSeenInfo(string tooltip)
|
||||
{
|
||||
if (_pair.IsVisible) return tooltip;
|
||||
|
||||
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
|
||||
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
|
||||
|
||||
return tooltip + " (Vu sous : " + lastSeen + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.UI.Components;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.Localization;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
@@ -33,6 +33,7 @@ internal sealed class GroupPanel
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
|
||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||
private readonly UiSharedService _uiShared;
|
||||
@@ -73,16 +74,9 @@ internal sealed class GroupPanel
|
||||
private string _syncShellPassword = string.Empty;
|
||||
private string _syncShellToJoin = string.Empty;
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
|
||||
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
|
||||
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager,
|
||||
CharaDataManager charaDataManager)
|
||||
CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService)
|
||||
{
|
||||
_mainUi = mainUi;
|
||||
_uiShared = uiShared;
|
||||
@@ -92,6 +86,7 @@ internal sealed class GroupPanel
|
||||
_mareConfig = mareConfig;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_charaDataManager = charaDataManager;
|
||||
_autoDetectRequestService = autoDetectRequestService;
|
||||
}
|
||||
|
||||
private ApiController ApiController => _uiShared.ApiController;
|
||||
@@ -107,15 +102,13 @@ internal sealed class GroupPanel
|
||||
{
|
||||
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
|
||||
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
|
||||
ImGui.InputTextWithHint("##syncshellid", L("GroupPanel.Join.InputHint", "Syncshell GID/Alias (leave empty to create)"), ref _syncShellToJoin, 50);
|
||||
ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 50);
|
||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
||||
|
||||
bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser;
|
||||
bool userCanCreateMoreGroups = _pairManager.GroupPairs.Count(u => string.Equals(u.Key.Owner.UID, ApiController.UID, StringComparison.Ordinal)) < ApiController.ServerInfo.MaxGroupsCreatedByUser;
|
||||
bool alreadyInGroup = _pairManager.GroupPairs.Select(p => p.Key).Any(p => string.Equals(p.Group.Alias, _syncShellToJoin, StringComparison.Ordinal)
|
||||
|| string.Equals(p.Group.GID, _syncShellToJoin, StringComparison.Ordinal));
|
||||
var enterPasswordPopupTitle = L("GroupPanel.Join.PasswordPopup", "Enter Syncshell Password");
|
||||
var createPopupTitle = L("GroupPanel.Create.PopupTitle", "Create Syncshell");
|
||||
|
||||
if (alreadyInGroup) ImGui.BeginDisabled();
|
||||
if (_uiShared.IconButton(FontAwesomeIcon.Plus))
|
||||
@@ -126,7 +119,7 @@ internal sealed class GroupPanel
|
||||
{
|
||||
_errorGroupJoin = false;
|
||||
_showModalEnterPassword = true;
|
||||
ImGui.OpenPopup(enterPasswordPopupTitle);
|
||||
ImGui.OpenPopup("Enter Syncshell Password");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -140,37 +133,30 @@ internal sealed class GroupPanel
|
||||
_tempSyncshellDurationHours = 24;
|
||||
_errorGroupCreateMessage = string.Empty;
|
||||
_showModalCreateGroup = true;
|
||||
ImGui.OpenPopup(createPopupTitle);
|
||||
ImGui.OpenPopup("Create Syncshell");
|
||||
}
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip(_syncShellToJoin.IsNullOrEmpty()
|
||||
? (userCanCreateMoreGroups
|
||||
? L("GroupPanel.Create.Tooltip", "Create Syncshell")
|
||||
: L("GroupPanel.Create.TooMany", "You cannot create more than {0} Syncshells", ApiController.ServerInfo.MaxGroupsCreatedByUser))
|
||||
: (userCanJoinMoreGroups
|
||||
? L("GroupPanel.Join.Tooltip", "Join Syncshell {0}", _syncShellToJoin)
|
||||
: L("GroupPanel.Join.TooMany", "You cannot join more than {0} Syncshells", ApiController.ServerInfo.MaxGroupsJoinedByUser)));
|
||||
? (userCanCreateMoreGroups ? "Create Syncshell" : $"You cannot create more than {ApiController.ServerInfo.MaxGroupsCreatedByUser} Syncshells")
|
||||
: (userCanJoinMoreGroups ? "Join Syncshell" + _syncShellToJoin : $"You cannot join more than {ApiController.ServerInfo.MaxGroupsJoinedByUser} Syncshells"));
|
||||
|
||||
if (alreadyInGroup) ImGui.EndDisabled();
|
||||
|
||||
if (ImGui.BeginPopupModal(enterPasswordPopupTitle, ref _showModalEnterPassword, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Enter Syncshell Password", ref _showModalEnterPassword, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Join.Warning", "Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell."));
|
||||
UiSharedService.TextWrapped("Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell.");
|
||||
ImGui.Separator();
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Join.EnterPassword", "Enter the password for Syncshell {0}:", _syncShellToJoin));
|
||||
UiSharedService.TextWrapped("Enter the password for Syncshell " + _syncShellToJoin + ":");
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##password", L("GroupPanel.Join.PasswordHint", "{0} Password", _syncShellToJoin), ref _syncShellPassword, 255, ImGuiInputTextFlags.Password);
|
||||
ImGui.InputTextWithHint("##password", _syncShellToJoin + " Password", ref _syncShellPassword, 255, ImGuiInputTextFlags.Password);
|
||||
if (_errorGroupJoin)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(
|
||||
L("GroupPanel.Join.Error",
|
||||
"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({0}), it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({1} users) or the Syncshell has closed invites.",
|
||||
ApiController.ServerInfo.MaxGroupsJoinedByUser,
|
||||
ApiController.ServerInfo.MaxGroupUserCount),
|
||||
UiSharedService.ColorTextWrapped($"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({ApiController.ServerInfo.MaxGroupsJoinedByUser}), " +
|
||||
$"it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({ApiController.ServerInfo.MaxGroupUserCount} users) or the Syncshell has closed invites.",
|
||||
new Vector4(1, 0, 0, 1));
|
||||
}
|
||||
if (ImGui.Button(L("GroupPanel.Join.Button", "Join {0}", _syncShellToJoin)))
|
||||
if (ImGui.Button("Join " + _syncShellToJoin))
|
||||
{
|
||||
var shell = _syncShellToJoin;
|
||||
var pw = _syncShellPassword;
|
||||
@@ -186,16 +172,16 @@ internal sealed class GroupPanel
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopupModal(createPopupTitle, ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.ChooseType", "Choisissez le type de Syncshell à créer."));
|
||||
UiSharedService.TextWrapped("Choisissez le type de Syncshell à créer.");
|
||||
bool showPermanent = !_createIsTemporary;
|
||||
if (ImGui.RadioButton(L("GroupPanel.Create.Permanent", "Permanente"), showPermanent))
|
||||
if (ImGui.RadioButton("Permanente", showPermanent))
|
||||
{
|
||||
_createIsTemporary = false;
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.RadioButton(L("GroupPanel.Create.Temporary", "Temporaire"), _createIsTemporary))
|
||||
if (ImGui.RadioButton("Temporaire", _createIsTemporary))
|
||||
{
|
||||
_createIsTemporary = true;
|
||||
_newSyncShellAlias = string.Empty;
|
||||
@@ -203,9 +189,9 @@ internal sealed class GroupPanel
|
||||
|
||||
if (!_createIsTemporary)
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.AliasPrompt", "Donnez un nom à votre Syncshell (optionnel) puis créez-la."));
|
||||
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##syncshellalias", L("GroupPanel.Create.AliasHint", "Nom du Syncshell"), ref _newSyncShellAlias, 50);
|
||||
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -214,7 +200,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (_createIsTemporary)
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.TempMaxDuration", "Durée maximale d'une Syncshell temporaire : 7 jours."));
|
||||
UiSharedService.TextWrapped("Durée maximale d'une Syncshell temporaire : 7 jours.");
|
||||
if (_tempSyncshellDurationHours > 168) _tempSyncshellDurationHours = 168;
|
||||
for (int i = 0; i < _temporaryDurationOptions.Length; i++)
|
||||
{
|
||||
@@ -222,10 +208,8 @@ internal sealed class GroupPanel
|
||||
var isSelected = _tempSyncshellDurationHours == option;
|
||||
string label = option switch
|
||||
{
|
||||
>= 24 when option % 24 == 0 => option == 24
|
||||
? L("GroupPanel.Create.Duration.SingleDay", "24h")
|
||||
: L("GroupPanel.Create.Duration.Days", "{0}j", option / 24),
|
||||
_ => L("GroupPanel.Create.Duration.Hours", "{0}h", option)
|
||||
>= 24 when option % 24 == 0 => option == 24 ? "24h" : $"{option / 24}j",
|
||||
_ => option + "h"
|
||||
};
|
||||
|
||||
if (ImGui.RadioButton(label, isSelected))
|
||||
@@ -233,6 +217,7 @@ internal sealed class GroupPanel
|
||||
_tempSyncshellDurationHours = option;
|
||||
}
|
||||
|
||||
// Start a new line after every 3 buttons
|
||||
if ((i + 1) % 3 == 0)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
@@ -244,12 +229,12 @@ internal sealed class GroupPanel
|
||||
}
|
||||
|
||||
var expiresLocal = DateTime.Now.AddHours(_tempSyncshellDurationHours);
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.TempExpires", "Expiration le {0:g} (heure locale).", expiresLocal));
|
||||
UiSharedService.TextWrapped($"Expiration le {expiresLocal:g} (heure locale).");
|
||||
}
|
||||
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.Instruction", "Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell."));
|
||||
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
|
||||
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.Button(L("GroupPanel.Create.Button", "Create Syncshell")))
|
||||
if (ImGui.Button("Create Syncshell"))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -274,7 +259,7 @@ internal sealed class GroupPanel
|
||||
_errorGroupCreate = true;
|
||||
if (ex.Message.Contains("name is already in use", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_errorGroupCreateMessage = L("GroupPanel.Create.Error.NameInUse", "Le nom de la Syncshell est déjà utilisé.");
|
||||
_errorGroupCreateMessage = "Le nom de la Syncshell est déjà utilisé.";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -290,28 +275,28 @@ internal sealed class GroupPanel
|
||||
_errorGroupCreateMessage = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
|
||||
{
|
||||
ImGui.TextUnformatted(L("GroupPanel.Create.Result.Name", "Syncshell Name: {0}", _lastCreatedGroup.Group.Alias));
|
||||
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
|
||||
}
|
||||
ImGui.TextUnformatted(L("GroupPanel.Create.Result.Id", "Syncshell ID: {0}", _lastCreatedGroup.Group.GID));
|
||||
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(L("GroupPanel.Create.Result.Password", "Syncshell Password: {0}", _lastCreatedGroup.Password));
|
||||
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);
|
||||
ImGui.SameLine();
|
||||
if (_uiShared.IconButton(FontAwesomeIcon.Copy))
|
||||
{
|
||||
ImGui.SetClipboardText(_lastCreatedGroup.Password);
|
||||
}
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.Result.ChangeLater", "You can change the Syncshell password later at any time."));
|
||||
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
|
||||
if (_lastCreatedGroup.IsTemporary && _lastCreatedGroup.ExpiresAt != null)
|
||||
{
|
||||
var expiresLocal = _lastCreatedGroup.ExpiresAt.Value.ToLocalTime();
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Create.Result.TempExpires", "Cette Syncshell expirera le {0:g} (heure locale).", expiresLocal));
|
||||
UiSharedService.TextWrapped($"Cette Syncshell expirera le {expiresLocal:g} (heure locale).");
|
||||
}
|
||||
}
|
||||
|
||||
if (_errorGroupCreate)
|
||||
{
|
||||
var msg = string.IsNullOrWhiteSpace(_errorGroupCreateMessage)
|
||||
? L("GroupPanel.Create.Error.Generic", "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.")
|
||||
? "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells."
|
||||
: _errorGroupCreateMessage;
|
||||
UiSharedService.ColorTextWrapped(msg, new Vector4(1, 0, 0, 1));
|
||||
}
|
||||
@@ -350,7 +335,7 @@ internal sealed class GroupPanel
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Syncshell.OwnerTooltip", "You are the owner of Syncshell {0}", groupName));
|
||||
UiSharedService.AttachToolTip("You are the owner of Syncshell " + groupName);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
else if (groupDto.GroupUserInfo.IsModerator())
|
||||
@@ -358,7 +343,7 @@ internal sealed class GroupPanel
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString());
|
||||
ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Syncshell.ModeratorTooltip", "You are a moderator of Syncshell {0}", groupName));
|
||||
UiSharedService.AttachToolTip("You are a moderator of Syncshell " + groupName);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
@@ -376,33 +361,29 @@ internal sealed class GroupPanel
|
||||
var totalMembers = pairsInGroup.Count + 1;
|
||||
var connectedMembers = pairsInGroup.Count(p => p.IsOnline) + 1;
|
||||
var maxCapacity = ApiController.ServerInfo.MaxGroupUserCount;
|
||||
ImGui.TextUnformatted(L("GroupPanel.Syncshell.MemberCount", "{0}/{1}", connectedMembers, totalMembers));
|
||||
UiSharedService.AttachToolTip(
|
||||
L("GroupPanel.Syncshell.MemberCountTooltip",
|
||||
"Membres connectés / membres totaux\nCapacité maximale : {0}\nSyncshell ID: {1}",
|
||||
maxCapacity,
|
||||
groupDto.Group.GID));
|
||||
ImGui.TextUnformatted($"{connectedMembers}/{totalMembers}");
|
||||
UiSharedService.AttachToolTip("Membres connectés / membres totaux" + Environment.NewLine +
|
||||
$"Capacité maximale : {maxCapacity}" + Environment.NewLine +
|
||||
"Syncshell ID: " + groupDto.Group.GID);
|
||||
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(groupName);
|
||||
if (textIsGid) ImGui.PopFont();
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Syncshell.NameTooltip",
|
||||
"Left click to switch between GID display and comment\nRight click to change comment for {0}\n\nUsers: {1}, Owner: {2}",
|
||||
groupName,
|
||||
pairsInGroup.Count + 1,
|
||||
groupDto.OwnerAliasOrUID));
|
||||
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
|
||||
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
|
||||
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
|
||||
if (groupDto.IsTemporary)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
UiSharedService.ColorText(L("GroupPanel.Syncshell.TempTag", "(Temp)"), ImGuiColors.DalamudOrange);
|
||||
UiSharedService.ColorText("(Temp)", ImGuiColors.DalamudOrange);
|
||||
if (groupDto.ExpiresAt != null)
|
||||
{
|
||||
var tempExpireLocal = groupDto.ExpiresAt.Value.ToLocalTime();
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Syncshell.TempExpires", "Expire le {0:g}", tempExpireLocal));
|
||||
UiSharedService.AttachToolTip($"Expire le {tempExpireLocal:g}");
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Syncshell.TempTooltip", "Syncshell temporaire"));
|
||||
UiSharedService.AttachToolTip("Syncshell temporaire");
|
||||
}
|
||||
}
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
@@ -428,7 +409,7 @@ internal sealed class GroupPanel
|
||||
{
|
||||
var buttonSizes = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X + _uiShared.GetIconButtonSize(FontAwesomeIcon.LockOpen).X;
|
||||
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2);
|
||||
if (ImGui.InputTextWithHint("", L("GroupPanel.CommentHint", "Comment/Notes"), ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
{
|
||||
_serverConfigurationManager.SetNoteForGid(groupDto.GID, _editGroupComment);
|
||||
_editGroupEntry = string.Empty;
|
||||
@@ -439,7 +420,7 @@ internal sealed class GroupPanel
|
||||
{
|
||||
_editGroupEntry = string.Empty;
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.CommentTooltip", "Hit ENTER to save\nRight click to cancel"));
|
||||
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
|
||||
}
|
||||
|
||||
|
||||
@@ -448,26 +429,26 @@ internal sealed class GroupPanel
|
||||
if (_showModalBanList && !_modalBanListOpened)
|
||||
{
|
||||
_modalBanListOpened = true;
|
||||
ImGui.OpenPopup(L("GroupPanel.Banlist.Title", "Manage Banlist for {0}", groupDto.GID));
|
||||
ImGui.OpenPopup("Manage Banlist for " + groupDto.GID);
|
||||
}
|
||||
|
||||
if (!_showModalBanList) _modalBanListOpened = false;
|
||||
|
||||
if (ImGui.BeginPopupModal(L("GroupPanel.Banlist.Title", "Manage Banlist for {0}", groupDto.GID), ref _showModalBanList, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Manage Banlist for " + groupDto.GID, ref _showModalBanList, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Retweet, L("GroupPanel.Banlist.Refresh", "Refresh Banlist from Server")))
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
|
||||
{
|
||||
_bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result;
|
||||
}
|
||||
|
||||
if (ImGui.BeginTable("bannedusertable" + groupDto.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY))
|
||||
{
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.Uid", "UID"), ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.Alias", "Alias"), ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.By", "By"), ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.Date", "Date"), ImGuiTableColumnFlags.None, 2);
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.Reason", "Reason"), ImGuiTableColumnFlags.None, 3);
|
||||
ImGui.TableSetupColumn(L("GroupPanel.Banlist.Column.Actions", "Actions"), ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1);
|
||||
ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2);
|
||||
ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3);
|
||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
@@ -484,7 +465,7 @@ internal sealed class GroupPanel
|
||||
ImGui.TableNextColumn();
|
||||
UiSharedService.TextWrapped(bannedUser.Reason);
|
||||
ImGui.TableNextColumn();
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Check, L("GroupPanel.Banlist.Unban", "Unban") + "#" + bannedUser.UID))
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Check, "Unban#" + bannedUser.UID))
|
||||
{
|
||||
_ = ApiController.GroupUnbanUser(bannedUser);
|
||||
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
||||
@@ -500,18 +481,18 @@ internal sealed class GroupPanel
|
||||
if (_showModalChangePassword && !_modalChangePwOpened)
|
||||
{
|
||||
_modalChangePwOpened = true;
|
||||
ImGui.OpenPopup(L("GroupPanel.Password.Title", "Change Syncshell Password"));
|
||||
ImGui.OpenPopup("Change Syncshell Password");
|
||||
}
|
||||
|
||||
if (!_showModalChangePassword) _modalChangePwOpened = false;
|
||||
|
||||
if (ImGui.BeginPopupModal(L("GroupPanel.Password.Title", "Change Syncshell Password"), ref _showModalChangePassword, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Change Syncshell Password", ref _showModalChangePassword, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Password.Description", "Enter the new Syncshell password for Syncshell {0} here.", name));
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Password.Warning", "This action is irreversible"));
|
||||
UiSharedService.TextWrapped("Enter the new Syncshell password for Syncshell " + name + " here.");
|
||||
UiSharedService.TextWrapped("This action is irreversible");
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##changepw", L("GroupPanel.Password.Hint", "New password for {0}", name), ref _newSyncShellPassword, 255);
|
||||
if (ImGui.Button(L("GroupPanel.Password.Button", "Change password")))
|
||||
ImGui.InputTextWithHint("##changepw", "New password for " + name, ref _newSyncShellPassword, 255);
|
||||
if (ImGui.Button("Change password"))
|
||||
{
|
||||
var pw = _newSyncShellPassword;
|
||||
_isPasswordValid = ApiController.GroupChangePassword(new(groupDto.Group, pw)).Result;
|
||||
@@ -521,7 +502,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (!_isPasswordValid)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("GroupPanel.Password.Error.TooShort", "The selected password is too short. It must be at least 10 characters."), new Vector4(1, 0, 0, 1));
|
||||
UiSharedService.ColorTextWrapped("The selected password is too short. It must be at least 10 characters.", new Vector4(1, 0, 0, 1));
|
||||
}
|
||||
|
||||
UiSharedService.SetScaledWindowSize(290);
|
||||
@@ -531,28 +512,29 @@ internal sealed class GroupPanel
|
||||
if (_showModalBulkOneTimeInvites && !_modalBulkOneTimeInvitesOpened)
|
||||
{
|
||||
_modalBulkOneTimeInvitesOpened = true;
|
||||
ImGui.OpenPopup(L("GroupPanel.Invites.Title", "Create Bulk One-Time Invites"));
|
||||
ImGui.OpenPopup("Create Bulk One-Time Invites");
|
||||
}
|
||||
|
||||
if (!_showModalBulkOneTimeInvites) _modalBulkOneTimeInvitesOpened = false;
|
||||
|
||||
if (ImGui.BeginPopupModal(L("GroupPanel.Invites.Title", "Create Bulk One-Time Invites"), ref _showModalBulkOneTimeInvites, UiSharedService.PopupWindowFlags))
|
||||
if (ImGui.BeginPopupModal("Create Bulk One-Time Invites", ref _showModalBulkOneTimeInvites, UiSharedService.PopupWindowFlags))
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Invites.Description", "This allows you to create up to 100 one-time invites at once for the Syncshell {0}.\nThe invites are valid for 24h after creation and will automatically expire.", name));
|
||||
UiSharedService.TextWrapped("This allows you to create up to 100 one-time invites at once for the Syncshell " + name + "." + Environment.NewLine
|
||||
+ "The invites are valid for 24h after creation and will automatically expire.");
|
||||
ImGui.Separator();
|
||||
if (_bulkOneTimeInvites.Count == 0)
|
||||
{
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.SliderInt(L("GroupPanel.Invites.AmountLabel", "Amount") + "##bulkinvites", ref _bulkInviteCount, 1, 100);
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.MailBulk, L("GroupPanel.Invites.CreateButton", "Create invites")))
|
||||
ImGui.SliderInt("Amount##bulkinvites", ref _bulkInviteCount, 1, 100);
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.MailBulk, "Create invites"))
|
||||
{
|
||||
_bulkOneTimeInvites = ApiController.GroupCreateTempInvite(groupDto, _bulkInviteCount).Result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Invites.Result", "A total of {0} invites have been created.", _bulkOneTimeInvites.Count));
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, L("GroupPanel.Invites.Copy", "Copy invites to clipboard")))
|
||||
UiSharedService.TextWrapped("A total of " + _bulkOneTimeInvites.Count + " invites have been created.");
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard"))
|
||||
{
|
||||
ImGui.SetClipboardText(string.Join(Environment.NewLine, _bulkOneTimeInvites));
|
||||
}
|
||||
@@ -587,7 +569,9 @@ internal sealed class GroupPanel
|
||||
).Value,
|
||||
_uidDisplayHandler,
|
||||
_uiShared,
|
||||
_charaDataManager);
|
||||
_charaDataManager,
|
||||
_autoDetectRequestService,
|
||||
_serverConfigurationManager);
|
||||
|
||||
if (pair.IsVisible)
|
||||
visibleUsers.Add(drawPair);
|
||||
@@ -599,14 +583,14 @@ internal sealed class GroupPanel
|
||||
|
||||
if (visibleUsers.Count > 0)
|
||||
{
|
||||
ImGui.TextUnformatted(L("GroupPanel.List.Visible", "Visible"));
|
||||
ImGui.TextUnformatted("Visible");
|
||||
ImGui.Separator();
|
||||
_uidDisplayHandler.RenderPairList(visibleUsers);
|
||||
}
|
||||
|
||||
if (onlineUsers.Count > 0)
|
||||
{
|
||||
ImGui.TextUnformatted(L("GroupPanel.List.Online", "Online"));
|
||||
ImGui.TextUnformatted("Online");
|
||||
ImGui.Separator();
|
||||
_uidDisplayHandler.RenderPairList(onlineUsers);
|
||||
}
|
||||
@@ -614,12 +598,12 @@ internal sealed class GroupPanel
|
||||
if (offlineUsers.Count > 0)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
ImGui.TextUnformatted(L("GroupPanel.List.Offline", "Offline/Unknown"));
|
||||
ImGui.TextUnformatted("Offline/Unknown");
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.Separator();
|
||||
if (hideOfflineUsers)
|
||||
{
|
||||
UiSharedService.ColorText(L("GroupPanel.List.OfflineOmitted", " {0} offline users omitted from display.", offlineUsers.Count), ImGuiColors.DalamudGrey);
|
||||
UiSharedService.ColorText($" {offlineUsers.Count} offline users omitted from display.", ImGuiColors.DalamudGrey);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -648,12 +632,12 @@ internal sealed class GroupPanel
|
||||
bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled;
|
||||
|
||||
var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock;
|
||||
var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running;
|
||||
var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
|
||||
var vfxIcon = vfxDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun;
|
||||
var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running;
|
||||
var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
|
||||
var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun;
|
||||
var animIcon = animDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||
var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||
var vfxIcon = vfxDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||
var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||
var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||
var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||
|
||||
var iconSize = UiSharedService.GetIconSize(infoIcon);
|
||||
var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||
@@ -674,11 +658,11 @@ internal sealed class GroupPanel
|
||||
ImGui.BeginTooltip();
|
||||
if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled)
|
||||
{
|
||||
ImGui.TextUnformatted(L("GroupPanel.Permissions.Header", "Syncshell permissions"));
|
||||
ImGui.TextUnformatted("Syncshell permissions");
|
||||
|
||||
if (!invitesEnabled)
|
||||
{
|
||||
var lockedText = L("GroupPanel.Permissions.InvitesDisabled", "Syncshell is closed for joining");
|
||||
var lockedText = "Syncshell is closed for joining";
|
||||
_uiShared.IconText(lockedIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(lockedText);
|
||||
@@ -686,7 +670,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (soundsDisabled)
|
||||
{
|
||||
var soundsText = L("GroupPanel.Permissions.SoundDisabledOwner", "Sound sync disabled through owner");
|
||||
var soundsText = "Sound sync disabled through owner";
|
||||
_uiShared.IconText(soundsIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(soundsText);
|
||||
@@ -694,7 +678,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (animDisabled)
|
||||
{
|
||||
var animText = L("GroupPanel.Permissions.AnimationDisabledOwner", "Animation sync disabled through owner");
|
||||
var animText = "Animation sync disabled through owner";
|
||||
_uiShared.IconText(animIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(animText);
|
||||
@@ -702,7 +686,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (vfxDisabled)
|
||||
{
|
||||
var vfxText = L("GroupPanel.Permissions.VfxDisabledOwner", "VFX sync disabled through owner");
|
||||
var vfxText = "VFX sync disabled through owner";
|
||||
_uiShared.IconText(vfxIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(vfxText);
|
||||
@@ -714,11 +698,11 @@ internal sealed class GroupPanel
|
||||
if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled)
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.TextUnformatted(L("GroupPanel.Permissions.OwnHeader", "Your permissions"));
|
||||
ImGui.TextUnformatted("Your permissions");
|
||||
|
||||
if (userSoundsDisabled)
|
||||
{
|
||||
var userSoundsText = L("GroupPanel.Permissions.SoundDisabledSelf", "Sound sync disabled through you");
|
||||
var userSoundsText = "Sound sync disabled through you";
|
||||
_uiShared.IconText(userSoundsIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userSoundsText);
|
||||
@@ -726,7 +710,7 @@ internal sealed class GroupPanel
|
||||
|
||||
if (userAnimDisabled)
|
||||
{
|
||||
var userAnimText = L("GroupPanel.Permissions.AnimationDisabledSelf", "Animation sync disabled through you");
|
||||
var userAnimText = "Animation sync disabled through you";
|
||||
_uiShared.IconText(userAnimIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userAnimText);
|
||||
@@ -734,14 +718,14 @@ internal sealed class GroupPanel
|
||||
|
||||
if (userVFXDisabled)
|
||||
{
|
||||
var userVFXText = L("GroupPanel.Permissions.VfxDisabledSelf", "VFX sync disabled through you");
|
||||
var userVFXText = "VFX sync disabled through you";
|
||||
_uiShared.IconText(userVFXIcon);
|
||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted(userVFXText);
|
||||
}
|
||||
|
||||
if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled)
|
||||
UiSharedService.TextWrapped(L("GroupPanel.Permissions.NotePriority", "Note that syncshell permissions for disabling take precedence over your own set permissions"));
|
||||
UiSharedService.TextWrapped("Note that syncshell permissions for disabling take precedence over your own set permissions");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
@@ -753,8 +737,7 @@ internal sealed class GroupPanel
|
||||
var userPerm = groupDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
||||
_ = ApiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupDto.Group, new UserData(ApiController.UID), userPerm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.PauseToggle.Tooltip", "{0} pairing with all users in this Syncshell",
|
||||
groupDto.GroupUserPermissions.IsPaused() ? L("GroupPanel.PauseToggle.Resume", "Resume") : L("GroupPanel.PauseToggle.Pause", "Pause")));
|
||||
UiSharedService.AttachToolTip((groupDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " pairing with all users in this Syncshell");
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_uiShared.IconButton(FontAwesomeIcon.Bars))
|
||||
@@ -764,69 +747,75 @@ internal sealed class GroupPanel
|
||||
|
||||
if (ImGui.BeginPopup("ShellPopup"))
|
||||
{
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, L("GroupPanel.Popup.Leave", "Leave Syncshell")) && UiSharedService.CtrlPressed())
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell") && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_ = ApiController.GroupLeave(groupDto);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.LeaveTooltip", "Hold CTRL and click to leave this Syncshell{0}",
|
||||
!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal)
|
||||
? string.Empty
|
||||
: Environment.NewLine + L("GroupPanel.Popup.LeaveWarning", "WARNING: This action is irreversible\nLeaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")));
|
||||
UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine
|
||||
+ "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell."));
|
||||
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, L("GroupPanel.Popup.CopyId", "Copy ID")))
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy ID"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
ImGui.SetClipboardText(groupDto.GroupAliasOrGID);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.CopyIdTooltip", "Copy Syncshell ID to Clipboard"));
|
||||
UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard");
|
||||
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, L("GroupPanel.Popup.CopyNotes", "Copy Notes")))
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
ImGui.SetClipboardText(UiSharedService.GetNotes(groupPairs));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.CopyNotesTooltip", "Copies all your notes for all users in this Syncshell to the clipboard.\nThey can be imported via Settings -> General -> Notes -> Import notes from clipboard"));
|
||||
UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> General -> Notes -> Import notes from clipboard");
|
||||
|
||||
var soundsText = userSoundsDisabled
|
||||
? L("GroupPanel.Popup.EnableSound", "Enable sound sync")
|
||||
: L("GroupPanel.Popup.DisableSound", "Disable sound sync");
|
||||
var soundsText = userSoundsDisabled ? "Enable sound sync" : "Disable sound sync";
|
||||
if (_uiShared.IconTextButton(userSoundsIcon, soundsText))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
var perm = groupDto.GroupUserPermissions;
|
||||
perm.SetDisableSounds(!perm.IsDisableSounds());
|
||||
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, perm.IsDisableSounds(), null, null));
|
||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.SoundTooltip", "Sets your allowance for sound synchronization for users of this syncshell.\nDisabling the synchronization will stop applying sound modifications for users of this syncshell.\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\nNote: this setting does not apply to individual pairs that are also in the syncshell."));
|
||||
UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell."
|
||||
+ Environment.NewLine + "Disabling the synchronization will stop applying sound modifications for users of this syncshell."
|
||||
+ Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner."
|
||||
+ Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell.");
|
||||
|
||||
var animText = userAnimDisabled
|
||||
? L("GroupPanel.Popup.EnableAnimations", "Enable animations sync")
|
||||
: L("GroupPanel.Popup.DisableAnimations", "Disable animations sync");
|
||||
var animText = userAnimDisabled ? "Enable animations sync" : "Disable animations sync";
|
||||
if (_uiShared.IconTextButton(userAnimIcon, animText))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
var perm = groupDto.GroupUserPermissions;
|
||||
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
||||
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, perm.IsDisableAnimations(), null));
|
||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.AnimTooltip", "Sets your allowance for animations synchronization for users of this syncshell.\nDisabling the synchronization will stop applying animations modifications for users of this syncshell.\nNote: this setting might also affect sound synchronization\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\nNote: this setting does not apply to individual pairs that are also in the syncshell."));
|
||||
UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell."
|
||||
+ Environment.NewLine + "Disabling the synchronization will stop applying animations modifications for users of this syncshell."
|
||||
+ Environment.NewLine + "Note: this setting might also affect sound synchronization"
|
||||
+ Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner."
|
||||
+ Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell.");
|
||||
|
||||
var vfxText = userVFXDisabled
|
||||
? L("GroupPanel.Popup.EnableVfx", "Enable VFX sync")
|
||||
: L("GroupPanel.Popup.DisableVfx", "Disable VFX sync");
|
||||
var vfxText = userVFXDisabled ? "Enable VFX sync" : "Disable VFX sync";
|
||||
if (_uiShared.IconTextButton(userVFXIcon, vfxText))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
var perm = groupDto.GroupUserPermissions;
|
||||
perm.SetDisableVFX(!perm.IsDisableVFX());
|
||||
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, null, perm.IsDisableVFX()));
|
||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.Popup.VfxTooltip", "Sets your allowance for VFX synchronization for users of this syncshell.\nDisabling the synchronization will stop applying VFX modifications for users of this syncshell.\nNote: this setting might also affect animation synchronization to some degree\nNote: this setting can be forcefully overridden to 'disabled' through the syncshell owner.\nNote: this setting does not apply to individual pairs that are also in the syncshell."));
|
||||
UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell."
|
||||
+ Environment.NewLine + "Disabling the synchronization will stop applying VFX modifications for users of this syncshell."
|
||||
+ Environment.NewLine + "Note: this setting might also affect animation synchronization to some degree"
|
||||
+ Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner."
|
||||
+ Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell.");
|
||||
|
||||
if (isOwner || groupDto.GroupUserInfo.IsModerator())
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Cog, L("GroupPanel.Popup.OpenAdmin", "Open Admin Panel")))
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
_mainUi.Mediator.Publish(new OpenSyncshellAdminPanel(groupDto));
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.Localization;
|
||||
|
||||
namespace MareSynchronos.UI.Components;
|
||||
|
||||
@@ -19,13 +17,6 @@ public class PairGroupsUi
|
||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
|
||||
private string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(System.Globalization.CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
|
||||
public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, ApiController apiController,
|
||||
SelectPairForGroupUi selectGroupForPairUi, UiSharedService uiSharedService)
|
||||
{
|
||||
@@ -74,21 +65,21 @@ public class PairGroupsUi
|
||||
}
|
||||
if (allArePaused)
|
||||
{
|
||||
UiSharedService.AttachToolTip(L("PairGroups.ResumeAll", "Resume pairing with all pairs in {0}", tag));
|
||||
UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}");
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AttachToolTip(L("PairGroups.PauseAll", "Pause pairing with all pairs in {0}", tag));
|
||||
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}");
|
||||
}
|
||||
|
||||
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
|
||||
ImGui.SameLine(buttonDeleteOffset);
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
||||
{
|
||||
ImGui.OpenPopup(L("PairGroups.Menu.Title", "Group Flyout Menu"));
|
||||
ImGui.OpenPopup("Group Flyout Menu");
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopup(L("PairGroups.Menu.Title", "Group Flyout Menu")))
|
||||
if (ImGui.BeginPopup("Group Flyout Menu"))
|
||||
{
|
||||
using (ImRaii.PushId($"buttons-{tag}")) DrawGroupMenu(tag);
|
||||
ImGui.EndPopup();
|
||||
@@ -141,35 +132,31 @@ public class PairGroupsUi
|
||||
|
||||
private void DrawGroupMenu(string tag)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, L("PairGroups.Menu.AddPeople", "Add people to {0}", tag)))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag))
|
||||
{
|
||||
_selectGroupForPairUi.Open(tag);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("PairGroups.Menu.AddPeople.Tooltip", "Add more users to Group {0}", tag));
|
||||
UiSharedService.AttachToolTip($"Add more users to Group {tag}");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, L("PairGroups.Menu.Delete", "Delete {0}", tag)) && UiSharedService.CtrlPressed())
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_tagHandler.RemoveTag(tag);
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("PairGroups.Menu.Delete.Tooltip", "Delete Group {0} (Will not delete the pairs)\nHold CTRL to delete", tag));
|
||||
UiSharedService.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete");
|
||||
}
|
||||
|
||||
private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total)
|
||||
{
|
||||
string displayedName = tag switch
|
||||
{
|
||||
TagHandler.CustomUnpairedTag => L("PairGroups.Tag.Unpaired", "Unpaired"),
|
||||
TagHandler.CustomOfflineTag => L("PairGroups.Tag.Offline", "Offline"),
|
||||
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately
|
||||
? L("PairGroups.Tag.Online", "Online")
|
||||
: L("PairGroups.Tag.Contacts", "Contacts"),
|
||||
TagHandler.CustomVisibleTag => L("PairGroups.Tag.Visible", "Visible"),
|
||||
TagHandler.CustomUnpairedTag => "Unpaired",
|
||||
TagHandler.CustomOfflineTag => "Offline",
|
||||
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online" : "Contacts",
|
||||
TagHandler.CustomVisibleTag => "Visible",
|
||||
_ => tag
|
||||
};
|
||||
|
||||
string resultFolderName = !isSpecialTag
|
||||
? L("PairGroups.Header.WithCounts", "{0} ({1}/{2}/{3} Pairs)", displayedName, visible, online, total ?? 0)
|
||||
: L("PairGroups.Header.Special", "{0} ({1} Pairs)", displayedName, online);
|
||||
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
||||
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
||||
_uiSharedService.IconText(icon);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
@@ -186,11 +173,11 @@ public class PairGroupsUi
|
||||
if (!isSpecialTag && ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted(L("PairGroups.Tooltip.Title", "Group {0}", tag));
|
||||
ImGui.TextUnformatted($"Group {tag}");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted(L("PairGroups.Tooltip.Visible", "{0} Pairs visible", visible));
|
||||
ImGui.TextUnformatted(L("PairGroups.Tooltip.Online", "{0} Pairs online/paused", online));
|
||||
ImGui.TextUnformatted(L("PairGroups.Tooltip.Total", "{0} Pairs total", total ?? 0));
|
||||
ImGui.TextUnformatted($"{visible} Pairs visible");
|
||||
ImGui.TextUnformatted($"{online} Pairs online/paused");
|
||||
ImGui.TextUnformatted($"{total} Pairs total");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,17 +28,17 @@ public class BanUserPopupHandler : IPopupHandler
|
||||
|
||||
public void DrawContent()
|
||||
{
|
||||
UiSharedService.TextWrapped(_uiSharedService.Localize("Popup.BanUser.Description", "User {0} will be banned and removed from this Syncshell.", _reportedPair.UserData.AliasOrUID));
|
||||
ImGui.InputTextWithHint("##banreason", _uiSharedService.Localize("Popup.BanUser.ReasonHint", "Ban Reason"), ref _banReason, 255);
|
||||
UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell.");
|
||||
ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, _uiSharedService.Localize("Popup.BanUser.Button", "Ban User")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
var reason = _banReason;
|
||||
_ = _apiController.GroupBanUser(new GroupPairDto(_group.Group, _reportedPair.UserData), reason);
|
||||
_banReason = string.Empty;
|
||||
}
|
||||
UiSharedService.TextWrapped(_uiSharedService.Localize("Popup.BanUser.ReasonNote", "The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."));
|
||||
UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason.");
|
||||
}
|
||||
|
||||
public void Open(OpenBanUserPopupMessage message)
|
||||
|
||||
@@ -72,7 +72,7 @@ public class PopupHandler : WindowMediatorSubscriberBase
|
||||
if (_currentHandler.ShowClose)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, _uiSharedService.Localize("Popup.Generic.Close", "Close")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
@@ -29,21 +29,23 @@ internal class ReportPopupHandler : IPopupHandler
|
||||
public void DrawContent()
|
||||
{
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
UiSharedService.TextWrapped(_uiSharedService.Localize("Popup.Report.Title", "Report {0} Profile", _reportedPair!.UserData.AliasOrUID));
|
||||
UiSharedService.TextWrapped("Report " + _reportedPair!.UserData.AliasOrUID + " Profile");
|
||||
|
||||
ImGui.InputTextMultiline("##reportReason", ref _reportReason, 500, new Vector2(500 - ImGui.GetStyle().ItemSpacing.X * 2, 200));
|
||||
UiSharedService.TextWrapped(_uiSharedService.Localize("Popup.Report.Note", "Note: Sending a report will disable the offending profile globally.\nThe report will be sent to the team of your currently connected server.\nDepending on the severity of the offense the users profile or account can be permanently disabled or banned."));
|
||||
UiSharedService.ColorTextWrapped(_uiSharedService.Localize("Popup.Report.Warning", "Report spam and wrong reports will not be tolerated and can lead to permanent account suspension."), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped(_uiSharedService.Localize("Popup.Report.Scope", "This is not for reporting misbehavior but solely for the actual profile. Reports that are not solely for the profile will be ignored."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" +
|
||||
$"The report will be sent to the team of your currently connected server.{Environment.NewLine}" +
|
||||
$"Depending on the severity of the offense the users profile or account can be permanently disabled or banned.");
|
||||
UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", UiSharedService.AccentColor);
|
||||
UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " +
|
||||
"Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow);
|
||||
|
||||
using (ImRaii.Disabled(string.IsNullOrEmpty(_reportReason)))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, _uiSharedService.Localize("Popup.Report.Button", "Send Report")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Send Report"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
var reason = _reportReason;
|
||||
_ = _apiController.UserReportProfile(new(_reportedPair.UserData, reason));
|
||||
_reportReason = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class SelectGroupForPairUi
|
||||
}
|
||||
|
||||
var name = PairName(_pair);
|
||||
var popupName = _uiSharedService.Localize("PairGroups.Popup.Title", "Choose Groups for {0}", name);
|
||||
var popupName = $"Choose Groups for {name}";
|
||||
if (_show)
|
||||
{
|
||||
ImGui.OpenPopup(popupName);
|
||||
@@ -49,7 +49,7 @@ public class SelectGroupForPairUi
|
||||
var childHeight = tags.Count != 0 ? tags.Count * 25 : 1;
|
||||
var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale;
|
||||
|
||||
ImGui.TextUnformatted(_uiSharedService.Localize("PairGroups.Popup.SelectPrompt", "Select the groups you want {0} to be in.", name));
|
||||
ImGui.TextUnformatted($"Select the groups you want {name} to be in.");
|
||||
if (ImGui.BeginChild(name + "##listGroups", childSize))
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
@@ -60,13 +60,13 @@ public class SelectGroupForPairUi
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted(_uiSharedService.Localize("PairGroups.Popup.CreatePrompt", "Create a new group for {0}.", name));
|
||||
ImGui.TextUnformatted($"Create a new group for {name}.");
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
||||
{
|
||||
HandleAddTag();
|
||||
}
|
||||
ImGui.SameLine();
|
||||
ImGui.InputTextWithHint("##category_name", _uiSharedService.Localize("PairGroups.Popup.NewGroupHint", "New Group"), ref _tagNameToAdd, 40);
|
||||
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40);
|
||||
if (ImGui.IsKeyDown(ImGuiKey.Enter))
|
||||
{
|
||||
HandleAddTag();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.UI;
|
||||
using MareSynchronos.UI.Handlers;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -16,13 +15,11 @@ public class SelectPairForGroupUi
|
||||
private HashSet<string> _peopleInGroup = new(StringComparer.Ordinal);
|
||||
private bool _show = false;
|
||||
private string _tag = string.Empty;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
|
||||
public SelectPairForGroupUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService)
|
||||
public SelectPairForGroupUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler)
|
||||
{
|
||||
_tagHandler = tagHandler;
|
||||
_uidDisplayHandler = uidDisplayHandler;
|
||||
_uiSharedService = uiSharedService;
|
||||
}
|
||||
|
||||
public void Draw(List<Pair> pairs)
|
||||
@@ -31,7 +28,7 @@ public class SelectPairForGroupUi
|
||||
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;
|
||||
var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale;
|
||||
|
||||
var popupName = _uiSharedService.Localize("PairGroups.SelectPairs.Title", "Choose Users for Group {0}", _tag);
|
||||
var popupName = $"Choose Users for Group {_tag}";
|
||||
|
||||
if (!_show)
|
||||
{
|
||||
@@ -49,9 +46,9 @@ public class SelectPairForGroupUi
|
||||
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
|
||||
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
|
||||
{
|
||||
ImGui.TextUnformatted(_uiSharedService.Localize("PairGroups.SelectPairs.SelectPrompt", "Select users for group {0}", _tag));
|
||||
ImGui.TextUnformatted($"Select users for group {_tag}");
|
||||
|
||||
ImGui.InputTextWithHint("##filter", _uiSharedService.Localize("PairGroups.SelectPairs.FilterHint", "Filter"), ref _filter, 255, ImGuiInputTextFlags.None);
|
||||
ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None);
|
||||
foreach (var item in pairs
|
||||
.Where(p => string.IsNullOrEmpty(_filter) || PairName(p).Contains(_filter, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(p => PairName(p), StringComparer.OrdinalIgnoreCase)
|
||||
|
||||
@@ -9,8 +9,6 @@ using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using MareSynchronos.Localization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
@@ -35,17 +33,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
private ObjectKind _selectedObjectTab;
|
||||
private bool _showModal = false;
|
||||
|
||||
private string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return _uiSharedService.Localize(key, fallback, safeArgs);
|
||||
}
|
||||
|
||||
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, MareMediator mediator,
|
||||
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
UiSharedService uiSharedService)
|
||||
: base(logger, mediator, uiSharedService.Localize("DataAnalysis.WindowTitle", "Character Data Analysis"), performanceCollectorService)
|
||||
: base(logger, mediator, "Character Data Analysis", performanceCollectorService)
|
||||
{
|
||||
_characterAnalyzer = characterAnalyzer;
|
||||
_ipcManager = ipcManager;
|
||||
@@ -73,15 +65,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
var modalTitle = L("DataAnalysis.Bc7.ModalTitle", "BC7 Conversion in Progress");
|
||||
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
||||
{
|
||||
_showModal = true;
|
||||
if (ImGui.BeginPopupModal(modalTitle))
|
||||
if (ImGui.BeginPopupModal("BC7 Conversion in Progress"))
|
||||
{
|
||||
ImGui.TextUnformatted(L("DataAnalysis.Bc7.Status", "BC7 Conversion in progress: {0}/{1}", _conversionCurrentFileProgress, _texturesToConvert.Count));
|
||||
UiSharedService.TextWrapped(L("DataAnalysis.Bc7.CurrentFile", "Current file: {0}", _conversionCurrentFileName));
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, L("DataAnalysis.Bc7.Cancel", "Cancel conversion")))
|
||||
ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count);
|
||||
UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
|
||||
{
|
||||
_conversionCancellationTokenSource.Cancel();
|
||||
}
|
||||
@@ -104,7 +95,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (_showModal && !_modalOpen)
|
||||
{
|
||||
ImGui.OpenPopup(modalTitle);
|
||||
ImGui.OpenPopup("BC7 Conversion in Progress");
|
||||
_modalOpen = true;
|
||||
}
|
||||
|
||||
@@ -115,7 +106,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_sortDirty = true;
|
||||
}
|
||||
|
||||
UiSharedService.TextWrapped(L("DataAnalysis.Description", "This window shows you all files and their sizes that are currently in use through your character and associated entities"));
|
||||
UiSharedService.TextWrapped("This window shows you all files and their sizes that are currently in use through your character and associated entities");
|
||||
|
||||
if (_cachedAnalysis == null || _cachedAnalysis.Count == 0) return;
|
||||
|
||||
@@ -123,9 +114,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
bool needAnalysis = _cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed));
|
||||
if (isAnalyzing)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("DataAnalysis.Analyzing", "Analyzing {0}/{1}", _characterAnalyzer.CurrentFile, _characterAnalyzer.TotalFiles),
|
||||
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
|
||||
ImGuiColors.DalamudYellow);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, L("DataAnalysis.Button.CancelAnalysis", "Cancel analysis")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
|
||||
{
|
||||
_characterAnalyzer.CancelAnalyze();
|
||||
}
|
||||
@@ -134,16 +125,16 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (needAnalysis)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("DataAnalysis.Analyze.MissingNotice", "Some entries in the analysis have file size not determined yet, press the button below to analyze your current data"),
|
||||
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
|
||||
ImGuiColors.DalamudYellow);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, L("DataAnalysis.Button.StartMissing", "Start analysis (missing entries)")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
|
||||
{
|
||||
_ = _characterAnalyzer.ComputeAnalysis(print: false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, L("DataAnalysis.Button.StartAll", "Start analysis (recalculate all entries)")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)"))
|
||||
{
|
||||
_ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true);
|
||||
}
|
||||
@@ -152,7 +143,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.TotalFiles", "Total files:"));
|
||||
ImGui.TextUnformatted("Total files:");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString());
|
||||
ImGui.SameLine();
|
||||
@@ -162,16 +153,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
string text = "";
|
||||
var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal);
|
||||
var summary = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
|
||||
.Select(f => L("DataAnalysis.Tooltip.FileSummary", "{0}: {1} files, size: {2}, compressed: {3}",
|
||||
f.Key, f.Count(), UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))));
|
||||
ImGui.SetTooltip(summary);
|
||||
text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
|
||||
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
|
||||
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
ImGui.TextUnformatted(L("DataAnalysis.TotalSizeActual", "Total size (actual):"));
|
||||
ImGui.TextUnformatted("Total size (actual):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
|
||||
ImGui.TextUnformatted(L("DataAnalysis.TotalSizeDownload", "Total size (download size):"));
|
||||
ImGui.TextUnformatted("Total size (download size):");
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis))
|
||||
{
|
||||
@@ -181,11 +173,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString());
|
||||
UiSharedService.AttachToolTip(L("DataAnalysis.Tooltip.CalculateDownloadSize", "Click \"Start analysis\" to calculate download size"));
|
||||
UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size");
|
||||
}
|
||||
}
|
||||
ImGui.TextUnformatted(L("DataAnalysis.TotalTriangles", "Total modded model triangles: {0}",
|
||||
UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))));
|
||||
ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}");
|
||||
ImGui.Separator();
|
||||
|
||||
using var tabbar = ImRaii.TabBar("objectSelection");
|
||||
@@ -199,7 +190,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
||||
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.FilesFor", "Files for {0}", kvp.Key));
|
||||
ImGui.TextUnformatted("Files for " + kvp.Key);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
||||
ImGui.SameLine();
|
||||
@@ -210,15 +201,16 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
var summary = string.Join(Environment.NewLine, groupedfiles
|
||||
.Select(f => L("DataAnalysis.Tooltip.FileSummary", "{0}: {1} files, size: {2}, compressed: {3}",
|
||||
f.Key, f.Count(), UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))));
|
||||
ImGui.SetTooltip(summary);
|
||||
string text = "";
|
||||
text = string.Join(Environment.NewLine, groupedfiles
|
||||
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
|
||||
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
ImGui.TextUnformatted(L("DataAnalysis.Object.SizeActual", "{0} size (actual):", kvp.Key));
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
|
||||
ImGui.TextUnformatted(L("DataAnalysis.Object.SizeDownload", "{0} size (download size):", kvp.Key));
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (download size):");
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis))
|
||||
{
|
||||
@@ -228,18 +220,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString());
|
||||
UiSharedService.AttachToolTip(L("DataAnalysis.Tooltip.CalculateDownloadSize", "Click \"Start analysis\" to calculate download size"));
|
||||
UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size");
|
||||
}
|
||||
}
|
||||
ImGui.TextUnformatted(L("DataAnalysis.Object.Vram", "{0} VRAM usage:", kvp.Key));
|
||||
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
|
||||
ImGui.SameLine();
|
||||
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||
if (vramUsage != null)
|
||||
{
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize)));
|
||||
}
|
||||
ImGui.TextUnformatted(L("DataAnalysis.Object.Triangles", "{0} modded model triangles: {1}", kvp.Key,
|
||||
UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))));
|
||||
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}");
|
||||
|
||||
ImGui.Separator();
|
||||
if (_selectedObjectTab != kvp.Key)
|
||||
@@ -275,35 +266,33 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_texturesToConvert.Clear();
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.FileGroup.Count", "{0} files", fileGroup.Key));
|
||||
ImGui.TextUnformatted($"{fileGroup.Key} files");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(fileGroup.Count().ToString());
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.FileGroup.SizeActual", "{0} files size (actual):", fileGroup.Key));
|
||||
ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize)));
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.FileGroup.SizeDownload", "{0} files size (download size):", fileGroup.Key));
|
||||
ImGui.TextUnformatted($"{fileGroup.Key} files size (download size):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize)));
|
||||
|
||||
if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.Checkbox(L("DataAnalysis.Bc7.EnableMode", "Enable BC7 Conversion Mode"), ref _enableBc7ConversionMode);
|
||||
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
|
||||
if (_enableBc7ConversionMode)
|
||||
{
|
||||
UiSharedService.ColorText(L("DataAnalysis.Bc7.WarningTitle", "WARNING BC7 CONVERSION:"), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow);
|
||||
ImGui.SameLine();
|
||||
UiSharedService.ColorText(L("DataAnalysis.Bc7.WarningIrreversible", "Converting textures to BC7 is irreversible!"), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped(L("DataAnalysis.Bc7.WarningDetails",
|
||||
"- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures.\n"
|
||||
+ "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts.\n"
|
||||
+ "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues.\n"
|
||||
+ "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically.\n"
|
||||
+ "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."),
|
||||
ImGuiColors.DalamudYellow);
|
||||
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle,
|
||||
L("DataAnalysis.Bc7.StartConversion", "Start conversion of {0} texture(s)", _texturesToConvert.Count)))
|
||||
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor);
|
||||
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
|
||||
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
|
||||
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
|
||||
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
|
||||
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
|
||||
, ImGuiColors.DalamudYellow);
|
||||
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
|
||||
{
|
||||
_conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
|
||||
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token);
|
||||
@@ -321,33 +310,33 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.TextUnformatted(L("DataAnalysis.SelectedFile", "Selected file:"));
|
||||
ImGui.TextUnformatted("Selected file:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow);
|
||||
|
||||
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
|
||||
{
|
||||
var filePaths = item.FilePaths;
|
||||
ImGui.TextUnformatted(L("DataAnalysis.LocalFilePath", "Local file path:"));
|
||||
ImGui.TextUnformatted("Local file path:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(filePaths[0]);
|
||||
if (filePaths.Count > 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(L("DataAnalysis.MoreCount", "(and {0} more)", filePaths.Count - 1));
|
||||
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
|
||||
}
|
||||
|
||||
var gamepaths = item.GamePaths;
|
||||
ImGui.TextUnformatted(L("DataAnalysis.GamePath", "Used by game path:"));
|
||||
ImGui.TextUnformatted("Used by game path:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(gamepaths[0]);
|
||||
if (gamepaths.Count > 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(L("DataAnalysis.MoreCount", "(and {0} more)", gamepaths.Count - 1));
|
||||
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
|
||||
@@ -383,19 +372,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
|
||||
new Vector2(0, 300));
|
||||
if (!table.Success) return;
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.Hash", "Hash"));
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.Filepaths", "Filepaths"), ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.Gamepaths", "Gamepaths"), ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.FileSize", "File Size"), ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.DownloadSize", "Download Size"), ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn("Hash");
|
||||
ImGui.TableSetupColumn("Filepaths", ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn("Gamepaths", ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn("File Size", ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn("Download Size", ImGuiTableColumnFlags.PreferSortDescending);
|
||||
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.Format", "Format"));
|
||||
if (_enableBc7ConversionMode) ImGui.TableSetupColumn(L("DataAnalysis.Table.ConvertToBc7", "Convert to BC7"));
|
||||
ImGui.TableSetupColumn("Format");
|
||||
if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7");
|
||||
}
|
||||
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.TableSetupColumn(L("DataAnalysis.Table.Triangles", "Triangles"), ImGuiTableColumnFlags.PreferSortDescending);
|
||||
ImGui.TableSetupColumn("Triangles", ImGuiTableColumnFlags.PreferSortDescending);
|
||||
}
|
||||
ImGui.TableSetupScrollFreeze(0, 1);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
|
||||
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, MareConfigService configService,
|
||||
FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, uiShared.Localize("DownloadUi.WindowTitle", "Umbra Downloads"), performanceCollectorService)
|
||||
: base(logger, mediator, "Umbra Downloads", performanceCollectorService)
|
||||
{
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_configService = configService;
|
||||
@@ -69,8 +69,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiShared.Localize(key, fallback, args);
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
if (_configService.Current.ShowTransferWindow)
|
||||
@@ -89,7 +87,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.SameLine();
|
||||
var xDistance = ImGui.GetCursorPosX();
|
||||
UiSharedService.DrawOutlinedFont(L("DownloadUi.UploadStatus", "Compressing+Uploading {0}/{1}", doneUploads, totalUploads),
|
||||
UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(xDistance);
|
||||
@@ -122,7 +120,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
var xDistance = ImGui.GetCursorPosX();
|
||||
UiSharedService.DrawOutlinedFont(
|
||||
L("DownloadUi.DownloadStatus", "{0} [W:{1}/Q:{2}/P:{3}/D:{4}]", item.Key.Name, dlSlot, dlQueue, dlProg, dlDecomp),
|
||||
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(xDistance);
|
||||
@@ -193,7 +191,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
using var _ = _uiShared.UidFont.Push();
|
||||
var uploadText = L("DownloadUi.UploadingLabel", "Uploading");
|
||||
var uploadText = "Uploading";
|
||||
|
||||
var textSize = ImGui.CalcTextSize(uploadText);
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ using Dalamud.Interface;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Configurations;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using MareSynchronos.Localization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
@@ -43,19 +42,13 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
private readonly ILogger<DtrEntry> _logger;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly NearbyPendingService _nearbyPendingService;
|
||||
private Task? _runTask;
|
||||
private string? _text;
|
||||
private string? _tooltip;
|
||||
private Colors _colors;
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
|
||||
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController)
|
||||
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController, NearbyPendingService nearbyPendingService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dtrBar = dtrBar;
|
||||
@@ -64,6 +57,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
_mareMediator = mareMediator;
|
||||
_pairManager = pairManager;
|
||||
_apiController = apiController;
|
||||
_nearbyPendingService = nearbyPendingService;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -115,7 +109,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
private IDtrBarEntry CreateEntry()
|
||||
{
|
||||
_logger.LogTrace("Creating new DtrBar entry");
|
||||
var entry = _dtrBar.Get(L("DtrEntry.EntryName", "Umbra"));
|
||||
var entry = _dtrBar.Get("Umbra");
|
||||
entry.OnClick = _ => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
||||
|
||||
return entry;
|
||||
@@ -156,8 +150,9 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
if (_apiController.IsConnected)
|
||||
{
|
||||
var pairCount = _pairManager.GetVisibleUserCount();
|
||||
|
||||
text = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString());
|
||||
var baseText = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString());
|
||||
var pendingNearby = _nearbyPendingService.Pending.Count;
|
||||
text = pendingNearby > 0 ? $"{baseText} ({pendingNearby})" : baseText;
|
||||
if (pairCount > 0)
|
||||
{
|
||||
IEnumerable<string> visiblePairs;
|
||||
@@ -174,20 +169,28 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName));
|
||||
}
|
||||
|
||||
var header = L("DtrEntry.Tooltip.Connected", "Umbra: Connected");
|
||||
tooltip = header + Environment.NewLine + "----------" + Environment.NewLine + string.Join(Environment.NewLine, visiblePairs);
|
||||
tooltip = $"Umbra: Connected";
|
||||
if (pendingNearby > 0)
|
||||
{
|
||||
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
|
||||
}
|
||||
tooltip += $"{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
||||
colors = _configService.Current.DtrColorsPairsInRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
tooltip = L("DtrEntry.Tooltip.Connected", "Umbra: Connected");
|
||||
tooltip = "Umbra: Connected";
|
||||
if (pendingNearby > 0)
|
||||
{
|
||||
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
|
||||
}
|
||||
colors = _configService.Current.DtrColorsDefault;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text = RenderDtrStyle(_configService.Current.DtrStyle, "\uE04C");
|
||||
tooltip = L("DtrEntry.Tooltip.Disconnected", "Umbra: Not Connected");
|
||||
tooltip = "Umbra: Not Connected";
|
||||
colors = _configService.Current.DtrColorsNotConnected;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
@@ -35,7 +36,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, uiSharedService.Localize("EditProfile.WindowTitle", "Umbra Edit Profile") + "###UmbraSyncEditProfileUI", performanceCollectorService)
|
||||
: base(logger, mediator, "Umbra Edit Profile###UmbraSyncEditProfileUI", performanceCollectorService)
|
||||
{
|
||||
IsOpen = false;
|
||||
this.SizeConstraints = new()
|
||||
@@ -62,17 +63,16 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiSharedService.Localize(key, fallback, args);
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
_uiSharedService.BigText(L("EditProfile.CurrentProfile", "Current Profile (as saved on server)"));
|
||||
_uiSharedService.BigText("Current Profile (as saved on server)");
|
||||
ImGuiHelpers.ScaledDummy(new Vector2(0f, ImGui.GetStyle().ItemSpacing.Y / 2));
|
||||
|
||||
var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID));
|
||||
|
||||
if (profile.IsFlagged)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped(profile.Description, UiSharedService.AccentColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,13 +89,12 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
_descriptionText = _profileDescription;
|
||||
}
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
if (_pfpTextureWrap != null)
|
||||
{
|
||||
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||
}
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||
}
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 256f);
|
||||
@@ -127,9 +126,9 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
ImGui.Separator();
|
||||
_uiSharedService.BigText("Profile Settings");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, L("EditProfile.Button.UploadPicture", "Upload new profile picture")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||
{
|
||||
_fileDialogManager.OpenFileDialog(L("EditProfile.Dialog.PictureTitle", "Select new Profile picture"), ".png", (success, file) =>
|
||||
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||
{
|
||||
if (!success) return;
|
||||
_ = Task.Run(async () =>
|
||||
@@ -150,29 +149,29 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
});
|
||||
});
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("EditProfile.Tooltip.UploadPicture", "Select and upload a new profile picture"));
|
||||
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, L("EditProfile.Button.ClearPicture", "Clear uploaded profile picture")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("EditProfile.Tooltip.ClearPicture", "Clear your currently uploaded profile picture"));
|
||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||
if (_showFileDialogError)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("EditProfile.Error.PictureTooLarge", "The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size"), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", UiSharedService.AccentColor);
|
||||
}
|
||||
var isNsfw = profile.IsNSFW;
|
||||
if (ImGui.Checkbox(L("EditProfile.Checkbox.Nsfw", "Profile is NSFW"), ref isNsfw))
|
||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
||||
}
|
||||
_uiSharedService.DrawHelpText(L("EditProfile.Help.Nsfw", "If your profile description or image can be considered NSFW, toggle this to ON"));
|
||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||
var widthTextBox = 400;
|
||||
var posX = ImGui.GetCursorPosX();
|
||||
ImGui.TextUnformatted(L("EditProfile.DescriptionCounter", "Description {0}/1500", _descriptionText.Length));
|
||||
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||
ImGui.SetCursorPosX(posX);
|
||||
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||
ImGui.TextUnformatted(L("EditProfile.PreviewLabel", "Preview (approximate)"));
|
||||
ImGui.TextUnformatted("Preview (approximate)");
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||
|
||||
@@ -201,17 +200,17 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, L("EditProfile.Button.SaveDescription", "Save Description")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("EditProfile.Tooltip.SaveDescription", "Sets your profile description text"));
|
||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, L("EditProfile.Button.ClearDescription", "Clear Description")))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("EditProfile.Tooltip.ClearDescription", "Clears your profile description text"));
|
||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -40,7 +40,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
public EventViewerUI(ILogger<EventViewerUI> logger, MareMediator mediator,
|
||||
EventAggregator eventAggregator, UiSharedService uiSharedService, MareConfigService configService,
|
||||
PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, uiSharedService.Localize("EventViewer.WindowTitle", "Event Viewer"), performanceCollectorService)
|
||||
: base(logger, mediator, "Event Viewer", performanceCollectorService)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_uiSharedService = uiSharedService;
|
||||
@@ -52,8 +52,6 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
_filteredEvents = RecreateFilter();
|
||||
}
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiSharedService.Localize(key, fallback, args);
|
||||
|
||||
private Lazy<List<Event>> RecreateFilter()
|
||||
{
|
||||
return new(() =>
|
||||
@@ -83,22 +81,20 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
var newEventsAvailable = _eventAggregator.NewEventsAvailable;
|
||||
|
||||
var unfreezeLabel = L("EventViewer.Button.Unfreeze", "Unfreeze View");
|
||||
var freezeLabel = L("EventViewer.Button.Freeze", "Freeze View");
|
||||
var freezeSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.PlayCircle, unfreezeLabel);
|
||||
var freezeSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.PlayCircle, "Unfreeze View");
|
||||
if (_isPaused)
|
||||
{
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, newEventsAvailable))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, unfreezeLabel))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Unfreeze View"))
|
||||
_isPaused = false;
|
||||
if (newEventsAvailable)
|
||||
UiSharedService.AttachToolTip(L("EventViewer.Tooltip.NewEvents", "New events are available. Click to resume updating."));
|
||||
UiSharedService.AttachToolTip("New events are available. Click to resume updating.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PauseCircle, freezeLabel))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PauseCircle, "Freeze View"))
|
||||
_isPaused = true;
|
||||
}
|
||||
|
||||
@@ -109,7 +105,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
|
||||
bool changedFilter = false;
|
||||
ImGui.SetNextItemWidth(200);
|
||||
changedFilter |= ImGui.InputText(L("EventViewer.FilterLabel", "Filter lines"), ref _filterFreeText, 50);
|
||||
changedFilter |= ImGui.InputText("Filter lines", ref _filterFreeText, 50);
|
||||
if (changedFilter) _filteredEvents = RecreateFilter();
|
||||
|
||||
using (ImRaii.Disabled(_filterFreeText.IsNullOrEmpty()))
|
||||
@@ -124,11 +120,10 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
|
||||
if (_configService.Current.LogEvents)
|
||||
{
|
||||
var openLogLabel = L("EventViewer.Button.OpenLog", "Open EventLog folder");
|
||||
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.FolderOpen, openLogLabel);
|
||||
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.FolderOpen, "Open EventLog Folder");
|
||||
var dist = ImGui.GetWindowContentRegionMax().X - buttonSize;
|
||||
ImGui.SameLine(dist);
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, openLogLabel))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Open EventLog folder"))
|
||||
{
|
||||
ProcessStartInfo ps = new()
|
||||
{
|
||||
@@ -157,11 +152,11 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
ImGui.TableSetupScrollFreeze(0, 1);
|
||||
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.NoSort);
|
||||
ImGui.TableSetupColumn(L("EventViewer.Column.Time", "Time"), ImGuiTableColumnFlags.None, timeColWidth);
|
||||
ImGui.TableSetupColumn(L("EventViewer.Column.Source", "Source"), ImGuiTableColumnFlags.None, sourceColWidth);
|
||||
ImGui.TableSetupColumn(L("EventViewer.Column.Uid", "UID"), ImGuiTableColumnFlags.None, uidColWidth);
|
||||
ImGui.TableSetupColumn(L("EventViewer.Column.Character", "Character"), ImGuiTableColumnFlags.None, characterColWidth);
|
||||
ImGui.TableSetupColumn(L("EventViewer.Column.Event", "Event"), ImGuiTableColumnFlags.None);
|
||||
ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.None, timeColWidth);
|
||||
ImGui.TableSetupColumn("Source", ImGuiTableColumnFlags.None, sourceColWidth);
|
||||
ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, uidColWidth);
|
||||
ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.None, characterColWidth);
|
||||
ImGui.TableSetupColumn("Event", ImGuiTableColumnFlags.None);
|
||||
ImGui.TableHeadersRow();
|
||||
int i = 0;
|
||||
foreach (var ev in _filteredEvents.Value)
|
||||
@@ -180,13 +175,13 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
EventSeverity.Informational => new Vector4(),
|
||||
EventSeverity.Warning => ImGuiColors.DalamudYellow,
|
||||
EventSeverity.Error => ImGuiColors.DalamudRed,
|
||||
EventSeverity.Error => UiSharedService.AccentColor,
|
||||
_ => new Vector4()
|
||||
};
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_uiSharedService.IconText(icon, iconColor == new Vector4() ? null : iconColor);
|
||||
UiSharedService.AttachToolTip(L($"EventViewer.Severity.{ev.EventSeverity}", ev.EventSeverity.ToString()));
|
||||
UiSharedService.AttachToolTip(ev.EventSeverity.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(ev.EventTime.ToString("T", CultureInfo.CurrentCulture));
|
||||
@@ -205,7 +200,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(L("EventViewer.NoValue", "--"));
|
||||
ImGui.TextUnformatted("--");
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
@@ -219,7 +214,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(L("EventViewer.NoValue", "--"));
|
||||
ImGui.TextUnformatted("--");
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.UI.Components;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MareSynchronos.UI.Handlers;
|
||||
|
||||
@@ -24,13 +22,6 @@ public class UidDisplayHandler
|
||||
private bool _popupShown = false;
|
||||
private DateTime? _popupTime;
|
||||
|
||||
private static string L(string key, string fallback, params object[] args)
|
||||
{
|
||||
var safeArgs = args ?? Array.Empty<object>();
|
||||
return LocalizationService.Instance?.GetString(key, fallback, safeArgs)
|
||||
?? string.Format(CultureInfo.CurrentCulture, fallback, safeArgs);
|
||||
}
|
||||
|
||||
public UidDisplayHandler(MareMediator mediator, PairManager pairManager,
|
||||
ServerConfigurationManager serverManager, MareConfigService mareConfigService)
|
||||
{
|
||||
@@ -86,7 +77,21 @@ public class UidDisplayHandler
|
||||
|
||||
if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow)
|
||||
{
|
||||
ImGui.SetTooltip(L("UidDisplay.Tooltip", "Left click to switch between UID display and nick\nRight click to change nick for {0}\nMiddle Mouse Button to open their profile in a separate window", pair.UserData.AliasOrUID));
|
||||
// Build tooltip; prepend last-seen when player is offline or not visible
|
||||
string tooltip = "Left click to switch between UID display and nick" + Environment.NewLine
|
||||
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
|
||||
+ "Middle Mouse Button to open their profile in a separate window";
|
||||
|
||||
if (!pair.IsOnline || !pair.IsVisible)
|
||||
{
|
||||
var lastSeen = _serverManager.GetNameForUid(pair.UserData.UID);
|
||||
if (!string.IsNullOrEmpty(lastSeen))
|
||||
{
|
||||
tooltip = "Vu sous : " + lastSeen + Environment.NewLine + tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetTooltip(tooltip);
|
||||
}
|
||||
else if (_popupTime < DateTime.UtcNow && !_popupShown)
|
||||
{
|
||||
@@ -132,7 +137,7 @@ public class UidDisplayHandler
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
|
||||
ImGui.SetNextItemWidth(editBoxWidth.Invoke());
|
||||
if (ImGui.InputTextWithHint("##" + pair.UserData.UID, L("UidDisplay.EditNotes.Hint", "Nick/Notes"), ref _editUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
{
|
||||
_serverManager.SetNoteForUid(pair.UserData.UID, _editUserComment);
|
||||
_serverManager.SaveNotes();
|
||||
@@ -143,16 +148,21 @@ public class UidDisplayHandler
|
||||
{
|
||||
_editNickEntry = string.Empty;
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("GroupPanel.CommentTooltip", "Hit ENTER to save\nRight click to cancel"));
|
||||
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
|
||||
}
|
||||
}
|
||||
|
||||
public (bool isUid, string text) GetPlayerText(Pair pair)
|
||||
{
|
||||
var textIsUid = true;
|
||||
bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
|
||||
if (showUidInsteadOfName)
|
||||
{
|
||||
return (true, pair.UserData.UID);
|
||||
}
|
||||
|
||||
var textIsUid = true;
|
||||
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
|
||||
if (!showUidInsteadOfName && playerText != null)
|
||||
if (playerText != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerText))
|
||||
{
|
||||
@@ -168,7 +178,7 @@ public class UidDisplayHandler
|
||||
playerText = pair.UserData.AliasOrUID;
|
||||
}
|
||||
|
||||
if (_mareConfigService.Current.ShowCharacterNames && textIsUid && !showUidInsteadOfName)
|
||||
if (_mareConfigService.Current.ShowCharacterNames && textIsUid && pair.IsOnline && pair.IsVisible)
|
||||
{
|
||||
var name = pair.PlayerName;
|
||||
if (name != null)
|
||||
@@ -204,8 +214,11 @@ public class UidDisplayHandler
|
||||
|
||||
private bool ShowUidInsteadOfName(Pair pair)
|
||||
{
|
||||
_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName);
|
||||
|
||||
if (_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName))
|
||||
{
|
||||
return showUidInsteadOfName;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using MareSynchronos.API.Dto.Account;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services;
|
||||
@@ -37,8 +36,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
private string? _registrationMessage;
|
||||
private RegisterReplyDto? _registrationReply;
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiShared.Localize(key, fallback, args);
|
||||
|
||||
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService,
|
||||
CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator,
|
||||
PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mareMediator, "Umbra Setup", performanceCollectorService)
|
||||
@@ -72,17 +69,17 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
return _uiShared.ApiController.ServerState switch
|
||||
{
|
||||
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
||||
ServerState.Reconnecting => UiSharedService.AccentColor,
|
||||
ServerState.Connected => ImGuiColors.HealerGreen,
|
||||
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
||||
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
||||
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
|
||||
ServerState.Offline => ImGuiColors.DalamudRed,
|
||||
ServerState.Unauthorized => UiSharedService.AccentColor,
|
||||
ServerState.VersionMisMatch => UiSharedService.AccentColor,
|
||||
ServerState.Offline => UiSharedService.AccentColor,
|
||||
ServerState.RateLimited => ImGuiColors.DalamudYellow,
|
||||
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
|
||||
ServerState.MultiChara => ImGuiColors.DalamudYellow,
|
||||
_ => ImGuiColors.DalamudRed
|
||||
_ => UiSharedService.AccentColor
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,17 +87,17 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
return _uiShared.ApiController.ServerState switch
|
||||
{
|
||||
ServerState.Reconnecting => L("Compact.UidText.Reconnecting", "Reconnecting"),
|
||||
ServerState.Connecting => L("Compact.UidText.Connecting", "Connecting"),
|
||||
ServerState.Disconnected => L("Compact.UidText.Disconnected", "Disconnected"),
|
||||
ServerState.Disconnecting => L("Compact.UidText.Disconnecting", "Disconnecting"),
|
||||
ServerState.Unauthorized => L("Compact.UidText.Unauthorized", "Unauthorized"),
|
||||
ServerState.VersionMisMatch => L("Compact.UidText.VersionMismatch", "Version mismatch"),
|
||||
ServerState.Offline => L("Compact.UidText.Offline", "Unavailable"),
|
||||
ServerState.RateLimited => L("Compact.UidText.RateLimited", "Rate Limited"),
|
||||
ServerState.NoSecretKey => L("Compact.UidText.NoSecretKey", "No Secret Key"),
|
||||
ServerState.MultiChara => L("Compact.UidText.MultiChara", "Duplicate Characters"),
|
||||
ServerState.Connected => L("Intro.ConnectionStatus.Connected", "Connected"),
|
||||
ServerState.Reconnecting => "Reconnecting",
|
||||
ServerState.Connecting => "Connecting",
|
||||
ServerState.Disconnected => "Disconnected",
|
||||
ServerState.Disconnecting => "Disconnecting",
|
||||
ServerState.Unauthorized => "Unauthorized",
|
||||
ServerState.VersionMisMatch => "Version mismatch",
|
||||
ServerState.Offline => "Unavailable",
|
||||
ServerState.RateLimited => "Rate Limited",
|
||||
ServerState.NoSecretKey => "No Secret Key",
|
||||
ServerState.MultiChara => "Duplicate Characters",
|
||||
ServerState.Connected => "Connected",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
@@ -111,15 +108,18 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (!_configService.Current.AcceptedAgreement && !_readFirstPage)
|
||||
{
|
||||
_uiShared.BigText(L("Intro.Welcome.Title", "Welcome to Umbra"));
|
||||
_uiShared.BigText("Welcome to Umbra");
|
||||
ImGui.Separator();
|
||||
UiSharedService.TextWrapped(L("Intro.Welcome.Paragraph1", "Umbra is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."));
|
||||
UiSharedService.TextWrapped(L("Intro.Welcome.Paragraph2", "We will have to setup a few things first before you can start using this plugin. Click on next to continue."));
|
||||
UiSharedService.TextWrapped("Umbra is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. " +
|
||||
"Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.");
|
||||
UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue.");
|
||||
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Welcome.Note", "Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients might look broken because of this or others players mods might not apply on your end altogether. If you want to use this plugin you will have to move your mods to Penumbra."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " +
|
||||
"might look broken because of this or others players mods might not apply on your end altogether. " +
|
||||
"If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow);
|
||||
if (!_uiShared.DrawOtherPluginState(intro: true)) return;
|
||||
ImGui.Separator();
|
||||
if (ImGui.Button(L("Intro.Welcome.Next", "Next") + "##toAgreement"))
|
||||
if (ImGui.Button("Next##toAgreement"))
|
||||
{
|
||||
_readFirstPage = true;
|
||||
#if !DEBUG
|
||||
@@ -127,7 +127,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
for (int i = 10; i > 0; i--)
|
||||
{
|
||||
_timeoutLabel = L("Intro.Agreement.Timeout", "'I agree' button will be available in {0}s", i);
|
||||
_timeoutLabel = $"'I agree' button will be available in {i}s";
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
@@ -140,40 +140,52 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
using (_uiShared.UidFont.Push())
|
||||
{
|
||||
ImGui.TextUnformatted(L("Intro.Agreement.Title", "Agreement of Usage of Service"));
|
||||
ImGui.TextUnformatted("Conditions d'utilisation");
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.SetWindowFontScale(1.5f);
|
||||
string readThis = L("Intro.Agreement.Callout", "READ THIS CAREFULLY");
|
||||
string readThis = "MERCI DE LIRE ATTENTIVEMENT";
|
||||
Vector2 textSize = ImGui.CalcTextSize(readThis);
|
||||
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
||||
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorText(readThis, UiSharedService.AccentColor);
|
||||
ImGui.SetWindowFontScale(1.0f);
|
||||
ImGui.Separator();
|
||||
var agreementParagraphs = new[]
|
||||
{
|
||||
L("Intro.Agreement.Paragraph1", "To use Umbra, you must be over the age of 18, or 21 in some jurisdictions."),
|
||||
L("Intro.Agreement.Paragraph2", "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod."),
|
||||
L("Intro.Agreement.Paragraph3", "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again."),
|
||||
L("Intro.Agreement.Paragraph4", "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."),
|
||||
L("Intro.Agreement.Paragraph5", "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."),
|
||||
L("Intro.Agreement.Paragraph6", "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted."),
|
||||
L("Intro.Agreement.Paragraph7", "Accounts that are inactive for ninety (90) days will be deleted for privacy reasons."),
|
||||
L("Intro.Agreement.Paragraph8", "Umbra is operated from servers located in the European Union. You agree not to upload any content to the service that violates EU law; and more specifically, German law."),
|
||||
L("Intro.Agreement.Paragraph9", "You may delete your account at any time from within the Settings panel of the plugin. Any mods unique to you will then be removed from the server within 14 days."),
|
||||
L("Intro.Agreement.Paragraph10", "This service is provided as-is."),
|
||||
};
|
||||
|
||||
foreach (var paragraph in agreementParagraphs)
|
||||
{
|
||||
UiSharedService.TextWrapped(paragraph);
|
||||
}
|
||||
UiSharedService.TextWrapped("""
|
||||
Pour utiliser les services UmbraSync, vous devez être âgé de plus de 18 ans, où plus de 21 ans dans certaines juridictions.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Tout les mods actuellement actifs sur votre personnage et ses états associés seront automatiquement téléchargés vers le serveur UmbraSync auquel vous vous êtes inscrit.Il sera téléchargé exclusivement les fichiers nécessaires à la synchronisation et non l'intégralité du mod.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Si vous disposez d'une connexion Internet limitée, des frais supplémentaires peuvent s'appliquer en fonction du nombre de fichiers envoyés et reçus. Les fichiers seront compressés afin d'économiser la bande passante. En raison des variations de vitesse de débit, les sychronisations peuvent ne pas être visible immédiatement.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Les fichiers téléchargés sont confidentiels et ne seront pas distribués à des solutions tierces où autres personnes. Uniquement les personnes avec qui vous êtes appairés demandent exactement les mêmes fichiers. Réfléchissez donc bien avec qui vous allez vous appairer.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Le gentil dev' a fait de son mieux pour assurer votre sécurité. Cependant le risque 0 n'existe pas. Ne vous appairez pas aveuglément avec n'importe qui.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Après une periode d'inactivité, les mods enregistrés sur le serveur UmbraSync seront automatiquement supprimés.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Les comptes inactifs pendant 90 jours seront supprimés pour des raisons de stockage et de confidentialité.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
L'infrastructure Umbrasync est hebergé dans l'Union Européenne (Allemagne) et en Suisse. Vous acceptez alors de ne pas télécharger de contenu qui pourrait aller à l'encontre des législations de ces deux pays.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Vous pouvez supprimer votre compte à tout moment. Votre compte et toutes les données associées seront supprimés dans un délai de 14 jours.
|
||||
""");
|
||||
UiSharedService.TextWrapped("""
|
||||
Ce service est fourni tel quel.
|
||||
""");
|
||||
|
||||
ImGui.Separator();
|
||||
if (_timeoutTask?.IsCompleted ?? true)
|
||||
{
|
||||
if (ImGui.Button(L("Intro.Agreement.Accept", "I agree") + "##toSetup"))
|
||||
if (ImGui.Button("I agree##toSetup"))
|
||||
{
|
||||
_configService.Current.AcceptedAgreement = true;
|
||||
_configService.Save();
|
||||
@@ -190,26 +202,29 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
|| !Directory.Exists(_configService.Current.CacheFolder)))
|
||||
{
|
||||
using (_uiShared.UidFont.Push())
|
||||
ImGui.TextUnformatted(L("Intro.Storage.Title", "File Storage Setup"));
|
||||
ImGui.TextUnformatted("File Storage Setup");
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (!_uiShared.HasValidPenumbraModPath)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Storage.NoPenumbra", "You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory."), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", UiSharedService.AccentColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.TextWrapped(L("Intro.Storage.Description", "To not unnecessarily download files already present on your computer, Umbra will have to scan your Penumbra mod directory. Additionally, a local storage folder must be set where Umbra will download other character files to. Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."));
|
||||
UiSharedService.TextWrapped(L("Intro.Storage.ScanNote", "Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."));
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Storage.Warning.FileCache", "Warning: once past this step you should not delete the FileCache.csv of Umbra in the Plugin Configurations folder of Dalamud. Otherwise on the next launch a full re-scan of the file cache database will be initiated."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Storage.Warning.ScanHang", "Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Umbra will have to scan your Penumbra mod directory. " +
|
||||
"Additionally, a local storage folder must be set where Umbra will download other character files to. " +
|
||||
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
|
||||
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
|
||||
UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Umbra in the Plugin Configurations folder of Dalamud. " +
|
||||
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
|
||||
_uiShared.DrawCacheDirectorySetting();
|
||||
}
|
||||
|
||||
if (!_cacheMonitor.IsScanRunning && !string.IsNullOrEmpty(_configService.Current.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_configService.Current.CacheFolder))
|
||||
{
|
||||
if (ImGui.Button(L("Intro.Storage.StartScan", "Start Scan") + "##startScan"))
|
||||
if (ImGui.Button("Start Scan##startScan"))
|
||||
{
|
||||
_cacheMonitor.InvokeScan();
|
||||
}
|
||||
@@ -221,21 +236,22 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
if (!_dalamudUtilService.IsWine)
|
||||
{
|
||||
var useFileCompactor = _configService.Current.UseCompactor;
|
||||
if (ImGui.Checkbox(L("Intro.Storage.UseCompactor", "Use File Compactor"), ref useFileCompactor))
|
||||
if (ImGui.Checkbox("Use File Compactor", ref useFileCompactor))
|
||||
{
|
||||
_configService.Current.UseCompactor = useFileCompactor;
|
||||
_configService.Save();
|
||||
}
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Storage.CompactorDescription", "The File Compactor can save a tremendous amount of space on the hard disk for downloads through Umbra. It will incur a minor CPU penalty on download but can speed up loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Umbra settings."), ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through Umbra. It will incur a minor CPU penalty on download but can speed up " +
|
||||
"loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Umbra settings.", ImGuiColors.DalamudYellow);
|
||||
}
|
||||
}
|
||||
else if (!_uiShared.ApiController.IsConnected)
|
||||
{
|
||||
using (_uiShared.UidFont.Push())
|
||||
ImGui.TextUnformatted(L("Intro.Registration.Title", "Service Registration"));
|
||||
ImGui.TextUnformatted("Service Registration");
|
||||
ImGui.Separator();
|
||||
UiSharedService.TextWrapped(L("Intro.Registration.Description", "To be able to use Umbra you will have to register an account."));
|
||||
UiSharedService.TextWrapped(L("Intro.Registration.Support", "Refer to the instructions at the location you obtained this plugin for more information or support."));
|
||||
UiSharedService.TextWrapped("To be able to use Umbra you will have to register an account.");
|
||||
UiSharedService.TextWrapped("Refer to the instructions at the location you obtained this plugin for more information or support.");
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
@@ -246,8 +262,8 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted(L("Intro.Registration.NewAccountInfo", "If you have not used Umbra before, click below to register a new account."));
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, L("Intro.Registration.RegisterButton", "Register a new Umbra account")))
|
||||
ImGui.TextUnformatted("If you have not used Umbra before, click below to register a new account.");
|
||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Umbra account"))
|
||||
{
|
||||
_registrationInProgress = true;
|
||||
_ = Task.Run(async () => {
|
||||
@@ -257,12 +273,12 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
if (!reply.Success)
|
||||
{
|
||||
_logger.LogWarning("Registration failed: {err}", reply.ErrorMessage);
|
||||
_registrationMessage = string.IsNullOrEmpty(reply.ErrorMessage)
|
||||
? L("Intro.Registration.UnknownError", "An unknown error occured. Please try again later.")
|
||||
: reply.ErrorMessage;
|
||||
_registrationMessage = reply.ErrorMessage;
|
||||
if (_registrationMessage.IsNullOrEmpty())
|
||||
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||
return;
|
||||
}
|
||||
_registrationMessage = L("Intro.Registration.Success", "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC.");
|
||||
_registrationMessage = "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC.";
|
||||
_secretKey = reply.SecretKey ?? "";
|
||||
_registrationReply = reply;
|
||||
_registrationSuccess = true;
|
||||
@@ -271,7 +287,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
_logger.LogWarning(ex, "Registration failed");
|
||||
_registrationSuccess = false;
|
||||
_registrationMessage = L("Intro.Registration.UnknownError", "An unknown error occured. Please try again later.");
|
||||
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -282,7 +298,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
|
||||
if (_registrationInProgress)
|
||||
{
|
||||
ImGui.TextUnformatted(L("Intro.Registration.SendingRequest", "Sending request..."));
|
||||
ImGui.TextUnformatted("Sending request...");
|
||||
}
|
||||
else if (!_registrationMessage.IsNullOrEmpty())
|
||||
{
|
||||
@@ -295,40 +311,42 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
var secretKeyLabel = _registrationSuccess
|
||||
? L("Intro.Registration.SecretKeyLabelRegistered", "Secret Key")
|
||||
: L("Intro.Registration.SecretKeyLabel", "Enter Secret Key");
|
||||
var text = "Enter Secret Key";
|
||||
|
||||
if (!_registrationSuccess)
|
||||
if (_registrationSuccess)
|
||||
{
|
||||
ImGui.TextUnformatted(L("Intro.Registration.SecretKeyInstructions", "If you already have a registered account, you can enter its secret key below to use it instead."));
|
||||
text = "Secret Key";
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("If you already have a registered account, you can enter its secret key below to use it instead.");
|
||||
}
|
||||
|
||||
var textSize = ImGui.CalcTextSize(secretKeyLabel);
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(secretKeyLabel);
|
||||
ImGui.TextUnformatted(text);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - textSize.X);
|
||||
ImGui.InputText("", ref _secretKey, 64);
|
||||
if (_secretKey.Length > 0 && _secretKey.Length != 64)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Registration.SecretKeyLength", "Your secret key must be exactly 64 characters long."), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(L("Intro.Registration.SecretKeyCharacters", "Your secret key can only contain ABCDEF and the numbers 0-9."), ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (_secretKey.Length == 64)
|
||||
{
|
||||
using var saveDisabled = ImRaii.Disabled(_uiShared.ApiController.ServerState == ServerState.Connecting || _uiShared.ApiController.ServerState == ServerState.Reconnecting);
|
||||
if (ImGui.Button(L("Intro.Registration.SaveAndConnect", "Save and Connect")))
|
||||
if (ImGui.Button("Save and Connect"))
|
||||
{
|
||||
string keyName;
|
||||
if (_serverConfigurationManager.CurrentServer == null) _serverConfigurationManager.SelectServer(0);
|
||||
if (_registrationReply != null && _secretKey.Equals(_registrationReply.SecretKey, StringComparison.Ordinal))
|
||||
keyName = _registrationReply.UID + " " + L("Intro.Registration.SavedKeyRegistered", "(registered {0})", DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
keyName = _registrationReply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})";
|
||||
else
|
||||
keyName = L("Intro.Registration.SavedKeySetup", "Secret Key added on Setup ({0})", DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
keyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})";
|
||||
_serverConfigurationManager.CurrentServer!.SecretKeys.Add(_serverConfigurationManager.CurrentServer.SecretKeys.Select(k => k.Key).LastOrDefault() + 1, new SecretKey()
|
||||
{
|
||||
FriendlyName = keyName,
|
||||
|
||||
@@ -22,9 +22,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
||||
|
||||
public PermissionWindowUI(ILogger<PermissionWindowUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService,
|
||||
ApiController apiController, PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator,
|
||||
uiSharedService.Localize("PermissionWindow.Title", "Permissions for {0}", pair.UserData.AliasOrUID) + "###UmbraSyncPermissions" + pair.UserData.UID,
|
||||
performanceCollectorService)
|
||||
: base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###UmbraSyncPermissions" + pair.UserData.UID, performanceCollectorService)
|
||||
{
|
||||
Pair = pair;
|
||||
_uiSharedService = uiSharedService;
|
||||
@@ -39,8 +37,6 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
||||
IsOpen = true;
|
||||
}
|
||||
|
||||
private string L(string key, string fallback, params object[] args) => _uiSharedService.Localize(key, fallback, args);
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
var paused = _ownPermissions.IsPaused();
|
||||
@@ -50,19 +46,18 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
||||
var style = ImGui.GetStyle();
|
||||
var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X;
|
||||
|
||||
_uiSharedService.BigText(L("PermissionWindow.Title", "Permissions for {0}", Pair.UserData.AliasOrUID));
|
||||
_uiSharedService.BigText("Permissions for " + Pair.UserData.AliasOrUID);
|
||||
ImGuiHelpers.ScaledDummy(1f);
|
||||
|
||||
if (Pair.UserPair == null)
|
||||
return;
|
||||
|
||||
if (ImGui.Checkbox(L("PermissionWindow.Pause.Label", "Pause Sync"), ref paused))
|
||||
if (ImGui.Checkbox("Pause Sync", ref paused))
|
||||
{
|
||||
_ownPermissions.SetPaused(paused);
|
||||
}
|
||||
_uiSharedService.DrawHelpText(L("PermissionWindow.Pause.HelpMain", "Pausing will completely cease any sync with this user.")
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("PermissionWindow.Pause.HelpNote", "Note: this is bidirectional, either user pausing will cease sync completely."));
|
||||
_uiSharedService.DrawHelpText("Pausing will completely cease any sync with this user." + UiSharedService.TooltipSeparator
|
||||
+ "Note: this is bidirectional, either user pausing will cease sync completely.");
|
||||
var otherPerms = Pair.UserPair.OtherPermissions;
|
||||
|
||||
var otherIsPaused = otherPerms.IsPaused();
|
||||
@@ -75,68 +70,53 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
||||
_uiSharedService.BooleanToColoredIcon(!otherIsPaused, false);
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
var pausedText = otherIsPaused
|
||||
? L("PermissionWindow.OtherPaused.True", "{0} has paused you", Pair.UserData.AliasOrUID)
|
||||
: L("PermissionWindow.OtherPaused.False", "{0} has not paused you", Pair.UserData.AliasOrUID);
|
||||
ImGui.TextUnformatted(pausedText);
|
||||
ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherIsPaused ? "not " : string.Empty) + "paused you");
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
ImGui.Separator();
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
|
||||
if (ImGui.Checkbox(L("PermissionWindow.Sounds.Label", "Disable Sounds"), ref disableSounds))
|
||||
if (ImGui.Checkbox("Disable Sounds", ref disableSounds))
|
||||
{
|
||||
_ownPermissions.SetDisableSounds(disableSounds);
|
||||
}
|
||||
_uiSharedService.DrawHelpText(L("PermissionWindow.Sounds.HelpMain", "Disabling sounds will remove all sounds synced with this user on both sides.")
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("PermissionWindow.Sounds.HelpNote", "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides."));
|
||||
_uiSharedService.DrawHelpText("Disabling sounds will remove all sounds synced with this user on both sides." + UiSharedService.TooltipSeparator
|
||||
+ "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides.");
|
||||
using (ImRaii.PushIndent(indentSize, false))
|
||||
{
|
||||
_uiSharedService.BooleanToColoredIcon(!otherDisableSounds, false);
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
var soundText = otherDisableSounds
|
||||
? L("PermissionWindow.OtherSoundDisabled.True", "{0} has disabled sound sync with you", Pair.UserData.AliasOrUID)
|
||||
: L("PermissionWindow.OtherSoundDisabled.False", "{0} has not disabled sound sync with you", Pair.UserData.AliasOrUID);
|
||||
ImGui.TextUnformatted(soundText);
|
||||
ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableSounds ? "not " : string.Empty) + "disabled sound sync with you");
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox(L("PermissionWindow.Animations.Label", "Disable Animations"), ref disableAnimations))
|
||||
if (ImGui.Checkbox("Disable Animations", ref disableAnimations))
|
||||
{
|
||||
_ownPermissions.SetDisableAnimations(disableAnimations);
|
||||
}
|
||||
_uiSharedService.DrawHelpText(L("PermissionWindow.Animations.HelpMain", "Disabling sounds will remove all animations synced with this user on both sides.")
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("PermissionWindow.Animations.HelpNote", "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides."));
|
||||
_uiSharedService.DrawHelpText("Disabling sounds will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator
|
||||
+ "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides.");
|
||||
using (ImRaii.PushIndent(indentSize, false))
|
||||
{
|
||||
_uiSharedService.BooleanToColoredIcon(!otherDisableAnimations, false);
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
var animText = otherDisableAnimations
|
||||
? L("PermissionWindow.OtherAnimationDisabled.True", "{0} has disabled animation sync with you", Pair.UserData.AliasOrUID)
|
||||
: L("PermissionWindow.OtherAnimationDisabled.False", "{0} has not disabled animation sync with you", Pair.UserData.AliasOrUID);
|
||||
ImGui.TextUnformatted(animText);
|
||||
ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableAnimations ? "not " : string.Empty) + "disabled animation sync with you");
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox(L("PermissionWindow.Vfx.Label", "Disable VFX"), ref disableVfx))
|
||||
if (ImGui.Checkbox("Disable VFX", ref disableVfx))
|
||||
{
|
||||
_ownPermissions.SetDisableVFX(disableVfx);
|
||||
}
|
||||
_uiSharedService.DrawHelpText(L("PermissionWindow.Vfx.HelpMain", "Disabling sounds will remove all VFX synced with this user on both sides.")
|
||||
+ UiSharedService.TooltipSeparator
|
||||
+ L("PermissionWindow.Vfx.HelpNote", "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides."));
|
||||
_uiSharedService.DrawHelpText("Disabling sounds will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator
|
||||
+ "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides.");
|
||||
using (ImRaii.PushIndent(indentSize, false))
|
||||
{
|
||||
_uiSharedService.BooleanToColoredIcon(!otherDisableVFX, false);
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
var vfxText = otherDisableVFX
|
||||
? L("PermissionWindow.OtherVfxDisabled.True", "{0} has disabled VFX sync with you", Pair.UserData.AliasOrUID)
|
||||
: L("PermissionWindow.OtherVfxDisabled.False", "{0} has not disabled VFX sync with you", Pair.UserData.AliasOrUID);
|
||||
ImGui.TextUnformatted(vfxText);
|
||||
ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you");
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
@@ -146,35 +126,44 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
||||
bool hasChanges = _ownPermissions != Pair.UserPair.OwnPermissions;
|
||||
|
||||
using (ImRaii.Disabled(!hasChanges))
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, L("PermissionWindow.Button.Save", "Save")))
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save"))
|
||||
{
|
||||
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
|
||||
_ownPermissions.IsDisableSounds(),
|
||||
_ownPermissions.IsDisableAnimations(),
|
||||
_ownPermissions.IsDisableVFX()));
|
||||
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("PermissionWindow.Tooltip.Save", "Save and apply all changes"));
|
||||
UiSharedService.AttachToolTip("Save and apply all changes");
|
||||
|
||||
var rightSideButtons = _uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.Undo, L("PermissionWindow.Button.Revert", "Revert")) +
|
||||
_uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, L("PermissionWindow.Button.Reset", "Reset to Default"));
|
||||
var rightSideButtons = _uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert") +
|
||||
_uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default");
|
||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
|
||||
ImGui.SameLine(availableWidth - rightSideButtons);
|
||||
|
||||
using (ImRaii.Disabled(!hasChanges))
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Undo, L("PermissionWindow.Button.Revert", "Revert")))
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert"))
|
||||
{
|
||||
_ownPermissions = Pair.UserPair.OwnPermissions.DeepClone();
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("PermissionWindow.Tooltip.Revert", "Revert all changes"));
|
||||
UiSharedService.AttachToolTip("Revert all changes");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, L("PermissionWindow.Button.Reset", "Reset to Default")))
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
|
||||
{
|
||||
var defaults = _uiSharedService.ConfigService.Current;
|
||||
_ownPermissions.SetPaused(false);
|
||||
_ownPermissions.SetDisableVFX(false);
|
||||
_ownPermissions.SetDisableSounds(false);
|
||||
_ownPermissions.SetDisableAnimations(false);
|
||||
_ownPermissions.SetDisableSounds(defaults.DefaultDisableSounds);
|
||||
_ownPermissions.SetDisableAnimations(defaults.DefaultDisableAnimations);
|
||||
_ownPermissions.SetDisableVFX(defaults.DefaultDisableVfx);
|
||||
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
|
||||
_ownPermissions.IsDisableSounds(),
|
||||
_ownPermissions.IsDisableAnimations(),
|
||||
_ownPermissions.IsDisableVFX()));
|
||||
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
||||
}
|
||||
UiSharedService.AttachToolTip(L("PermissionWindow.Tooltip.Reset", "This will set all permissions to their default setting"));
|
||||
UiSharedService.AttachToolTip("This will set all permissions to their default setting");
|
||||
|
||||
var ySize = ImGui.GetCursorPosY() + style.FramePadding.Y * ImGuiHelpers.GlobalScale + style.FrameBorderSize;
|
||||
ImGui.SetWindowSize(new(400, ySize));
|
||||
|
||||
@@ -113,7 +113,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
||||
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
||||
}
|
||||
string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline");
|
||||
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
|
||||
if (_pair.IsVisible)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,7 +111,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
||||
}
|
||||
string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
||||
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
|
||||
if (Pair.IsVisible)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
@@ -374,7 +374,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
ImGui.TextUnformatted("Sound Sync");
|
||||
_uiSharedService.BooleanToColoredIcon(!isDisableSounds);
|
||||
ImGui.SameLine(230);
|
||||
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute,
|
||||
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp,
|
||||
isDisableSounds ? "Enable sound sync" : "Disable sound sync"))
|
||||
{
|
||||
perm.SetDisableSounds(!perm.IsDisableSounds());
|
||||
@@ -385,7 +385,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
ImGui.TextUnformatted("Animation Sync");
|
||||
_uiSharedService.BooleanToColoredIcon(!isDisableAnimations);
|
||||
ImGui.SameLine(230);
|
||||
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop,
|
||||
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running,
|
||||
isDisableAnimations ? "Enable animation sync" : "Disable animation sync"))
|
||||
{
|
||||
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
||||
@@ -396,7 +396,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
ImGui.TextUnformatted("VFX Sync");
|
||||
_uiSharedService.BooleanToColoredIcon(!isDisableVfx);
|
||||
ImGui.SameLine(230);
|
||||
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle,
|
||||
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun,
|
||||
isDisableVfx ? "Enable VFX sync" : "Disable VFX sync"))
|
||||
{
|
||||
perm.SetDisableVFX(!perm.IsDisableVFX());
|
||||
|
||||
533
MareSynchronos/UI/TypingIndicatorOverlay.cs
Normal file
533
MareSynchronos/UI/TypingIndicatorOverlay.cs
Normal file
@@ -0,0 +1,533 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Linq;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace MareSynchronos.UI;
|
||||
|
||||
public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
{
|
||||
private const int NameplateIconId = 61397;
|
||||
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||
private static readonly TimeSpan TypingDisplayDelay = TimeSpan.FromMilliseconds(500);
|
||||
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
|
||||
|
||||
private readonly ILogger<TypingIndicatorOverlay> _logger;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly ITextureProvider _textureProvider;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly IPartyList _partyList;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly TypingIndicatorStateService _typingStateService;
|
||||
private readonly ApiController _apiController;
|
||||
|
||||
public TypingIndicatorOverlay(ILogger<TypingIndicatorOverlay> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
||||
MareConfigService configService, IGameGui gameGui, ITextureProvider textureProvider, IClientState clientState,
|
||||
IPartyList partyList, IObjectTable objectTable, DalamudUtilService dalamudUtil, PairManager pairManager,
|
||||
TypingIndicatorStateService typingStateService, ApiController apiController)
|
||||
: base(logger, mediator, nameof(TypingIndicatorOverlay), performanceCollectorService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_gameGui = gameGui;
|
||||
_textureProvider = textureProvider;
|
||||
_clientState = clientState;
|
||||
_partyList = partyList;
|
||||
_objectTable = objectTable;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_typingStateService = typingStateService;
|
||||
_apiController = apiController;
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
IsOpen = true;
|
||||
Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing
|
||||
| ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav;
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||
ImGui.SetWindowPos(viewport.Pos);
|
||||
ImGui.SetWindowSize(viewport.Size);
|
||||
|
||||
if (!_clientState.IsLoggedIn)
|
||||
return;
|
||||
|
||||
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||
|
||||
if (!showParty && !showNameplates)
|
||||
return;
|
||||
|
||||
var overlayDrawList = ImGui.GetWindowDrawList();
|
||||
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||
var hasSelf = _typingStateService.TryGetSelfTyping(TypingDisplayTime, out var selfStart, out var selfLast);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (showParty)
|
||||
{
|
||||
DrawPartyIndicators(overlayDrawList, activeTypers, hasSelf, now, selfStart, selfLast);
|
||||
}
|
||||
|
||||
if (showNameplates)
|
||||
{
|
||||
DrawNameplateIndicators(ImGui.GetWindowDrawList(), activeTypers, hasSelf, now, selfStart, selfLast);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||
{
|
||||
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
|
||||
if (partyAddon == null || !partyAddon->IsVisible)
|
||||
return;
|
||||
|
||||
if (selfActive
|
||||
&& (now - selfStart) >= TypingDisplayDelay
|
||||
&& (now - selfLast) <= TypingDisplayFade)
|
||||
{
|
||||
DrawPartyMemberTyping(drawList, partyAddon, 0);
|
||||
}
|
||||
|
||||
foreach (var (uid, entry) in activeTypers)
|
||||
{
|
||||
if ((now - entry.LastUpdate) > TypingDisplayFade)
|
||||
continue;
|
||||
|
||||
var pair = _pairManager.GetPairByUID(uid);
|
||||
var targetIndex = -1;
|
||||
var playerName = pair?.PlayerName;
|
||||
var objectId = pair?.PlayerCharacterId ?? uint.MaxValue;
|
||||
|
||||
if (objectId != 0 && objectId != uint.MaxValue)
|
||||
{
|
||||
targetIndex = GetPartyIndexForObjectId(objectId);
|
||||
if (targetIndex >= 0 && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
var member = _partyList[targetIndex];
|
||||
var memberName = member?.Name?.TextValue;
|
||||
if (!string.IsNullOrEmpty(memberName) && !memberName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var nameIndex = GetPartyIndexForName(playerName);
|
||||
targetIndex = nameIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex < 0 && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
targetIndex = GetPartyIndexForName(playerName);
|
||||
}
|
||||
|
||||
if (targetIndex < 0)
|
||||
continue;
|
||||
|
||||
DrawPartyMemberTyping(drawList, partyAddon, targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawPartyMemberTyping(ImDrawListPtr drawList, AtkUnitBase* partyList, int memberIndex)
|
||||
{
|
||||
if (memberIndex < 0 || memberIndex > 7) return;
|
||||
|
||||
var nodeIndex = 23 - memberIndex;
|
||||
if (partyList->UldManager.NodeListCount <= nodeIndex) return;
|
||||
|
||||
var memberNode = (AtkComponentNode*)partyList->UldManager.NodeList[nodeIndex];
|
||||
if (memberNode == null || !memberNode->AtkResNode.IsVisible()) return;
|
||||
|
||||
var iconNode = memberNode->Component->UldManager.NodeListCount > 4 ? memberNode->Component->UldManager.NodeList[4] : null;
|
||||
if (iconNode == null) return;
|
||||
|
||||
var align = partyList->UldManager.NodeList[3]->Y;
|
||||
var partyScale = partyList->Scale;
|
||||
|
||||
var iconOffset = new Vector2(-14, 8) * partyScale;
|
||||
var iconSize = new Vector2(iconNode->Width / 2f, iconNode->Height / 2f) * partyScale;
|
||||
|
||||
var iconPos = new Vector2(
|
||||
partyList->X + (memberNode->AtkResNode.X * partyScale) + (iconNode->X * partyScale) + (iconNode->Width * partyScale / 2f),
|
||||
partyList->Y + align + (memberNode->AtkResNode.Y * partyScale) + (iconNode->Y * partyScale) + (iconNode->Height * partyScale / 2f));
|
||||
|
||||
iconPos += iconOffset;
|
||||
|
||||
var texture = _textureProvider.GetFromGame("ui/uld/charamake_dataimport.tex").GetWrapOrEmpty();
|
||||
if (texture == null) return;
|
||||
|
||||
drawList.AddImage(texture.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
|
||||
}
|
||||
|
||||
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||
{
|
||||
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
|
||||
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
|
||||
return;
|
||||
|
||||
if (selfActive
|
||||
&& _clientState.LocalPlayer != null
|
||||
&& (now - selfStart) >= TypingDisplayDelay
|
||||
&& (now - selfLast) <= TypingDisplayFade)
|
||||
{
|
||||
var selfId = GetEntityId(_clientState.LocalPlayer.Address);
|
||||
if (selfId != 0 && !TryDrawNameplateBubble(drawList, iconWrap, selfId))
|
||||
{
|
||||
DrawWorldFallbackIcon(drawList, iconWrap, _clientState.LocalPlayer.Position);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (uid, entry) in activeTypers)
|
||||
{
|
||||
if ((now - entry.LastUpdate) > TypingDisplayFade)
|
||||
continue;
|
||||
|
||||
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var pair = _pairManager.GetPairByUID(uid);
|
||||
var objectId = pair?.PlayerCharacterId ?? 0;
|
||||
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
|
||||
var pairIdent = pair?.Ident ?? string.Empty;
|
||||
var isPartyMember = IsPartyMember(objectId, pairName);
|
||||
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
|
||||
|
||||
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId})", uid, objectId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasWorldPosition = TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos);
|
||||
var isNearby = hasWorldPosition && IsWithinRelevantDistance(worldPos);
|
||||
|
||||
if (!isRelevantMember && !isNearby)
|
||||
continue;
|
||||
|
||||
if (pair == null)
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
|
||||
}
|
||||
|
||||
_logger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
|
||||
uid, objectId, pairName, pairIdent);
|
||||
|
||||
if (hasWorldPosition)
|
||||
{
|
||||
DrawWorldFallbackIcon(drawList, iconWrap, worldPos);
|
||||
_logger.LogTrace("TypingIndicator: fallback world draw for {uid} at {pos}", uid, worldPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: could not resolve position for {uid}", uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 GetConfiguredBubbleSize(float scaleX, float scaleY, bool isNameplateVisible, TypingIndicatorBubbleSize? overrideSize = null)
|
||||
{
|
||||
var sizeSetting = overrideSize ?? _configService.Current.TypingIndicatorBubbleSize;
|
||||
var baseSize = sizeSetting switch
|
||||
{
|
||||
TypingIndicatorBubbleSize.Small when isNameplateVisible => 32f,
|
||||
TypingIndicatorBubbleSize.Medium when isNameplateVisible => 44f,
|
||||
TypingIndicatorBubbleSize.Large when isNameplateVisible => 56f,
|
||||
TypingIndicatorBubbleSize.Small => 15f,
|
||||
TypingIndicatorBubbleSize.Medium => 25f,
|
||||
TypingIndicatorBubbleSize.Large => 35f,
|
||||
_ => 35f,
|
||||
};
|
||||
|
||||
return new Vector2(baseSize * scaleX, baseSize * scaleY);
|
||||
}
|
||||
|
||||
private unsafe bool TryDrawNameplateBubble(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, uint objectId)
|
||||
{
|
||||
if (textureWrap == null || textureWrap.Handle == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
return false;
|
||||
|
||||
var ui3D = framework->GetUIModule()->GetUI3DModule();
|
||||
if (ui3D == null)
|
||||
return false;
|
||||
|
||||
var addonNamePlate = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
|
||||
if (addonNamePlate == null)
|
||||
return false;
|
||||
|
||||
AddonNamePlate.NamePlateObject* namePlate = null;
|
||||
float distance = 0f;
|
||||
|
||||
for (var i = 0; i < ui3D->NamePlateObjectInfoCount; i++)
|
||||
{
|
||||
var objectInfo = ui3D->NamePlateObjectInfoPointers[i];
|
||||
if (objectInfo.Value == null || objectInfo.Value->GameObject == null)
|
||||
continue;
|
||||
|
||||
if (objectInfo.Value->GameObject->EntityId != objectId)
|
||||
continue;
|
||||
|
||||
if (objectInfo.Value->GameObject->YalmDistanceFromPlayerX > 35f)
|
||||
return false;
|
||||
|
||||
namePlate = &addonNamePlate->NamePlateObjectArray[objectInfo.Value->NamePlateIndex];
|
||||
distance = objectInfo.Value->GameObject->YalmDistanceFromPlayerX;
|
||||
break;
|
||||
}
|
||||
|
||||
if (namePlate == null || namePlate->RootComponentNode == null)
|
||||
return false;
|
||||
|
||||
var iconNode = namePlate->RootComponentNode->Component->UldManager.NodeList[0];
|
||||
if (iconNode == null)
|
||||
return false;
|
||||
|
||||
var scaleX = namePlate->RootComponentNode->AtkResNode.ScaleX;
|
||||
var scaleY = namePlate->RootComponentNode->AtkResNode.ScaleY;
|
||||
var iconVisible = iconNode->IsVisible();
|
||||
var sizeScaleFactor = 1f;
|
||||
var scaleVector = new Vector2(scaleX, scaleY);
|
||||
var rootPosition = new Vector2(namePlate->RootComponentNode->AtkResNode.X, namePlate->RootComponentNode->AtkResNode.Y);
|
||||
var iconLocalPosition = new Vector2(iconNode->X, iconNode->Y) * scaleVector;
|
||||
var iconDimensions = new Vector2(iconNode->Width, iconNode->Height) * scaleVector;
|
||||
|
||||
if (!iconVisible)
|
||||
{
|
||||
sizeScaleFactor = 2.5f;
|
||||
var anchor = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X * 0.5f, 0f);
|
||||
|
||||
var distanceOffset = new Vector2(0f, -16f + distance) * scaleVector;
|
||||
if (iconNode->Height == 24)
|
||||
{
|
||||
distanceOffset.Y += 16f * scaleY;
|
||||
}
|
||||
distanceOffset.Y += 64f * scaleY;
|
||||
|
||||
var referenceSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false, TypingIndicatorBubbleSize.Small);
|
||||
var manualOffset = new Vector2(referenceSize.X * 2.00f, referenceSize.Y * 2.00f);
|
||||
|
||||
var iconSizeHidden = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false);
|
||||
var center = anchor + distanceOffset + manualOffset;
|
||||
var topLeft = center - (iconSizeHidden / 2f);
|
||||
|
||||
drawList.AddImage(textureWrap.Handle, topLeft, topLeft + iconSizeHidden, Vector2.Zero, Vector2.One,
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var iconPos = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X, 0f);
|
||||
|
||||
var iconOffset = new Vector2(distance / 1.5f, distance / 3.5f) * scaleVector;
|
||||
if (iconNode->Height == 24)
|
||||
{
|
||||
iconOffset.Y -= 8f * scaleY;
|
||||
}
|
||||
|
||||
iconPos += iconOffset;
|
||||
|
||||
var iconSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, true);
|
||||
|
||||
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrawWorldFallbackIcon(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, Vector3 worldPosition)
|
||||
{
|
||||
var offsetPosition = worldPosition + new Vector3(0f, 1.8f, 0f);
|
||||
if (!_gameGui.WorldToScreen(offsetPosition, out var screenPos))
|
||||
return;
|
||||
|
||||
var iconSize = GetConfiguredBubbleSize(ImGuiHelpers.GlobalScale, ImGuiHelpers.GlobalScale, false);
|
||||
var iconPos = screenPos - (iconSize / 2f) - new Vector2(0f, iconSize.Y * 0.6f);
|
||||
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||
}
|
||||
|
||||
private bool TryGetWorldPosition(uint objectId, out Vector3 position)
|
||||
{
|
||||
position = Vector3.Zero;
|
||||
if (objectId == 0 || objectId == uint.MaxValue)
|
||||
return false;
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj?.EntityId == objectId)
|
||||
{
|
||||
position = obj.Position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryResolveWorldPosition(Pair? pair, UserData userData, uint objectId, out Vector3 position)
|
||||
{
|
||||
if (TryGetWorldPosition(objectId, out position))
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: resolved by objectId {objectId}", objectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pair != null)
|
||||
{
|
||||
var name = pair.PlayerName;
|
||||
if (!string.IsNullOrEmpty(name) && TryGetWorldPositionByName(name!, out position))
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: resolved by pair name {name}", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
var ident = pair.Ident;
|
||||
if (!string.IsNullOrEmpty(ident))
|
||||
{
|
||||
var cached = _dalamudUtil.FindPlayerByNameHash(ident);
|
||||
if (!string.IsNullOrEmpty(cached.Name) && TryGetWorldPositionByName(cached.Name, out position))
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: resolved by cached name {name}", cached.Name);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cached.Address != IntPtr.Zero)
|
||||
{
|
||||
var objRef = _objectTable.CreateObjectReference(cached.Address);
|
||||
if (objRef != null)
|
||||
{
|
||||
position = objRef.Position;
|
||||
_logger.LogTrace("TypingIndicator: resolved by cached address {addr}", cached.Address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var alias = userData.AliasOrUID;
|
||||
if (!string.IsNullOrEmpty(alias) && TryGetWorldPositionByName(alias, out position))
|
||||
{
|
||||
_logger.LogTrace("TypingIndicator: resolved by user alias {alias}", alias);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetWorldPositionByName(string name, out Vector3 position)
|
||||
{
|
||||
position = Vector3.Zero;
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj != null && obj.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
position = obj.Position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetPartyIndexForObjectId(uint objectId)
|
||||
{
|
||||
for (var i = 0; i < _partyList.Count; ++i)
|
||||
{
|
||||
var member = _partyList[i];
|
||||
if (member == null) continue;
|
||||
|
||||
var gameObject = member.GameObject;
|
||||
if (gameObject != null && GetEntityId(gameObject.Address) == objectId)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int GetPartyIndexForName(string name)
|
||||
{
|
||||
for (var i = 0; i < _partyList.Count; ++i)
|
||||
{
|
||||
var member = _partyList[i];
|
||||
if (member?.Name == null) continue;
|
||||
|
||||
if (member.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private bool IsPartyMember(uint objectId, string? playerName)
|
||||
{
|
||||
if (objectId != 0 && objectId != uint.MaxValue && GetPartyIndexForObjectId(objectId) >= 0)
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrEmpty(playerName) && GetPartyIndexForName(playerName) >= 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsPlayerRelevant(Pair? pair, bool isPartyMember)
|
||||
{
|
||||
if (isPartyMember)
|
||||
return true;
|
||||
|
||||
if (pair?.UserPair != null)
|
||||
{
|
||||
var userPair = pair.UserPair;
|
||||
if (userPair.OtherPermissions.IsPaired() || userPair.OwnPermissions.IsPaired())
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pair?.GroupPair != null && pair.GroupPair.Any(g =>
|
||||
!g.Value.GroupUserPermissions.IsPaused() &&
|
||||
!g.Key.GroupUserPermissions.IsPaused()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsWithinRelevantDistance(Vector3 position)
|
||||
{
|
||||
if (_clientState.LocalPlayer == null)
|
||||
return false;
|
||||
|
||||
var distance = Vector3.Distance(_clientState.LocalPlayer.Position, position);
|
||||
return distance <= 40f;
|
||||
}
|
||||
|
||||
private static unsafe uint GetEntityId(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return 0;
|
||||
return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address)->EntityId;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.Localization;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
@@ -21,7 +20,6 @@ using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -39,6 +37,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGuiWindowFlags.NoScrollWithMouse;
|
||||
|
||||
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudViolet;
|
||||
public static Vector4 AccentHoverColor { get; set; } = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||
public static Vector4 AccentActiveColor { get; set; } = AccentHoverColor;
|
||||
|
||||
public readonly FileDialogManager FileDialogManager;
|
||||
|
||||
@@ -55,7 +55,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly LocalizationService _localizationService;
|
||||
private readonly ITextureProvider _textureProvider;
|
||||
private readonly Dictionary<string, object> _selectedComboItems = new(StringComparer.Ordinal);
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
@@ -87,7 +86,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
public UiSharedService(ILogger<UiSharedService> logger, IpcManager ipcManager, ApiController apiController,
|
||||
CacheMonitor cacheMonitor, FileDialogManager fileDialogManager,
|
||||
MareConfigService configService, DalamudUtilService dalamudUtil, IDalamudPluginInterface pluginInterface,
|
||||
LocalizationService localizationService, ITextureProvider textureProvider,
|
||||
ITextureProvider textureProvider,
|
||||
ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
|
||||
{
|
||||
_ipcManager = ipcManager;
|
||||
@@ -97,7 +96,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
_configService = configService;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pluginInterface = pluginInterface;
|
||||
_localizationService = localizationService;
|
||||
_textureProvider = textureProvider;
|
||||
_serverConfigurationManager = serverManager;
|
||||
|
||||
@@ -128,10 +126,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
public ApiController ApiController => _apiController;
|
||||
public LocalizationService Localization => _localizationService;
|
||||
|
||||
public string Localize(string key, string fallback, params object[] args) => _localizationService.GetString(key, fallback, args);
|
||||
public string Localize(string fallback, params object[] args) => _localizationService.GetString(fallback, args);
|
||||
|
||||
public bool EditTrackerPosition { get; set; }
|
||||
|
||||
@@ -144,6 +138,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
public string PlayerName => _dalamudUtil.GetPlayerName();
|
||||
|
||||
public IFontHandle UidFont { get; init; }
|
||||
public MareConfigService ConfigService => _configService;
|
||||
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
|
||||
|
||||
public uint WorldId => _dalamudUtil.GetHomeWorldId();
|
||||
@@ -318,7 +313,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
public static Vector4 GetBoolColor(bool input) => input ? AccentColor : ImGuiColors.DalamudRed;
|
||||
public static Vector4 GetBoolColor(bool input) => input ? AccentColor : UiSharedService.AccentColor;
|
||||
|
||||
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
|
||||
{
|
||||
@@ -376,6 +371,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||
float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f;
|
||||
float frameHeight = height ?? ImGui.GetFrameHeight();
|
||||
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
|
||||
Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X,
|
||||
cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f));
|
||||
@@ -386,13 +383,19 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null)
|
||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool useAccentHover = true)
|
||||
{
|
||||
int num = 0;
|
||||
int colorsPushed = 0;
|
||||
if (defaultColor.HasValue)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
|
||||
num++;
|
||||
colorsPushed++;
|
||||
}
|
||||
if (useAccentHover)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||
colorsPushed += 2;
|
||||
}
|
||||
|
||||
ImGui.PushID(text);
|
||||
@@ -412,9 +415,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
||||
ImGui.PopID();
|
||||
if (num > 0)
|
||||
if (colorsPushed > 0)
|
||||
{
|
||||
ImGui.PopStyleColor(num);
|
||||
ImGui.PopStyleColor(colorsPushed);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -424,7 +427,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
return IconTextButtonInternal(icon, text,
|
||||
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
||||
width <= 0 ? null : width);
|
||||
width <= 0 ? null : width,
|
||||
!isInPopup);
|
||||
}
|
||||
|
||||
public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false)
|
||||
@@ -526,7 +530,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
public void BooleanToColoredIcon(bool value, bool inline = true)
|
||||
{
|
||||
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, AccentColor, value);
|
||||
using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value);
|
||||
using var colorred = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, !value);
|
||||
|
||||
if (inline) ImGui.SameLine();
|
||||
|
||||
@@ -600,24 +604,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
|
||||
if (_isPenumbraDirectory)
|
||||
{
|
||||
ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed);
|
||||
ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (_isOneDrive)
|
||||
{
|
||||
ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", ImGuiColors.DalamudRed);
|
||||
ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (!_isDirectoryWritable)
|
||||
{
|
||||
ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed);
|
||||
ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (_cacheDirectoryHasOtherFilesThanCache)
|
||||
{
|
||||
ColorTextWrapped("Your selected directory has files or directories inside that are not Umbra related. Use an empty directory or a previous storage directory only.", ImGuiColors.DalamudRed);
|
||||
ColorTextWrapped("Your selected directory has files or directories inside that are not Umbra related. Use an empty directory or a previous storage directory only.", UiSharedService.AccentColor);
|
||||
}
|
||||
else if (!_cacheDirectoryIsValidPath)
|
||||
{
|
||||
ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " +
|
||||
"Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed);
|
||||
"Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", UiSharedService.AccentColor);
|
||||
}
|
||||
|
||||
float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB;
|
||||
@@ -769,19 +773,15 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
var check = FontAwesomeIcon.Check;
|
||||
var cross = FontAwesomeIcon.SquareXmark;
|
||||
|
||||
var availableTemplate = Localize("Settings.Plugins.Tooltip.Available", "{0} is available and up to date.");
|
||||
var unavailableTemplate = Localize("Settings.Plugins.Tooltip.Unavailable", "{0} is unavailable or not up to date.");
|
||||
string FormatTooltip(bool exists, string pluginName) => string.Format(CultureInfo.CurrentCulture, exists ? availableTemplate : unavailableTemplate, pluginName);
|
||||
|
||||
if (intro)
|
||||
{
|
||||
ImGui.SetWindowFontScale(0.8f);
|
||||
BigText(Localize("Settings.Plugins.MandatoryHeading", "Mandatory Plugins"));
|
||||
BigText("Mandatory Plugins");
|
||||
ImGui.SetWindowFontScale(1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(Localize("Settings.Plugins.MandatoryLabel", "Mandatory Plugins:"));
|
||||
ImGui.TextUnformatted("Mandatory Plugins:");
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
@@ -789,23 +789,23 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_penumbraExists ? check : cross, GetBoolColor(_penumbraExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_penumbraExists, "Penumbra"));
|
||||
AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
|
||||
ImGui.TextUnformatted("Glamourer");
|
||||
ImGui.SameLine();
|
||||
IconText(_glamourerExists ? check : cross, GetBoolColor(_glamourerExists));
|
||||
AttachToolTip(FormatTooltip(_glamourerExists, "Glamourer"));
|
||||
AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
|
||||
if (intro)
|
||||
{
|
||||
ImGui.SetWindowFontScale(0.8f);
|
||||
BigText(Localize("Settings.Plugins.OptionalHeading", "Optional Addons"));
|
||||
BigText("Optional Addons");
|
||||
ImGui.SetWindowFontScale(1.0f);
|
||||
UiSharedService.TextWrapped(Localize("Settings.Plugins.OptionalDescription", "These addons are not required for basic operation, but without them you may not see others as intended."));
|
||||
UiSharedService.TextWrapped("These addons are not required for basic operation, but without them you may not see others as intended.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(Localize("Settings.Plugins.OptionalLabel", "Optional Addons:"));
|
||||
ImGui.TextUnformatted("Optional Addons:");
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
@@ -815,7 +815,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_heelsExists ? check : cross, GetBoolColor(_heelsExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_heelsExists, "SimpleHeels"));
|
||||
AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -823,7 +823,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_customizePlusExists ? check : cross, GetBoolColor(_customizePlusExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_customizePlusExists, "Customize+"));
|
||||
AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -831,7 +831,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_honorificExists ? check : cross, GetBoolColor(_honorificExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_honorificExists, "Honorific"));
|
||||
AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -839,7 +839,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_petNamesExists ? check : cross, GetBoolColor(_petNamesExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_petNamesExists, "PetNicknames"));
|
||||
AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.SetCursorPosX(alignPos);
|
||||
@@ -847,7 +847,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_moodlesExists ? check : cross, GetBoolColor(_moodlesExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_moodlesExists, "Moodles"));
|
||||
AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -855,12 +855,12 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.SameLine();
|
||||
IconText(_brioExists ? check : cross, GetBoolColor(_brioExists));
|
||||
ImGui.SameLine();
|
||||
AttachToolTip(FormatTooltip(_brioExists, "Brio"));
|
||||
AttachToolTip($"Brio is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
|
||||
ImGui.Spacing();
|
||||
|
||||
if (!_penumbraExists || !_glamourerExists)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, Localize("Settings.Plugins.WarningMandatoryMissing", "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra."));
|
||||
ImGui.TextColored(UiSharedService.AccentColor, "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.WebAPI.SignalR;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
@@ -65,15 +66,21 @@ public class DiscoveryApiClient
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SendRequestAsync(string endpoint, string token, string? displayName, CancellationToken ct)
|
||||
public async Task<bool> SendRequestAsync(string endpoint, string? token, string? targetUid, string? displayName, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(targetUid))
|
||||
{
|
||||
_logger.LogWarning("Discovery request aborted: no token or targetUid provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(jwt)) return false;
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
var body = JsonSerializer.Serialize(new { token, displayName });
|
||||
var body = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
|
||||
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
@@ -82,7 +89,7 @@ public class DiscoveryApiClient
|
||||
if (string.IsNullOrEmpty(jwt2)) return false;
|
||||
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||
var body2 = JsonSerializer.Serialize(new { token, displayName });
|
||||
var body2 = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
|
||||
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||
}
|
||||
@@ -102,6 +109,14 @@ public class DiscoveryApiClient
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RequestPayload(
|
||||
[property: JsonPropertyName("token"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
string? Token,
|
||||
[property: JsonPropertyName("targetUid"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
string? TargetUid,
|
||||
[property: JsonPropertyName("displayName"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
string? DisplayName);
|
||||
|
||||
public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -4,6 +4,7 @@ using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.Files;
|
||||
using MareSynchronos.API.Routes;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
@@ -20,17 +21,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private readonly FileTransferOrchestrator _orchestrator;
|
||||
private readonly List<ThrottledStream> _activeDownloadStreams;
|
||||
private readonly object _queueLock = new();
|
||||
private SemaphoreSlim? _downloadQueueSemaphore;
|
||||
private int _downloadQueueCapacity = -1;
|
||||
|
||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
|
||||
FileTransferOrchestrator orchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService) : base(logger, mediator)
|
||||
{
|
||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||
_orchestrator = orchestrator;
|
||||
_fileDbManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
_mareConfigService = mareConfigService;
|
||||
_activeDownloadStreams = [];
|
||||
|
||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||
@@ -59,6 +65,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
|
||||
{
|
||||
SemaphoreSlim? queueSemaphore = null;
|
||||
if (_mareConfigService.Current.EnableDownloadQueue)
|
||||
{
|
||||
queueSemaphore = GetQueueSemaphore();
|
||||
Logger.LogTrace("Queueing download for {name}. Currently queued: {queued}", gameObject.Name, queueSemaphore.CurrentCount);
|
||||
await queueSemaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
||||
try
|
||||
{
|
||||
@@ -70,6 +84,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (queueSemaphore != null)
|
||||
{
|
||||
queueSemaphore.Release();
|
||||
}
|
||||
|
||||
Mediator.Publish(new DownloadFinishedMessage(gameObject));
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
|
||||
}
|
||||
@@ -132,6 +151,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
return (string.Join("", hashName), long.Parse(string.Join("", fileLength)));
|
||||
}
|
||||
|
||||
private SemaphoreSlim GetQueueSemaphore()
|
||||
{
|
||||
var desiredCapacity = Math.Clamp(_mareConfigService.Current.ParallelDownloads, 1, 10);
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
if (_downloadQueueSemaphore == null || _downloadQueueCapacity != desiredCapacity)
|
||||
{
|
||||
_downloadQueueSemaphore = new SemaphoreSlim(desiredCapacity, desiredCapacity);
|
||||
_downloadQueueCapacity = desiredCapacity;
|
||||
}
|
||||
|
||||
return _downloadQueueSemaphore;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
|
||||
{
|
||||
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));
|
||||
|
||||
@@ -97,6 +97,12 @@ public partial class ApiController
|
||||
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetTypingState(bool isTyping)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
||||
{
|
||||
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||
|
||||
@@ -138,6 +138,13 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserTypingState(TypingStateDto dto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserTypingState: {uid} typing={typing}", dto.User.UID, dto.IsTyping);
|
||||
Mediator.Publish(new UserTypingStateMessage(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
|
||||
@@ -313,6 +320,12 @@ public partial class ApiController
|
||||
_mareHub!.On(nameof(Client_UserChatMsg), act);
|
||||
}
|
||||
|
||||
public void OnUserTypingState(Action<TypingStateDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserTypingState), act);
|
||||
}
|
||||
|
||||
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
@@ -348,6 +348,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
|
||||
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
|
||||
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
|
||||
OnUserTypingState(dto => _ = Client_UserTypingState(dto));
|
||||
|
||||
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
|
||||
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
|
||||
@@ -393,7 +394,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
foreach (var user in users)
|
||||
{
|
||||
Logger.LogDebug("Group Pair: {user}", user);
|
||||
_pairManager.AddGroupPair(user);
|
||||
_pairManager.AddGroupPair(user, isInitialLoad: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule Penumbra.Api updated: dd14131793...c23ee05c1e
15
Program.cs
Normal file
15
Program.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
var asm = AssemblyDefinition.ReadAssembly("/Users/luca.genovese/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/FFXIVClientStructs.dll");
|
||||
var type = asm.MainModule.GetType("FFXIVClientStructs.FFXIV.Client.UI.AddonNamePlate/NamePlateObject");
|
||||
foreach (var field in type.Fields)
|
||||
{
|
||||
Console.WriteLine($"FIELD {field.Name}: {field.FieldType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user