All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
578 lines
21 KiB
C#
578 lines
21 KiB
C#
using Dalamud.Interface;
|
|
using LightlessSync.API.Dto.Group;
|
|
using LightlessSync.API.Dto.User;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI;
|
|
using LightlessSync.UI.Models;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.Services.LightFinder;
|
|
public class LightFinderService : IHostedService, IMediatorSubscriber
|
|
{
|
|
private readonly ILogger<LightFinderService> _logger;
|
|
private readonly ApiController _apiController;
|
|
private readonly LightlessMediator _mediator;
|
|
private readonly LightlessConfigService _config;
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
private CancellationTokenSource? _lightfinderCancelTokens;
|
|
private Action? _connectedHandler;
|
|
public LightlessMediator Mediator => _mediator;
|
|
|
|
public bool IsLightFinderAvailable { get; private set; } = false;
|
|
|
|
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
|
private bool _syncedOnStartup = false;
|
|
private bool _waitingForTtlFetch = false;
|
|
private TimeSpan? _remainingTtl = null;
|
|
private DateTime _lastTtlCheck = DateTime.MinValue;
|
|
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
|
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
|
|
public TimeSpan? RemainingTtl => _remainingTtl;
|
|
public TimeSpan? RemainingCooldown
|
|
{
|
|
get
|
|
{
|
|
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
|
|
if (elapsed >= _disableCooldown) return null;
|
|
return _disableCooldown - elapsed;
|
|
}
|
|
}
|
|
|
|
public LightFinderService(ILogger<LightFinderService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
|
{
|
|
_logger = logger;
|
|
_mediator = mediator;
|
|
_config = config;
|
|
_dalamudUtil = dalamudUtil;
|
|
_apiController = apiController;
|
|
}
|
|
|
|
private async Task RequireConnectionAsync(string context, Func<Task> action)
|
|
{
|
|
if (!_apiController.IsConnected)
|
|
{
|
|
_logger.LogDebug("{context} skipped, not connected", context);
|
|
return;
|
|
}
|
|
await action().ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task<string?> GetLocalHashedCidAsync(string context)
|
|
{
|
|
try
|
|
{
|
|
var cid = _dalamudUtil.GetCID();
|
|
return cid.ToString().GetHash256();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
|
{
|
|
bool wasEnabled = _config.Current.BroadcastEnabled;
|
|
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
|
bool hadRemaining = _remainingTtl.HasValue;
|
|
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
|
|
if (wasEnabled || hadExpiry)
|
|
_config.Save();
|
|
|
|
_remainingTtl = null;
|
|
_waitingForTtlFetch = false;
|
|
_syncedOnStartup = false;
|
|
|
|
if (forcePublish || wasEnabled || hadRemaining)
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
|
|
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
|
{
|
|
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
|
{
|
|
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
|
return false;
|
|
}
|
|
|
|
bool wasEnabled = _config.Current.BroadcastEnabled;
|
|
TimeSpan? previousRemaining = _remainingTtl;
|
|
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
|
|
|
var newExpiry = DateTime.UtcNow + validTtl;
|
|
|
|
_config.Current.BroadcastEnabled = true;
|
|
_config.Current.BroadcastTtl = newExpiry;
|
|
|
|
if (!wasEnabled || previousExpiry != newExpiry)
|
|
_config.Save();
|
|
|
|
_remainingTtl = validTtl;
|
|
_waitingForTtlFetch = false;
|
|
|
|
if (!wasEnabled || previousRemaining != validTtl)
|
|
{
|
|
var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null;
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid));
|
|
}
|
|
|
|
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
|
return true;
|
|
}
|
|
|
|
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
|
{
|
|
if (ex != null)
|
|
_logger.LogWarning(ex, message);
|
|
else
|
|
_logger.LogWarning(message);
|
|
|
|
IsLightFinderAvailable = false;
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
}
|
|
|
|
private void OnDisconnected()
|
|
{
|
|
IsLightFinderAvailable = false;
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
|
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
|
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
|
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
|
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
|
|
|
IsLightFinderAvailable = false;
|
|
|
|
_lightfinderCancelTokens?.Cancel();
|
|
_lightfinderCancelTokens?.Dispose();
|
|
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
|
_apiController.OnConnected += _connectedHandler;
|
|
|
|
if (_apiController.IsConnected)
|
|
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_lightfinderCancelTokens?.Cancel();
|
|
_lightfinderCancelTokens?.Dispose();
|
|
_lightfinderCancelTokens = null;
|
|
|
|
if (_connectedHandler is not null)
|
|
{
|
|
_apiController.OnConnected -= _connectedHandler;
|
|
_connectedHandler = null;
|
|
}
|
|
|
|
_mediator.UnsubscribeAll(this);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
|
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
return;
|
|
|
|
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(hashedCid))
|
|
return;
|
|
|
|
BroadcastStatusInfoDto? status = null;
|
|
try
|
|
{
|
|
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
|
}
|
|
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
|
}
|
|
|
|
if (!IsLightFinderAvailable)
|
|
_logger.LogInformation("Lightfinder is available.");
|
|
|
|
IsLightFinderAvailable = true;
|
|
|
|
bool isBroadcasting = status?.IsBroadcasting == true;
|
|
TimeSpan? ttl = status?.TTL;
|
|
|
|
if (isBroadcasting)
|
|
{
|
|
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
|
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
|
|
|
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
|
{
|
|
_syncedOnStartup = true;
|
|
}
|
|
else
|
|
{
|
|
isBroadcasting = false;
|
|
}
|
|
}
|
|
|
|
if (!isBroadcasting)
|
|
{
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
|
}
|
|
|
|
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
|
{
|
|
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
|
|
|
_mediator.Publish(new NotificationMessage(
|
|
"Broadcast Auto-Enabled",
|
|
"Your Lightfinder broadcast has been automatically enabled.",
|
|
NotificationType.Info));
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Lightfinder check was canceled.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
|
}
|
|
}
|
|
|
|
private void OnEnableBroadcast(EnableBroadcastMessage msg)
|
|
{
|
|
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
|
|
{
|
|
try
|
|
{
|
|
GroupBroadcastRequestDto? groupDto = null;
|
|
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
|
|
{
|
|
groupDto = new GroupBroadcastRequestDto
|
|
{
|
|
HashedCID = msg.HashedCid,
|
|
GID = _config.Current.SelectedFinderSyncshell,
|
|
Enabled = msg.Enabled,
|
|
};
|
|
}
|
|
|
|
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false);
|
|
|
|
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
|
|
|
if (!msg.Enabled)
|
|
{
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
|
return;
|
|
}
|
|
|
|
_waitingForTtlFetch = true;
|
|
|
|
try
|
|
{
|
|
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
|
|
|
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
|
{
|
|
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
|
Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
|
}
|
|
else
|
|
{
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_waitingForTtlFetch = false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
|
|
_waitingForTtlFetch = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
|
{
|
|
_config.Current.BroadcastEnabled = msg.Enabled;
|
|
_config.Save();
|
|
}
|
|
|
|
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
|
|
{
|
|
bool result = false;
|
|
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
|
|
|
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
|
result = info?.TTL > TimeSpan.Zero;
|
|
|
|
|
|
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
|
|
{
|
|
TimeSpan? ttl = null;
|
|
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
|
try
|
|
{
|
|
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (cidForLog is { Length: > 0 })
|
|
{
|
|
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
|
}
|
|
}
|
|
}).ConfigureAwait(false);
|
|
return ttl;
|
|
}
|
|
|
|
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
|
|
{
|
|
Dictionary<string, BroadcastStatusInfoDto?> result = new(StringComparer.Ordinal);
|
|
|
|
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
|
|
{
|
|
try
|
|
{
|
|
var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false);
|
|
|
|
if (batch?.Results != null)
|
|
{
|
|
foreach (var kv in batch.Results)
|
|
result[kv.Key] = kv.Value;
|
|
}
|
|
|
|
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to batch check broadcast status");
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
return result;
|
|
}
|
|
|
|
public async void ToggleBroadcast()
|
|
{
|
|
|
|
if (!IsLightFinderAvailable)
|
|
{
|
|
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
|
_mediator.Publish(new NotificationMessage(
|
|
"Broadcast Unavailable",
|
|
"Lightfinder is not available on this server.",
|
|
NotificationType.Error));
|
|
return;
|
|
}
|
|
|
|
await RequireConnectionAsync(nameof(ToggleBroadcast), async () =>
|
|
{
|
|
var cooldown = RemainingCooldown;
|
|
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
|
{
|
|
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
|
_mediator.Publish(new NotificationMessage(
|
|
"Broadcast Cooldown",
|
|
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
|
|
NotificationType.Warning));
|
|
return;
|
|
}
|
|
|
|
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(hashedCid))
|
|
{
|
|
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
|
|
var newStatus = !isCurrentlyBroadcasting;
|
|
|
|
if (!newStatus)
|
|
{
|
|
_lastForcedDisableTime = DateTime.UtcNow;
|
|
_logger.LogDebug("Manual disable: cooldown timer started.");
|
|
}
|
|
|
|
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
|
|
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
|
|
|
_mediator.Publish(new NotificationMessage(
|
|
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
|
|
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
|
|
NotificationType.Info));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
|
_mediator.Publish(new NotificationMessage(
|
|
"Broadcast Toggle Failed",
|
|
$"Failed to toggle broadcast: {ex.Message}",
|
|
NotificationType.Error));
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private async void OnTick(PriorityFrameworkUpdateMessage _)
|
|
{
|
|
if (!IsLightFinderAvailable)
|
|
return;
|
|
|
|
if (_config?.Current == null)
|
|
return;
|
|
|
|
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
|
|
return;
|
|
|
|
_lastTtlCheck = DateTime.UtcNow;
|
|
|
|
await RequireConnectionAsync(nameof(OnTick), async () => {
|
|
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
|
{
|
|
try
|
|
{
|
|
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(hashedCid))
|
|
{
|
|
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
|
return;
|
|
}
|
|
|
|
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
|
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
|
{
|
|
_syncedOnStartup = true;
|
|
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
|
_syncedOnStartup = false;
|
|
}
|
|
}
|
|
if (_config.Current.BroadcastEnabled)
|
|
{
|
|
if (_waitingForTtlFetch)
|
|
{
|
|
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
|
|
return;
|
|
}
|
|
|
|
DateTime expiry = _config.Current.BroadcastTtl;
|
|
TimeSpan remaining = expiry - DateTime.UtcNow;
|
|
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
|
if (_remainingTtl == null)
|
|
{
|
|
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
|
ApplyBroadcastDisabled(forcePublish: true);
|
|
ShowBroadcastExpiredNotification();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_remainingTtl = null;
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private void ShowBroadcastExpiredNotification()
|
|
{
|
|
var notification = new LightlessNotification
|
|
{
|
|
Id = "broadcast_expired",
|
|
Title = "Broadcast Expired",
|
|
Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?",
|
|
Type = NotificationType.PairRequest,
|
|
Duration = TimeSpan.FromSeconds(180),
|
|
Actions = new List<LightlessNotificationAction>
|
|
{
|
|
new()
|
|
{
|
|
Id = "re_enable",
|
|
Label = "Re-enable",
|
|
Icon = FontAwesomeIcon.Plus,
|
|
Color = UIColors.Get("PairBlue"),
|
|
IsPrimary = true,
|
|
OnClick = (n) =>
|
|
{
|
|
_logger.LogInformation("Re-enabling broadcast from notification");
|
|
ToggleBroadcast();
|
|
n.IsDismissed = true;
|
|
n.IsAnimatingOut = true;
|
|
}
|
|
},
|
|
new()
|
|
{
|
|
Id = "close",
|
|
Label = "Close",
|
|
Icon = FontAwesomeIcon.Times,
|
|
Color = UIColors.Get("DimRed"),
|
|
OnClick = (n) =>
|
|
{
|
|
_logger.LogInformation("Broadcast expiration notification dismissed");
|
|
n.IsDismissed = true;
|
|
n.IsAnimatingOut = true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
_mediator.Publish(new LightlessNotificationMessage(notification));
|
|
}
|
|
} |