diff --git a/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs b/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs index 8ca435f9c9..f4357d5c53 100644 --- a/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs +++ b/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs @@ -207,8 +207,8 @@ private async Task RunThroughHeroic(string type, long appId, CancellationToken c try { - var start = DateTime.UtcNow; - while (!cancellationToken.IsCancellationRequested && start + timeout > DateTime.UtcNow) + var start = TimeProvider.System.GetTimestamp(); + while (!cancellationToken.IsCancellationRequested && TimeProvider.System.GetElapsedTime(start) > timeout) { var processes = Process.GetProcessesByName(processName); var target = existingProcesses is not null diff --git a/src/Abstractions/NexusMods.Abstractions.IO/StreamFactories/NativeFileStreamFactory.cs b/src/Abstractions/NexusMods.Abstractions.IO/StreamFactories/NativeFileStreamFactory.cs index 3c09b84f42..3c9b7bbc4e 100644 --- a/src/Abstractions/NexusMods.Abstractions.IO/StreamFactories/NativeFileStreamFactory.cs +++ b/src/Abstractions/NexusMods.Abstractions.IO/StreamFactories/NativeFileStreamFactory.cs @@ -8,7 +8,6 @@ namespace NexusMods.Abstractions.IO.StreamFactories; public class NativeFileStreamFactory : IStreamFactory { private AbsolutePath _file; - private DateTime? _lastModifiedCache; /// public Size Size => _file.FileInfo.Size; diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 2287d2a27c..15c45c7a7a 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -340,7 +340,7 @@ await ActionExtractToDisk(groupings, register, tx, tx.Add(gameMetadataId, GameInstallMetadata.LastSyncedLoadout, loadout.Id); tx.Add(gameMetadataId, GameInstallMetadata.LastSyncedLoadoutTransaction, EntityId.From(tx.ThisTxId.Value)); tx.Add(gameMetadataId, GameInstallMetadata.LastScannedDiskStateTransaction, EntityId.From(tx.ThisTxId.Value)); - tx.Add(loadout.Id, Loadout.LastAppliedDateTime, DateTime.UtcNow); + tx.Add(loadout.Id, Loadout.LastAppliedDateTime, TimeProvider.System.GetLocalNow()); await tx.Commit(); loadout = loadout.Rebase(); @@ -499,7 +499,7 @@ await _fileStore.ExtractFiles(toExtract.Select(item => { tx.Add(entry.Disk.Value.Id, DiskStateEntry.Hash, entry.LoadoutFileHash.Value); tx.Add(entry.Disk.Value.Id, DiskStateEntry.Size, entry.LoadoutFileSize.Value); - tx.Add(entry.Disk.Value.Id, DiskStateEntry.LastModified, DateTime.UtcNow); + tx.Add(entry.Disk.Value.Id, DiskStateEntry.LastModified, TimeProvider.System.GetLocalNow()); } else { @@ -508,7 +508,7 @@ await _fileStore.ExtractFiles(toExtract.Select(item => Path = entry.Path.ToGamePathParentTuple(gameMetadataId), Hash = entry.LoadoutFileHash.Value, Size = entry.LoadoutFileSize.Value, - LastModified = DateTime.UtcNow, + LastModified = TimeProvider.System.GetLocalNow(), GameId = gameMetadataId, }; } @@ -591,7 +591,7 @@ private async Task ActionIngestFromDisk(SyncActionGroupings groupi tx.Add(prevLoadoutFile.Id, LoadoutFile.Hash, file.Disk.Value.Hash); tx.Add(prevLoadoutFile.Id, LoadoutFile.Size, file.Disk.Value.Size); - tx.Add(file.Disk.Value.Id, DiskStateEntry.LastModified, DateTime.UtcNow); + tx.Add(file.Disk.Value.Id, DiskStateEntry.LastModified, TimeProvider.System.GetLocalNow()); continue; } } @@ -624,7 +624,7 @@ private async Task ActionIngestFromDisk(SyncActionGroupings groupi LoadoutFileEntry = loadoutFile, } ); - tx.Add(file.Disk.Value.Id, DiskStateEntry.LastModified, DateTime.UtcNow); + tx.Add(file.Disk.Value.Id, DiskStateEntry.LastModified, TimeProvider.System.GetLocalNow()); } if (added.Count > 0) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/DiskStateEntry.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/DiskStateEntry.cs index 7b26cec6c3..aed25153c0 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/DiskStateEntry.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/DiskStateEntry.cs @@ -36,7 +36,7 @@ public partial class DiskStateEntry : IModelDefinition /// /// The last modified time of the file /// - public static readonly DateTimeAttribute LastModified = new(Namespace, nameof(LastModified)); + public static readonly TimestampAttribute LastModified = new(Namespace, nameof(LastModified)); /// /// The owning game installation diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs index 60d61bd094..eb8020991d 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs @@ -54,14 +54,9 @@ public partial class Loadout : IModelDefinition /// /// DateTime when the loadout was last applied. - /// Returns DateTime.MinValue if the loadout has never been applied. /// - public static readonly DateTimeAttribute LastAppliedDateTime = new(Namespace, nameof(LastAppliedDateTime)) - { - IsOptional = true, - DefaultValue = DateTime.MinValue, - }; - + public static readonly TimestampAttribute LastAppliedDateTime = new(Namespace, nameof(LastAppliedDateTime)) { IsOptional = true, }; + /// /// All items in the Loadout. /// diff --git a/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/DateTimeAttribute.cs b/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/DateTimeAttribute.cs deleted file mode 100644 index 3193676425..0000000000 --- a/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/DateTimeAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NexusMods.MnemonicDB.Abstractions; -using NexusMods.MnemonicDB.Abstractions.Attributes; -using NexusMods.MnemonicDB.Abstractions.ElementComparers; -using NexusMods.MnemonicDB.Abstractions.ValueSerializers; - -namespace NexusMods.Abstractions.MnemonicDB.Attributes; - -/// -/// A MneumonicDB attribute for a DateTime value -/// -/// -/// Depending on use case, consider using transaction timestamps instead of a dedicated DateTimeAttribute. -/// -public class DateTimeAttribute(string ns, string name) : ScalarAttribute(ns, name) -{ - /// - protected override long ToLowLevel(DateTime value) => value.ToBinary(); - - /// - protected override DateTime FromLowLevel(long value, AttributeResolver resolver) - => DateTime.FromBinary(value); -} diff --git a/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/Extensions/EntityExtensions.cs b/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/Extensions/EntityExtensions.cs index fbd34baa46..2cfa454ca1 100644 --- a/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/Extensions/EntityExtensions.cs +++ b/src/Abstractions/NexusMods.Abstractions.MnemonicDB.Attributes/Extensions/EntityExtensions.cs @@ -24,11 +24,11 @@ public static TxId MostRecentTxId(this IReadOnlyModel model) /// /// A default value to return if the model doesn't exist. /// - public static DateTimeOffset GetCreatedAt(this T model, DateTime? dateTime = null) + public static DateTimeOffset GetCreatedAt(this T model, DateTimeOffset? dateTime = null) where T : IReadOnlyModel { if (model.Count == 0) - return dateTime ?? DateTime.MinValue; + return dateTime ?? DateTimeOffset.MinValue; var tx = new Transaction.ReadOnly(model.Db, EntityId.From(model.Min(m => m.T).Value)); return Transaction.Timestamp.Get(tx); } diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs index 3467b6f823..35cc2dd02f 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs @@ -35,8 +35,8 @@ public partial class NexusModsFileMetadata : IModelDefinition /// /// The date the file was uploaded at. /// - public static readonly DateTimeAttribute UploadedAt = new(Namespace, nameof(UploadedAt)); - + public static readonly TimestampAttribute UploadedAt = new(Namespace, nameof(UploadedAt)); + /// /// The size in bytes of the file. /// diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs index e627201442..9b902c5c6f 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs @@ -40,7 +40,7 @@ public partial class NexusModsModPageMetadata : IModelDefinition /// /// The last time the mod page was updated (UTC). This is useful for cache invalidation. /// - public static readonly DateTimeAttribute UpdatedAt = new(Namespace, nameof(UpdatedAt)); + public static readonly TimestampAttribute UpdatedAt = new(Namespace, nameof(UpdatedAt)); /// /// Uri for the full sized picture of the mod. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs index 4c8f9c8c17..83edff398c 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs @@ -89,7 +89,7 @@ public class GameInfo : IJsonArraySerializable /// /// Timestamp of when the game was approved by the site staff, expressed as UTC, Coordinated Universal Time. /// - public DateTime ApprovedDateUtc => DateTimeOffset.FromUnixTimeSeconds(ApprovedDate).UtcDateTime; + public DateTimeOffset ApprovedDateUtc => DateTimeOffset.FromUnixTimeSeconds(ApprovedDate); /// /// Number of views on this file. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs index 1167260354..93b6d52bc6 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs @@ -113,7 +113,7 @@ public class ModFile : IJsonSerializable /// Expressed as ISO 8601 compatible date/time notation. /// [JsonPropertyName("uploaded_time")] - public DateTime UploadedTime { get; set; } + public DateTimeOffset UploadedTime { get; set; } /// /// Version of the mod. See for more details. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModInfo.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModInfo.cs index c7f3ce0425..a3bda3cc9c 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModInfo.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModInfo.cs @@ -64,13 +64,13 @@ public class ModInfo : IJsonSerializable public int CreatedTimestamp { get; set; } [JsonPropertyName("created_time")] - public DateTime CreatedTime { get; set; } + public DateTimeOffset CreatedTime { get; set; } [JsonPropertyName("updated_timestamp")] public int UpdatedTimestamp { get; set; } [JsonPropertyName("updated_time")] - public DateTime UpdatedTime { get; set; } + public DateTimeOffset UpdatedTime { get; set; } [JsonPropertyName("author")] public string Author { get; set; } = ""; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs index c2b86c3f62..25c2ed8548 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs @@ -39,7 +39,7 @@ public class ModUpdate : IJsonArraySerializable /// /// The last time a file on the mod page was updated. /// - public DateTime LatestFileUpdatedUtc => DateTimeOffset.FromUnixTimeSeconds(LatestFileUpdated).UtcDateTime; + public DateTimeOffset LatestFileUpdatedUtc => DateTimeOffset.FromUnixTimeSeconds(LatestFileUpdated); /// /// The last time any change was made to the mod page. @@ -53,7 +53,7 @@ public class ModUpdate : IJsonArraySerializable /// /// The last time any change was made to the mod page. /// - public DateTime LatestModActivityUtc => DateTimeOffset.FromUnixTimeSeconds(LatestModActivity).UtcDateTime; + public DateTimeOffset LatestModActivityUtc => DateTimeOffset.FromUnixTimeSeconds(LatestModActivity); /// public static JsonTypeInfo GetArrayTypeInfo() => ModUpdateArrayContext.Default.ModUpdateArray; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ResponseMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ResponseMetadata.cs index 70bee92888..6201b96723 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ResponseMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ResponseMetadata.cs @@ -21,7 +21,7 @@ public class ResponseMetadata /// /// [Rate Limit] Stores the time when the daily limit is next reset. /// - public DateTime DailyReset { get; set; } + public DateTimeOffset DailyReset { get; set; } /// /// [Rate Limit] Stores the limit your will be reset to once @@ -37,7 +37,7 @@ public class ResponseMetadata /// /// [Rate Limit] Stores the time when the hourly limit is next reset. /// - public DateTime HourlyReset { get; set; } + public DateTimeOffset HourlyReset { get; set; } /// /// Time taken to execute the request server side, in seconds. @@ -58,11 +58,11 @@ void ParseInt(string headerName, out int output) output = limit; } - void ParseDateTime(string headerName, out DateTime output) + void ParseDateTime(string headerName, out DateTimeOffset output) { output = default; if (result.Headers.TryGetValues(headerName, out var values)) - if (DateTime.TryParse(values.First(), out var limit)) + if (DateTimeOffset.TryParse(values.First(), out var limit)) output = limit; } diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs index 5573c5fd28..80a25b5a60 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs @@ -63,8 +63,7 @@ public interface INexusApiClient /// /// Currently available for Premium users only; with some minor exceptions [nxm links]. /// - Task> DownloadLinksAsync(string domain, ModId modId, FileId fileId, NXMKey key, DateTime expireTime, CancellationToken token = default); - + Task> DownloadLinksAsync(string domain, ModId modId, FileId fileId, NXMKey key, DateTimeOffset expireTime, CancellationToken token = default); /// /// Get the download links for a collection. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMUrl.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMUrl.cs index 698707cb67..f909aae944 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMUrl.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMUrl.cs @@ -16,12 +16,12 @@ public class NXMUrl /// /// if applicable, the time the url becomes invalid /// - public DateTime? ExpireTime + public DateTimeOffset? ExpireTime { get { var expires = Query.Get("expires"); - return expires != null ? DateTime.UnixEpoch.AddSeconds(ulong.Parse(expires)) : null; + return expires != null ? DateTimeOffset.UnixEpoch.AddSeconds(ulong.Parse(expires)) : null; } } diff --git a/src/Abstractions/NexusMods.Abstractions.Resources.DB/PersistedDbResource.cs b/src/Abstractions/NexusMods.Abstractions.Resources.DB/PersistedDbResource.cs index b37b116349..6b1b31e66f 100644 --- a/src/Abstractions/NexusMods.Abstractions.Resources.DB/PersistedDbResource.cs +++ b/src/Abstractions/NexusMods.Abstractions.Resources.DB/PersistedDbResource.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using NexusMods.Abstractions.MnemonicDB.Attributes; +using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; namespace NexusMods.Abstractions.Resources.DB; @@ -20,7 +21,7 @@ public partial class PersistedDbResource : IModelDefinition /// /// The expiration date. /// - public static readonly DateTimeAttribute ExpiresAt = new(Namespace, nameof(ExpiresAt)); + public static readonly TimestampAttribute ExpiresAt = new(Namespace, nameof(ExpiresAt)); /// /// The resource identifier as a hash. @@ -32,6 +33,6 @@ public partial struct ReadOnly /// /// Whether the resource is expired. /// - public bool IsExpired => DateTime.UtcNow > ExpiresAt; + public bool IsExpired => TimeProvider.System.GetLocalNow() > ExpiresAt; } } diff --git a/src/Abstractions/NexusMods.Abstractions.Resources.IO/HttpLoader.cs b/src/Abstractions/NexusMods.Abstractions.Resources.IO/HttpLoader.cs index 9d4473b8c2..af9f63f40f 100644 --- a/src/Abstractions/NexusMods.Abstractions.Resources.IO/HttpLoader.cs +++ b/src/Abstractions/NexusMods.Abstractions.Resources.IO/HttpLoader.cs @@ -35,19 +35,18 @@ public async ValueTask> LoadResourceAsync(Uri resourceIdentifie }; } - private static DateTime GetExpiresAt(HttpResponseMessage responseMessage) + private static DateTimeOffset GetExpiresAt(HttpResponseMessage responseMessage) { var cacheControl = responseMessage.Headers.CacheControl; - if (cacheControl is null) return DateTime.MaxValue; - var maxAge = cacheControl.MaxAge; - if (!maxAge.HasValue) return DateTime.MaxValue; + var maxAge = cacheControl?.MaxAge; + if (!maxAge.HasValue) return DateTimeOffset.MaxValue; var age = responseMessage.Headers.Age; var diff = maxAge.Value; if (age.HasValue) diff -= age.Value; - return DateTime.UtcNow + diff; + return TimeProvider.System.GetUtcNow() + diff; } } diff --git a/src/Abstractions/NexusMods.Abstractions.Resources/Resource.cs b/src/Abstractions/NexusMods.Abstractions.Resources/Resource.cs index 239cd4a0e0..e4748b9d9d 100644 --- a/src/Abstractions/NexusMods.Abstractions.Resources/Resource.cs +++ b/src/Abstractions/NexusMods.Abstractions.Resources/Resource.cs @@ -16,7 +16,7 @@ public record Resource where TData : notnull /// /// Gets the expiration date. /// - public DateTime ExpiresAt { get; init; } = DateTime.MaxValue; + public DateTimeOffset ExpiresAt { get; init; } = DateTimeOffset.MaxValue; /// /// Creates a new resource. diff --git a/src/Networking/NexusMods.Networking.ModUpdates/IModFeedItem.cs b/src/Networking/NexusMods.Networking.ModUpdates/IModFeedItem.cs index d9d316698b..2407e04231 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/IModFeedItem.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/IModFeedItem.cs @@ -16,7 +16,6 @@ public interface IModFeedItem /// /// Retrieves the time the item was last updated. - /// This date is in UTC. /// - public DateTime GetLastUpdatedDateUtc(); + public DateTimeOffset GetLastUpdatedDateUtc(); } diff --git a/src/Networking/NexusMods.Networking.ModUpdates/Mixins/ModFeedItemUpdateMixin.cs b/src/Networking/NexusMods.Networking.ModUpdates/Mixins/ModFeedItemUpdateMixin.cs index 8ba3410d08..d58b45104e 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/Mixins/ModFeedItemUpdateMixin.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/Mixins/ModFeedItemUpdateMixin.cs @@ -8,7 +8,7 @@ namespace NexusMods.Networking.ModUpdates.Mixins; /// public readonly struct ModFeedItemUpdateMixin : IModFeedItem { - private readonly DateTime _lastUpdatedDate; + private readonly DateTimeOffset _lastUpdatedDate; private readonly GameId _gameId; private readonly ModId _modId; @@ -29,7 +29,7 @@ private ModFeedItemUpdateMixin(ModUpdate update, GameId gameId) public static IEnumerable FromUpdateResults(IEnumerable updates, GameId gameId) => updates.Select(update => new ModFeedItemUpdateMixin(update, gameId)); /// - public DateTime GetLastUpdatedDateUtc() => _lastUpdatedDate; + public DateTimeOffset GetLastUpdatedDateUtc() => _lastUpdatedDate; /// public UidForMod GetModPageId() => new() diff --git a/src/Networking/NexusMods.Networking.ModUpdates/Mixins/PageMetadataMixin.cs b/src/Networking/NexusMods.Networking.ModUpdates/Mixins/PageMetadataMixin.cs index b091f528ef..4565c1ac39 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/Mixins/PageMetadataMixin.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/Mixins/PageMetadataMixin.cs @@ -23,7 +23,7 @@ public struct PageMetadataMixin : IModFeedItem public EntityId GetModPageEntityId() => _metadata.Id; /// - public DateTime GetLastUpdatedDateUtc() => _metadata.UpdatedAt; // <= TODO: Change this with 'last file updated at' when V2 supports this field. + public DateTimeOffset GetLastUpdatedDateUtc() => _metadata.UpdatedAt; // <= TODO: Change this with 'last file updated at' when V2 supports this field. /// /// Returns the database entries containing page metadata(s) as a mixin. diff --git a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs index 56b99dda2b..8086b465d0 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs @@ -62,8 +62,7 @@ public PerFeedCacheUpdater(TUpdateableItem[] items, TimeSpan expiry) _itemToIndex[_items[x].GetModPageId().ModId] = x; // Set the action to refresh cache for any mods which exceed max age. - var utcNow = DateTime.UtcNow; - var minCachedDate = utcNow - expiry; + var minCachedDate = TimeProvider.System.GetLocalNow()- expiry; for (var x = 0; x < _items.Length; x++) { if (_items[x].GetLastUpdatedDateUtc() < minCachedDate) diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/Auth/JWTToken.cs b/src/Networking/NexusMods.Networking.NexusWebApi/Auth/JWTToken.cs index 3fc12123fb..663e03184a 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/Auth/JWTToken.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/Auth/JWTToken.cs @@ -63,7 +63,7 @@ public static Optional Create(IDb db, ITransaction tx, JwtTokenReply r tx.Add(entityId, AccessToken, reply.AccessToken); tx.Add(entityId, RefreshToken, reply.RefreshToken); - tx.Add(entityId, ExpiresAt, DateTimeOffset.FromUnixTimeSeconds(reply.CreatedAt).DateTime + TimeSpan.FromSeconds(reply.ExpiresIn)); + tx.Add(entityId, ExpiresAt, DateTimeOffset.FromUnixTimeSeconds(reply.CreatedAt) + TimeSpan.FromSeconds(reply.ExpiresIn)); return entityId; } diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs index 367cfe5413..6d4f498655 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs @@ -39,7 +39,7 @@ public static EntityId Resolve(this IModFileFragment modFileFragment, IDb db, IT nexusFileResolver.Add(NexusModsFileMetadata.ModPageId, modPageEid); nexusFileResolver.Add(NexusModsFileMetadata.Name, modFileFragment.Name); nexusFileResolver.Add(NexusModsFileMetadata.Version, modFileFragment.Version); - nexusFileResolver.Add(NexusModsFileMetadata.UploadedAt, DateTimeOffset.FromUnixTimeSeconds(modFileFragment.Date).DateTime); + nexusFileResolver.Add(NexusModsFileMetadata.UploadedAt, DateTimeOffset.FromUnixTimeSeconds(modFileFragment.Date)); if (ulong.TryParse(modFileFragment.SizeInBytes, out var size)) { @@ -62,7 +62,7 @@ public static EntityId Resolve(this IModFragment modFragment, IDb db, ITransacti var nexusModResolver = GraphQLResolver.Create(db, tx, NexusModsModPageMetadata.Uid, UidForMod.FromV2Api(modFragment.Uid)); nexusModResolver.Add(NexusModsModPageMetadata.Name, modFragment.Name); nexusModResolver.Add(NexusModsModPageMetadata.GameDomain, GameDomain.From(modFragment.Game.DomainName)); - nexusModResolver.Add(NexusModsModPageMetadata.UpdatedAt, modFragment.UpdatedAt.UtcDateTime); + nexusModResolver.Add(NexusModsModPageMetadata.UpdatedAt, modFragment.UpdatedAt); if (Uri.TryCreate(modFragment.PictureUrl, UriKind.Absolute, out var fullSizedPictureUri)) nexusModResolver.Add(NexusModsModPageMetadata.FullSizedPictureUri, fullSizedPictureUri); diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs index f36fb01a55..89514295c0 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs @@ -105,10 +105,10 @@ public async Task> DownloadLinksAsync(string domain, Mo /// /// Currently available for Premium users only; with some minor exceptions [nxm links]. /// - public async Task> DownloadLinksAsync(string domain, ModId modId, FileId fileId, NXMKey key, DateTime expireTime, CancellationToken token = default) + public async Task> DownloadLinksAsync(string domain, ModId modId, FileId fileId, NXMKey key, DateTimeOffset expireTime, CancellationToken token = default) { var msg = await _factory.Create(HttpMethod.Get, new Uri( - $"https://api.nexusmods.com/v1/games/{domain}/mods/{modId}/files/{fileId}/download_link.json?key={key}&expires={new DateTimeOffset(expireTime).ToUnixTimeSeconds()}")); + $"https://api.nexusmods.com/v1/games/{domain}/mods/{modId}/files/{fileId}/download_link.json?key={key}&expires={expireTime.ToUnixTimeSeconds()}")); return await SendAsyncArray(msg, token); } diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs index 1f9627c88c..c83bcc2ed1 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs @@ -105,7 +105,7 @@ public NexusModsLibrary(IServiceProvider serviceProvider) Version = fileNode.Version, ModPageId = modPage, Uid = uid, - UploadedAt = DateTimeOffset.FromUnixTimeSeconds(fileNode.Date).UtcDateTime, + UploadedAt = DateTimeOffset.FromUnixTimeSeconds(fileNode.Date), Size = size, }; @@ -115,7 +115,7 @@ public NexusModsLibrary(IServiceProvider serviceProvider) public async Task GetDownloadUri( NexusModsFileMetadata.ReadOnly file, - Optional<(NXMKey, DateTime)> nxmData, + Optional<(NXMKey, DateTimeOffset)> nxmData, CancellationToken cancellationToken = default) { Abstractions.NexusWebApi.DTOs.Response links; @@ -159,7 +159,7 @@ public async Task> CreateDownloadJo IGameDomainToGameIdMappingCache mappingCache, CancellationToken cancellationToken) { - var nxmData = url.Key is not null && url.ExpireTime is not null ? (url.Key.Value, url.ExpireTime.Value) : Optional.None<(NXMKey, DateTime)>(); + var nxmData = url.Key is not null && url.ExpireTime is not null ? (url.Key.Value, url.ExpireTime.Value) : Optional.None<(NXMKey, DateTimeOffset)>(); var gameId = (await mappingCache.TryGetIdAsync(GameDomain.From(url.Game), cancellationToken)).Value; return await CreateDownloadJob(destination, gameId, url.ModId, url.FileId, nxmData, cancellationToken); } @@ -172,7 +172,7 @@ public async Task> CreateDownloadJo GameId gameId, ModId modId, FileId fileId, - Optional<(NXMKey, DateTime)> nxmData = default, + Optional<(NXMKey, DateTimeOffset)> nxmData = default, CancellationToken cancellationToken = default) { var modPage = await GetOrAddModPage(modId, gameId, cancellationToken); diff --git a/src/NexusMods.App.UI/Controls/LoadoutCard/Standard/LoadoutCardViewModel.cs b/src/NexusMods.App.UI/Controls/LoadoutCard/Standard/LoadoutCardViewModel.cs index 556fe32216..21b561a4fb 100644 --- a/src/NexusMods.App.UI/Controls/LoadoutCard/Standard/LoadoutCardViewModel.cs +++ b/src/NexusMods.App.UI/Controls/LoadoutCard/Standard/LoadoutCardViewModel.cs @@ -126,7 +126,7 @@ private static string FormatCreatedTime(DateTimeOffset creationTime) private static string FormatLastAppliedTime(DateTimeOffset lastAppliedTime) { - var stringTime = lastAppliedTime == DateTime.MinValue ? Language.HumanizedDateTime_Never : lastAppliedTime.Humanize(); + var stringTime = lastAppliedTime == DateTimeOffset.MinValue ? Language.HumanizedDateTime_Never : lastAppliedTime.Humanize(); return string.Format(Language.LoadoutCardViewModel_FormatLastAppliedTime_Last_applied__0_, stringTime); } private async Task LoadImage(GameInstallation source) diff --git a/src/NexusMods.App.UI/Helpers/DoubleClickHelper.cs b/src/NexusMods.App.UI/Helpers/DoubleClickHelper.cs index ea2996843a..6073349d86 100644 --- a/src/NexusMods.App.UI/Helpers/DoubleClickHelper.cs +++ b/src/NexusMods.App.UI/Helpers/DoubleClickHelper.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using JetBrains.Annotations; +using LiveChartsCore.Defaults; namespace NexusMods.App.UI.Helpers; @@ -61,7 +62,7 @@ private class DoubleClickSubject : ADoubleClickSubject { - public ReactiveProperty InstalledAt { get; } = new(DateTime.UnixEpoch); + public ReactiveProperty InstalledAt { get; } = new(DateTimeOffset.UnixEpoch); public IObservable NameObservable { get; init; } = System.Reactive.Linq.Observable.Return("-"); public BindableReactiveProperty Name { get; } = new("-"); @@ -31,7 +31,7 @@ public class LoadoutItemModel : TreeDataGridItemModel GetLoadoutItemIds() => _fixedId; - public Observable? Ticker { get; set; } + public Observable? Ticker { get; set; } public BindableReactiveProperty FormattedInstalledAt { get; } = new("-"); private readonly IDisposable _modelActivationDisposable; @@ -52,7 +52,7 @@ public LoadoutItemModel(LoadoutItemId loadoutItemId) Debug.Assert(model.Ticker is not null, "should've been set before activation"); model.Ticker.Subscribe(model, static (now, model) => { - model.FormattedInstalledAt.Value = FormatDate(now, model.InstalledAt.Value); + model.FormattedInstalledAt.Value = model.InstalledAt.Value.FormatDate(now); }).AddTo(disposables); model.NameObservable.OnUI().Subscribe(name => model.Name.Value = name).AddTo(disposables); @@ -60,16 +60,10 @@ public LoadoutItemModel(LoadoutItemId loadoutItemId) model.SizeObservable.OnUI().Subscribe(size => model.ItemSize.Value = size).AddTo(disposables); model.IsEnabledObservable.OnUI().Subscribe(isEnabled => model.IsEnabled.Value = isEnabled).AddTo(disposables); - model.InstalledAt.Subscribe(model, static (date, model) => model.FormattedInstalledAt.Value = FormatDate(DateTime.Now, date)).AddTo(disposables); + model.InstalledAt.Subscribe(model, static (date, model) => model.FormattedInstalledAt.Value = date.FormatDate(TimeProvider.System.GetLocalNow())).AddTo(disposables); }); } - private static string FormatDate(DateTimeOffset now, DateTimeOffset date) - { - if (date == DateTime.UnixEpoch || date == default(DateTime)) return "-"; - return date.Humanize(dateToCompareAgainst: now > date ? now : DateTime.Now); - } - private bool _isDisposed; protected override void Dispose(bool disposing) { @@ -162,8 +156,8 @@ public static IColumn CreateInstalledAtColumn() getter: model => model.FormattedInstalledAt.Value, options: new TextColumnOptions { - CompareAscending = static (a, b) => a?.InstalledAt.Value.CompareTo(b?.InstalledAt.Value ?? DateTime.UnixEpoch) ?? 1, - CompareDescending = static (a, b) => b?.InstalledAt.Value.CompareTo(a?.InstalledAt.Value ?? DateTime.UnixEpoch) ?? 1, + CompareAscending = static (a, b) => a?.InstalledAt.Value.CompareTo(b?.InstalledAt.Value ?? DateTimeOffset.MinValue) ?? 1, + CompareDescending = static (a, b) => b?.InstalledAt.Value.CompareTo(a?.InstalledAt.Value ?? DateTimeOffset.MinValue) ?? 1, IsTextSearchEnabled = false, CanUserResizeColumn = true, CanUserSortColumn = true, diff --git a/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs b/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs index e5dd9112bd..57f15046fb 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs +++ b/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs @@ -41,8 +41,8 @@ public LoadoutViewModel(IWindowManager windowManager, IServiceProvider servicePr var ticker = Observable .Interval(period: TimeSpan.FromSeconds(30), timeProvider: ObservableSystem.DefaultTimeProvider) .ObserveOnUIThreadDispatcher() - .Select(_ => DateTime.Now) - .Publish(initialValue: DateTime.Now); + .Select(_ => TimeProvider.System.GetLocalNow()) + .Publish(initialValue: TimeProvider.System.GetLocalNow()); var loadoutFilter = new LoadoutFilter { @@ -188,7 +188,7 @@ public class LoadoutTreeDataGridAdapter : TreeDataGridAdapter { private readonly ILoadoutDataProvider[] _loadoutDataProviders; - private readonly ConnectableObservable _ticker; + private readonly ConnectableObservable _ticker; private readonly IConnection _connection; private readonly LoadoutFilter _loadoutFilter; @@ -196,7 +196,7 @@ public class LoadoutTreeDataGridAdapter : TreeDataGridAdapter _commandDisposables = new(); private readonly IDisposable _activationDisposable; - public LoadoutTreeDataGridAdapter(IServiceProvider serviceProvider, ConnectableObservable ticker, LoadoutFilter loadoutFilter) + public LoadoutTreeDataGridAdapter(IServiceProvider serviceProvider, ConnectableObservable ticker, LoadoutFilter loadoutFilter) { _loadoutFilter = loadoutFilter; _ticker = ticker; diff --git a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs index 3317d01ef8..33d726d20a 100644 --- a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs +++ b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs @@ -5,14 +5,14 @@ namespace NexusMods.Networking.ModUpdates.Tests.Helpers; // Helper class to simulate updateable items public class TestItem : IModFeedItem { - public DateTime LastUpdated { get; set; } + public DateTimeOffset LastUpdated { get; set; } public UidForMod Uid { get; set; } - public DateTime GetLastUpdatedDateUtc() => LastUpdated; + public DateTimeOffset GetLastUpdatedDateUtc() => LastUpdated; public UidForMod GetModPageId() => Uid; // Helper method to create a test item - public static TestItem Create(uint gameId, uint modId, DateTime lastUpdated) + public static TestItem Create(uint gameId, uint modId, DateTimeOffset lastUpdated) { return new TestItem { diff --git a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs index 63a8b7749b..9f20c65f05 100644 --- a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs +++ b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs @@ -23,7 +23,7 @@ public void Constructor_ShouldSetOldItemsToNeedUpdate() // should be marked as 'Out of Date' across multiple feeds. // Arrange - var now = DateTime.UtcNow; + var now = TimeProvider.System.GetLocalNow(); var items = new[] { Create(1, 1, now.AddDays(-40)), @@ -52,7 +52,7 @@ public void Update_ShouldMarkMissingItemsAsUndetermined() // under this category across multiple feeds. // Arrange - var now = DateTime.UtcNow; + var now = TimeProvider.System.GetLocalNow(); var items = new[] { Create(1, 1, now.AddDays(-10)), @@ -90,7 +90,7 @@ public void Update_ShouldMarkItemsAsUpToDateOrNeedingUpdateAcrossMultipleFeeds() // across multiple feeds. // Arrange - var now = DateTime.UtcNow; + var now = TimeProvider.System.GetLocalNow(); var items = new[] { Create(1, 1, now.AddDays(-10)), @@ -131,7 +131,7 @@ public void Update_HavingExtraItemsInUpdatePayloadHasNoSideEffects() // therefore they should be ignored without side effects. // Arrange - var now = DateTime.UtcNow; + var now = TimeProvider.System.GetLocalNow(); var items = new[] { Create(1, 1, now.AddDays(-10)), @@ -176,7 +176,7 @@ public void Update_ShouldHandleUpdatesFromDifferentFeeds() // from different feeds separately. // Arrange - var now = DateTime.UtcNow; + var now = TimeProvider.System.GetLocalNow(); var items = new[] { Create(1, 1, now.AddDays(-10)),