diff --git a/Source/LibationAvalonia/ViewModels/GridEntry.cs b/Source/LibationAvalonia/ViewModels/GridEntry.cs index f9fd3fd6..cf21da7c 100644 --- a/Source/LibationAvalonia/ViewModels/GridEntry.cs +++ b/Source/LibationAvalonia/ViewModels/GridEntry.cs @@ -43,6 +43,7 @@ namespace LibationAvalonia.ViewModels private string _narrators; private string _category; private string _misc; + private LastDownloadStatus _lastDownload; private string _description; private Rating _productrating; protected Rating _myRating; @@ -55,6 +56,7 @@ namespace LibationAvalonia.ViewModels public string Authors { get => _authors; protected set => this.RaiseAndSetIfChanged(ref _authors, value); } public string Narrators { get => _narrators; protected set => this.RaiseAndSetIfChanged(ref _narrators, value); } public string Category { get => _category; protected set => this.RaiseAndSetIfChanged(ref _category, value); } + public LastDownloadStatus LastDownload { get => _lastDownload; protected set => this.RaiseAndSetIfChanged(ref _lastDownload, value); } public string Misc { get => _misc; protected set => this.RaiseAndSetIfChanged(ref _misc, value); } public string Description { get => _description; protected set => this.RaiseAndSetIfChanged(ref _description, value); } public Rating ProductRating { get => _productrating; protected set => this.RaiseAndSetIfChanged(ref _productrating, value); } @@ -122,6 +124,7 @@ namespace LibationAvalonia.ViewModels { typeof(bool), new ObjectComparer() }, { typeof(DateTime), new ObjectComparer() }, { typeof(LiberateButtonStatus), new ObjectComparer() }, + { typeof(LastDownloadStatus), new ObjectComparer() }, }; #endregion diff --git a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs b/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs index b28f8704..8b823191 100644 --- a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs +++ b/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs @@ -87,6 +87,7 @@ namespace LibationAvalonia.ViewModels Narrators = Book.NarratorNames(); Category = string.Join(" > ", Book.CategoriesNames()); Misc = GetMiscDisplay(libraryBook); + LastDownload = new(Book.UserDefinedItem); LongDescription = GetDescriptionDisplay(Book); Description = TrimTextToWord(LongDescription, 62); SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; @@ -127,6 +128,10 @@ namespace LibationAvalonia.ViewModels _pdfStatus = udi.PdfStatus; this.RaisePropertyChanged(nameof(Liberate)); break; + case nameof(udi.LastDownloaded): + LastDownload = new(udi); + this.RaisePropertyChanged(nameof(LastDownload)); + break; } } @@ -149,6 +154,7 @@ namespace LibationAvalonia.ViewModels { nameof(Description), () => Description }, { nameof(Category), () => Category }, { nameof(Misc), () => Misc }, + { nameof(LastDownload), () => LastDownload }, { nameof(BookTags), () => BookTags?.Tags ?? string.Empty }, { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded }, diff --git a/Source/LibationAvalonia/ViewModels/SeriesEntry.cs b/Source/LibationAvalonia/ViewModels/SeriesEntry.cs index 6dbfcfb5..88d23d2f 100644 --- a/Source/LibationAvalonia/ViewModels/SeriesEntry.cs +++ b/Source/LibationAvalonia/ViewModels/SeriesEntry.cs @@ -96,6 +96,7 @@ namespace LibationAvalonia.ViewModels Narrators = Book.NarratorNames(); Category = string.Join(" > ", Book.CategoriesNames()); Misc = GetMiscDisplay(LibraryBook); + LastDownload = new(); LongDescription = GetDescriptionDisplay(Book); Description = TrimTextToWord(LongDescription, 62); @@ -124,6 +125,7 @@ namespace LibationAvalonia.ViewModels { nameof(Description), () => Description }, { nameof(Category), () => Category }, { nameof(Misc), () => Misc }, + { nameof(LastDownload), () => LastDownload }, { nameof(BookTags), () => BookTags?.Tags ?? string.Empty }, { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded }, diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml index 87dfb645..507a225d 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml @@ -183,6 +183,16 @@ + + + + + + + + + + diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs index 70f85e78..7997f7c7 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs @@ -306,6 +306,12 @@ namespace LibationAvalonia.Views imageDisplayDialog.Close(); } + public void Version_DoubleClick(object sender, Avalonia.Input.TappedEventArgs args) + { + if (sender is Control panel && panel.DataContext is LibraryBookEntry lbe && lbe.LastDownload.IsValid) + lbe.LastDownload.OpenReleaseUrl(); + } + public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args) { if (sender is not Image tblock || tblock.DataContext is not GridEntry gEntry) diff --git a/Source/LibationUiBase/LastDownloadStatus.cs b/Source/LibationUiBase/LastDownloadStatus.cs new file mode 100644 index 00000000..eddba0e1 --- /dev/null +++ b/Source/LibationUiBase/LastDownloadStatus.cs @@ -0,0 +1,41 @@ +using DataLayer; +using System; + +namespace LibationUiBase +{ + public class LastDownloadStatus : IComparable + { + public bool IsValid => LastDownloadedVersion is not null && LastDownloaded.HasValue; + public Version LastDownloadedVersion { get; } + public DateTime? LastDownloaded { get; } + public string ToolTipText => IsValid ? $"Double click to open v{LastDownloadedVersion.ToString(3)} release notes" : ""; + + public LastDownloadStatus() { } + public LastDownloadStatus(UserDefinedItem udi) + { + LastDownloadedVersion = udi.LastDownloadedVersion; + LastDownloaded = udi.LastDownloaded; + } + + public void OpenReleaseUrl() + { + if (IsValid) + Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{LastDownloadedVersion.ToString(3)}"); + } + + public override string ToString() + => IsValid ? $"{dateString()}\n\nLibation v{LastDownloadedVersion.ToString(3)}" : ""; + + //Call ToShortDateString to use current culture's date format. + private string dateString() => $"{LastDownloaded.Value.ToShortDateString()} {LastDownloaded.Value:HH:mm}"; + + public int CompareTo(object obj) + { + if (obj is not LastDownloadStatus second) return -1; + else if (IsValid && !second.IsValid) return -1; + else if (!IsValid && second.IsValid) return 1; + else if (!IsValid && !second.IsValid) return 0; + else return LastDownloaded.Value.CompareTo(second.LastDownloaded.Value); + } + } +} diff --git a/Source/LibationWinForms/GridView/AsyncNotifyPropertyChanged.cs b/Source/LibationWinForms/GridView/AsyncNotifyPropertyChanged.cs index e4532cfe..f02dd26e 100644 --- a/Source/LibationWinForms/GridView/AsyncNotifyPropertyChanged.cs +++ b/Source/LibationWinForms/GridView/AsyncNotifyPropertyChanged.cs @@ -12,6 +12,6 @@ namespace LibationWinForms.GridView // per standard INotifyPropertyChanged pattern: // https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-property-change-notification public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - => this.UIThreadAsync(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName))); + => this.UIThreadSync(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName))); } } diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index 026c5ad5..95ae7128 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -59,6 +59,7 @@ namespace LibationWinForms.GridView public string Narrators { get; protected set; } public string Category { get; protected set; } public string Misc { get; protected set; } + public virtual LastDownloadStatus LastDownload { get; protected set; } = new(); public string Description { get; protected set; } public string ProductRating { get; protected set; } protected Rating _myRating; @@ -120,6 +121,7 @@ namespace LibationWinForms.GridView { typeof(bool), new ObjectComparer() }, { typeof(DateTime), new ObjectComparer() }, { typeof(LiberateButtonStatus), new ObjectComparer() }, + { typeof(LastDownloadStatus), new ObjectComparer() }, }; #endregion diff --git a/Source/LibationWinForms/GridView/LastDownloadedGridViewColumn.cs b/Source/LibationWinForms/GridView/LastDownloadedGridViewColumn.cs new file mode 100644 index 00000000..c970f57d --- /dev/null +++ b/Source/LibationWinForms/GridView/LastDownloadedGridViewColumn.cs @@ -0,0 +1,39 @@ +using LibationUiBase; +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace LibationWinForms.GridView +{ + public class LastDownloadedGridViewColumn : DataGridViewColumn + { + public LastDownloadedGridViewColumn() : base(new LastDownloadedGridViewCell()) { } + public override DataGridViewCell CellTemplate + { + get => base.CellTemplate; + set + { + if (value is not LastDownloadedGridViewCell) + throw new InvalidCastException($"Must be a {nameof(LastDownloadedGridViewCell)}"); + + base.CellTemplate = value; + } + } + } + + internal class LastDownloadedGridViewCell : DataGridViewTextBoxCell + { + private LastDownloadStatus LastDownload => (LastDownloadStatus)Value; + protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) + { + ToolTipText = ((LastDownloadStatus)value).ToolTipText; + base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts); + } + + protected override void OnDoubleClick(DataGridViewCellEventArgs e) + { + LastDownload.OpenReleaseUrl(); + base.OnDoubleClick(e); + } + } +} diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs index ce029c79..a29320c7 100644 --- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs +++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs @@ -1,6 +1,7 @@ using ApplicationServices; using DataLayer; using Dinah.Core; +using LibationUiBase; using System; using System.Collections.Generic; using System.ComponentModel; @@ -24,6 +25,8 @@ namespace LibationWinForms.GridView private LiberatedStatus _bookStatus; private LiberatedStatus? _pdfStatus; + public override LastDownloadStatus LastDownload { get; protected set; } + public override RemoveStatus Remove { get @@ -87,6 +90,7 @@ namespace LibationWinForms.GridView Narrators = Book.NarratorNames(); Category = string.Join(" > ", Book.CategoriesNames()); Misc = GetMiscDisplay(libraryBook); + LastDownload = new(Book.UserDefinedItem); LongDescription = GetDescriptionDisplay(Book); Description = TrimTextToWord(LongDescription, 62); SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; @@ -126,6 +130,10 @@ namespace LibationWinForms.GridView _pdfStatus = udi.PdfStatus; NotifyPropertyChanged(nameof(Liberate)); break; + case nameof(udi.LastDownloaded): + LastDownload = new(udi); + NotifyPropertyChanged(nameof(LastDownload)); + break; } } @@ -153,6 +161,7 @@ namespace LibationWinForms.GridView { nameof(Description), () => Description }, { nameof(Category), () => Category }, { nameof(Misc), () => Misc }, + { nameof(LastDownload), () => LastDownload }, { nameof(DisplayTags), () => DisplayTags }, { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded }, diff --git a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs index 15d3bd2d..55f0154d 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs @@ -45,6 +45,7 @@ this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.myRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn(); this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.lastDownloadedGVColumn = new LastDownloadedGridViewColumn(); this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn(); this.showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components); @@ -75,7 +76,8 @@ this.purchaseDateGVColumn, this.myRatingGVColumn, this.miscGVColumn, - this.tagAndDetailsGVColumn}); + this.lastDownloadedGVColumn, + this.tagAndDetailsGVColumn}); this.gridEntryDataGridView.ContextMenuStrip = this.showHideColumnsContextMenuStrip; this.gridEntryDataGridView.DataSource = this.syncBindingSource; dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; @@ -216,6 +218,15 @@ this.miscGVColumn.ReadOnly = true; this.miscGVColumn.Width = 135; // + // lastDownloadedGVColumn + // + this.lastDownloadedGVColumn.DataPropertyName = "LastDownload"; + this.lastDownloadedGVColumn.HeaderText = "Last Download"; + this.lastDownloadedGVColumn.Name = "lastDownloadedGVColumn"; + this.lastDownloadedGVColumn.ReadOnly = true; + this.lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; + this.lastDownloadedGVColumn.Width = 108; + // // tagAndDetailsGVColumn // this.tagAndDetailsGVColumn.DataPropertyName = "DisplayTags"; @@ -268,6 +279,7 @@ private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGVColumn; private MyRatingGridViewColumn myRatingGVColumn; private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn; + private LastDownloadedGridViewColumn lastDownloadedGVColumn; private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn; } } diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index f8d004f9..cf99c96b 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -122,6 +122,7 @@ namespace LibationWinForms.GridView { nameof(Description), () => Description }, { nameof(Category), () => Category }, { nameof(Misc), () => Misc }, + { nameof(LastDownload), () => LastDownload }, { nameof(DisplayTags), () => string.Empty }, { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded },