' Copyright (C) 2023 Andy https://github.com/AAndyProgram ' This program is free software: you can redistribute it and/or modify ' it under the terms of the GNU General Public License as published by ' the Free Software Foundation, either version 3 of the License, or ' (at your option) any later version. ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Threading Imports SCrawler.API.Base Imports SCrawler.DownloadObjects.Groups Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.XML.Base Imports PersonalUtilities.Tools Imports PersonalUtilities.Tools.Notifications Namespace DownloadObjects Friend Class AutoDownloader : Inherits GroupParameters : Implements IIndexable, IEContainerProvider, IComparable(Of AutoDownloader) Friend Event PauseChanged(ByVal Value As PauseModes) Friend Event PlanChanged As Scheduler.PlanChangedEventHandler Friend Const NoPauseMode As Integer = -100 Friend Enum PauseModes As Integer Disabled = -2 Enabled = -1 Unlimited = -1 Until = 0 H1 = 1 H2 = 2 H3 = 3 H4 = 4 H6 = 6 H12 = 12 End Enum Friend Const DefaultTimer As Integer = 60 #Region "Notifications" Private Const KeyOpenFolder As String = "_____OPEN_FOLDER_SCRAWLER_AUTOMATION" Private Const KeyOpenSite As String = "_____OPEN_SITE_SCRAWLER_AUTOMATION" Private Const KeyOpenFeed As String = "_____OPEN_FEED_SCRAWLER_AUTOMATION" Private Const KeyBttDismiss As String = "_____DISMISS_SCRAWLER_AUTOMATION" Private Const KeyBttPhoto As String = "_____PHOTO_SCRAWLER_AUTOMATION" Private ReadOnly UserKeys As List(Of NotifiedUser) Private Class NotifiedUser : Implements IDisposable Private ReadOnly Property User As IUserData Friend ReadOnly Property IUserDataKey As String Private ReadOnly Property Key As String Private ReadOnly Property KeyFolder As String Private ReadOnly Property KeySite As String Private ReadOnly Property KeyFeed As String Private ReadOnly Property KeyDismiss As String Private ReadOnly Property Images As Dictionary(Of String, SFile) Private ReadOnly Property AutoDownloaderSource As AutoDownloader Private Sub New() Images = New Dictionary(Of String, SFile) End Sub Private Sub New(ByVal _Key As String) Me.New Key = _Key KeyFolder = $"{Key}{KeyOpenFolder}" KeySite = $"{Key}{KeyOpenSite}" KeyFeed = $"{Key}{KeyOpenFeed}" KeyDismiss = $"{Key}{KeyBttDismiss}" End Sub Friend Sub New(ByVal _Key As String, ByRef _User As IUserData, ByRef Source As AutoDownloader) Me.New(_Key) User = _User IUserDataKey = _User.Key AutoDownloaderSource = Source If _User.IncludedInCollection Then Dim cn$ = _User.CollectionName Dim i% = Settings.Users.FindIndex(Function(u) u.IsCollection And u.Name = cn) If i >= 0 Then IUserDataKey = Settings.Users(i).Key End If End Sub Public Shared Widening Operator CType(ByVal Key As String) As NotifiedUser Return New NotifiedUser(Key) End Operator Friend Sub ShowNotification() Try If Not AutoDownloaderSource Is Nothing And Settings.ProcessNotification(SettingsCLS.NotificationObjects.AutoDownloader) Then If AutoDownloaderSource.ShowNotifications Then If Not User Is Nothing Then Dim Text$ = $"{IIf(User.IsSubscription, "[Subscription] ", String.Empty)}{User.Site} - {User.Name}{vbNewLine}" & $"Downloaded: {User.DownloadedPictures(False)} images, {User.DownloadedVideos(False)} videos" Dim Title$ If Not User.CollectionName.IsEmptyString Then Title = User.CollectionName Else Title = User.ToString End If Using Notify As New Notification(Text, Title) With {.Key = Key} Dim uPic As SFile = Nothing Dim uif As SFile = Nothing Dim uif_orig As SFile = Nothing Dim uif_compressed As SFile = Nothing Dim uifKey$ = String.Empty If AutoDownloaderSource.ShowPictureUser Then uPic = DirectCast(User, UserDataBase).GetUserPictureToastAddress If AutoDownloaderSource.ShowPictureUser AndAlso uPic.Exists Then Notify.Images = {New ToastImage(uPic)} If AutoDownloaderSource.ShowPictureDownloaded And User.DownloadedPictures(False) > 0 And Not User.IsSubscription Then uif = DirectCast(User, UserDataBase).GetLastImageAddress uif_orig = uif If uif.Exists Then uif_compressed = uif uif_compressed.Path = Settings.Cache.RootDirectory.Path uif_compressed.Name = $"360_{uif.Name}" Using imgR As New ImageRenderer(uif, EDP.SendToLog) Try : imgR.FitToWidth(360).Save(uif_compressed) : Catch : End Try End Using If uif_compressed.Exists Then uif = uif_compressed If uif.Exists Then Notify.Images = {New ToastImage(uif, IImage.Modes.Inline)} uifKey = $"{Key}_{Images.Keys.Count + 1}_{KeyBttPhoto}" If Not Images.ContainsKey(uifKey) Then Images.Add(uifKey, uif_orig) End If End If End If Notify.Buttons = { New ToastButton(KeyFolder, "Folder"), New ToastButton(KeySite, "Site"), New ToastButton(KeyFeed, "Feed") } If Not uifKey.IsEmptyString Then Notify.Buttons = {New ToastButton(uifKey, "Photo")} Notify.Buttons = {New ToastButton(KeyDismiss, "Dismiss")} Notify.Show() End Using End If End If End If Catch ex As Exception ErrorsDescriber.Execute(EDP.SendToLog, ex, "[AutoDownloader.NotifiedUser.ShowNotification]") If Not User Is Nothing Then MainFrameObj.ShowNotification(SettingsCLS.NotificationObjects.AutoDownloader, If(User.IsSubscription, "[Subscription] ", String.Empty) & User.ToString & vbNewLine & $"Downloaded: {User.DownloadedPictures(False)} images, {User.DownloadedVideos(False)} videos" & If(User.HasError, vbNewLine & "With errors", String.Empty)) End If End Try End Sub ''' True to activate Friend Function Open(ByVal _Key As String) As Boolean If Not User Is Nothing Then If KeyDismiss = _Key Then ElseIf Key = _Key Then Return True ElseIf KeyFolder = _Key Then If User.IsSubscription Then Return True Else User.OpenFolder() End If ElseIf KeySite = _Key Then User.OpenSite() ElseIf KeyFeed = _Key Then With MainFrameObj : ControlInvokeFast(.MF, AddressOf .MF.ShowFeed, EDP.LogMessageValue) : End With ElseIf Images.ContainsKey(_Key) Then Images(_Key).Open() End If Else Return True End If Return False End Function Public Overrides Function Equals(ByVal Obj As Object) As Boolean With CType(Obj, NotifiedUser) Return .Key = Key Or .Key = KeyFolder Or .Key = KeySite Or .Key = KeyFeed Or .Key = KeyDismiss Or Images.ContainsKey(.Key) End With End Function #Region "IDisposable Support" Private disposedValue As Boolean = False Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean) If Not disposedValue Then If disposing Then Images.Clear() disposedValue = True End If End Sub Protected Overrides Sub Finalize() Dispose(False) MyBase.Finalize() End Sub Friend Overloads Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region End Class #End Region #Region "XML Names" 'TODELETE: AutoDownloader.Modes Private Const Name_Mode As String = "Mode" Private Const Name_Enabled As String = "Enabled" Private Const Name_IsManual As String = "IsManual" Private Const Name_Timer As String = "Timer" Private Const Name_StartupDelay As String = "StartupDelay" Private Const Name_LastDownloadDate As String = "LastDownloadDate" Private Const Name_ShowNotifications As String = "Notify" Private Const Name_ShowPictureDown As String = "ShowDownloadedPicture" Private Const Name_ShowPictureUser As String = "ShowUserPicture" Private Const Name_ShowSimpleNotification As String = "ShowSimpleNotification" #End Region #Region "Declarations" Private _LVIState As Integer = 0 Friend Function LVIStateChanged() As Boolean Dim ____LVIState% Dim result As Boolean If Downloading Then ____LVIState = 1 ElseIf DownloadReady(True, True) Or ForceStartRequested Then ____LVIState = 2 ElseIf Not Working And Not IsManual Then ____LVIState = 3 ElseIf IsPaused Then ____LVIState = 4 ElseIf Settings.AutomationBrushUndownloadedPlansMinutes.Value > 0 AndAlso LastDownloadDate.AddMinutes(Settings.AutomationBrushUndownloadedPlansMinutes.Value) < Now Then ____LVIState = 5 Else ____LVIState = 0 End If result = Not _LVIState = ____LVIState _LVIState = ____LVIState Return result End Function Friend ReadOnly Property LVI As ListViewItem Get Dim l As New ListViewItem(ToString) If Downloading Then l.BackColor = MyColor.OkBack l.ForeColor = MyColor.OkFore ElseIf DownloadReady(True, True) Or ForceStartRequested Then l.BackColor = MyColor.EditBack l.ForeColor = MyColor.EditFore ElseIf Not Working And Not IsManual Then l.BackColor = MyColor.DeleteBack l.ForeColor = MyColor.DeleteFore ElseIf Working And IsPaused And Not IsManual Then l.BackColor = MyColor.UpdateBack l.ForeColor = MyColor.UpdateFore ElseIf Settings.AutomationBrushUndownloadedPlansMinutes.Value > 0 AndAlso LastDownloadDate.AddMinutes(Settings.AutomationBrushUndownloadedPlansMinutes.Value) < Now Then l.BackColor = Color.FromArgb(224, 224, 224) l.ForeColor = Color.FromArgb(64, 64, 64) End If LVIStateChanged() Return l End Get End Property Friend Property Source As Scheduler Private _Enabled As Boolean = False Friend Property Enabled As Boolean Get Return _Enabled End Get Set(ByVal e As Boolean) _Enabled = e If Not _Enabled Then [Stop]() End Set End Property Friend Property IsManual As Boolean = False Friend Property Timer As Integer = DefaultTimer Friend Property StartupDelay As Integer = 1 Friend Property ShowNotifications As Boolean = True Friend Property ShowPictureDownloaded As Boolean = True Friend Property ShowPictureUser As Boolean = True Friend Property ShowSimpleNotification As Boolean = False Friend Property Index As Integer = -1 Implements IIndexable.Index Private Function SetIndex(ByVal Obj As Object, ByVal Index As Integer) As Object Implements IIndexable.SetIndex DirectCast(Obj, AutoDownloader).Index = Index Return Obj End Function #Region "Date" Private ReadOnly LastDownloadDateXML As Date? = Nothing Private _LastDownloadDate As Date = Now.AddYears(-1) Private _LastDownloadDateChanged As Boolean = False Private _LastDownloadDateSkip As Date? = Nothing Friend Property LastDownloadDate As Date Get Return _LastDownloadDate End Get Set(ByVal d As Date) _LastDownloadDate = d If Not Initialization Then _LastDownloadDateChanged = True End Set End Property Private ReadOnly Property NextExecutionDate As Date Get Dim lds As Date = If(_LastDownloadDateSkip, Date.MinValue) If _PauseValue.HasValue Then Return {LastDownloadDate.AddMinutes(Timer), _StartTime.AddMinutes(StartupDelay), _PauseValue.Value, lds}.Max Else Return {LastDownloadDate.AddMinutes(Timer), _StartTime.AddMinutes(StartupDelay), lds}.Max End If End Get End Property Private ReadOnly DateProvider As New ADateTime(ADateTime.Formats.BaseDateTime) Private Function GetLastDateString() As String If LastDownloadDateXML.HasValue Or _LastDownloadDateChanged Then Return LastDownloadDate.ToStringDate(DateProvider) Else Return "never" End If End Function Private Function GetNextDateString() As String Return NextExecutionDate.ToStringDate(DateProvider) End Function #End Region #Region "Information" Friend ReadOnly Property Information As String Get Return $"Last download date: {GetLastDateString()} ({GetWorkingState()})" End Get End Property Private Function GetWorkingState() As String Dim OutStr$ If Working And Not IsManual Then If _Downloading Then OutStr = "downloading" ElseIf _ForceStartRequested Then OutStr = "force start pending" ElseIf _StopRequested Then OutStr = "stopping" ElseIf StartupDelay > 0 And _StartTime.AddMinutes(StartupDelay) > Now Then OutStr = $"delayed ({StartupDelay}) until {_StartTime.AddMinutes(StartupDelay).ToStringDate(ADateTime.Formats.BaseDateTime)}" Else OutStr = "working" End If If IsPaused Then Select Case Pause Case PauseModes.Unlimited : OutStr &= ", paused" Case PauseModes.Until : OutStr &= $", paused (until {AConvert(Of String)(_PauseValue, ADateTime.Formats.BaseDateTime, "?")})" Case Else : OutStr &= $", paused ([{Pause}] until {AConvert(Of String)(_PauseValue, ADateTime.Formats.BaseDateTime, "?")})" End Select End If ElseIf IsManual Then If _Downloading Then OutStr = "downloading" ElseIf _ForceStartRequested Then OutStr = "force start pending" Else OutStr = String.Empty End If Else OutStr = "stopped" End If Return OutStr End Function Private Function GetWorkingState_Manual() As String Dim OutStr$ = GetWorkingState() If Not OutStr.IsEmptyString Then Return $" ({OutStr})" Else Return String.Empty End Function Public Overrides Function ToString() As String If IsManual Then Return $"{Name} (manual{GetWorkingState_Manual()}): last download date: {GetLastDateString()}" Else Return $"{Name} ({GetWorkingState()}): last download date: {GetLastDateString()}; next run: {GetNextDateString()}" End If End Function Friend Overrides Function ToStringViewFilters() As String Return $"Scheduler plan '{Name}'{IIf(IsManual, " (manual)", String.Empty)}" End Function #End Region #End Region #Region "Initializer" Private ReadOnly Initialization As Boolean = True Private _IsNewPlan As Boolean = False Friend ReadOnly Property IsNewPlan As Boolean Get Return _IsNewPlan End Get End Property Friend Sub New(Optional ByVal IsNewPlan As Boolean = False) UserKeys = New List(Of NotifiedUser) _IsNewPlan = IsNewPlan Initialization = False End Sub Friend Sub New(ByVal x As EContainer) Me.New Initialization = True Import(x) #Disable Warning BC40008 If x.Contains(Name_Mode) Then Dim g% = x.Value(Name_Mode).FromXML(Of Integer)(0) If g = 4 Then GroupsOnly = True Enabled = g Else Enabled = x.Value(Name_Enabled).FromXML(Of Boolean)(False) End If #Enable Warning If Name.IsEmptyString Then Name = "Default" IsManual = x.Value(Name_IsManual).FromXML(Of Boolean)(False) Timer = x.Value(Name_Timer).FromXML(Of Integer)(DefaultTimer) If Timer <= 0 Then Timer = DefaultTimer StartupDelay = x.Value(Name_StartupDelay).FromXML(Of Integer)(0) If StartupDelay < 0 Then StartupDelay = 0 ShowNotifications = x.Value(Name_ShowNotifications).FromXML(Of Boolean)(True) ShowPictureDownloaded = x.Value(Name_ShowPictureDown).FromXML(Of Boolean)(True) ShowPictureUser = x.Value(Name_ShowPictureUser).FromXML(Of Boolean)(True) ShowSimpleNotification = x.Value(Name_ShowSimpleNotification).FromXML(Of Boolean)(False) LastDownloadDateXML = AConvert(Of Date)(x.Value(Name_LastDownloadDate), DateProvider, Nothing) If LastDownloadDateXML.HasValue Then LastDownloadDate = LastDownloadDateXML.Value Else LastDownloadDate = Now.AddYears(-1) End If Initialization = False End Sub #End Region #Region "ICopier Support" Friend Overrides Function Copy() As Object Dim newObj As New AutoDownloader(True) newObj.Copy(Me) With newObj .Name = String.Empty .Enabled = Enabled .IsManual = IsManual .Timer = Timer .StartupDelay = StartupDelay .ShowNotifications = ShowNotifications .ShowPictureDownloaded = ShowPictureDownloaded .ShowPictureUser = ShowPictureUser .ShowSimpleNotification = ShowSimpleNotification End With Return newObj End Function #End Region #Region "Groups Support" Friend Sub GROUPS_Updated(ByVal Sender As DownloadGroup) If Groups.Count > 0 Then Dim i% = Groups.IndexOf(Sender.NameBefore) If i >= 0 Then Groups(i) = Sender.Name : Update() End If End Sub Friend Sub GROUPS_Deleted(ByVal Sender As DownloadGroup) If Groups.Count > 0 Then Dim i% = Groups.IndexOf(Sender.Name) If i >= 0 Then Groups.RemoveAt(i) : Update() End If End Sub #End Region #Region "Update" Friend Sub Update() If Not Source Is Nothing Then Source.Update() End Sub Private Function ToEContainer(Optional ByVal e As ErrorsDescriber = Nothing) As EContainer Implements IEContainerProvider.ToEContainer Return Export(New EContainer(Scheduler.Name_Plan, String.Empty) From { New EContainer(Name_Enabled, Enabled.BoolToInteger), New EContainer(Name_IsManual, IsManual.BoolToInteger), New EContainer(Name_Timer, Timer), New EContainer(Name_StartupDelay, StartupDelay), New EContainer(Name_ShowNotifications, ShowNotifications.BoolToInteger), New EContainer(Name_ShowPictureDown, ShowPictureDownloaded.BoolToInteger), New EContainer(Name_ShowPictureUser, ShowPictureUser.BoolToInteger), New EContainer(Name_ShowSimpleNotification, ShowSimpleNotification.BoolToInteger), New EContainer(Name_LastDownloadDate, CStr(AConvert(Of String)(If(LastDownloadDateXML.HasValue Or _LastDownloadDateChanged, CObj(LastDownloadDate), Nothing), DateProvider, String.Empty))) }) End Function #End Region #Region "Execution" Private _Working As Boolean = False Friend ReadOnly Property Working As Boolean Get Return _Working End Get End Property Private _StartTime As Date = Now Friend Sub Start(ByVal Init As Boolean, Optional ByVal Force As Boolean = False) If Not IsManual Or Force Then If Init Then _StartTime = Now _IsNewPlan = False If Not Working And Enabled Then _Working = True RaiseEvent PlanChanged(Me) End If End Sub Private _StopRequested As Boolean = False Private _Pause As PauseModes = PauseModes.Disabled Private _PauseValue As Date? = Nothing Friend Property Pause(Optional ByVal DateLimit As Date? = Nothing) As PauseModes Get Return _Pause End Get Set(ByVal p As PauseModes) _Pause = p Select Case p Case PauseModes.Disabled, PauseModes.Unlimited : _PauseValue = Nothing Case PauseModes.H1 : _PauseValue = Now.AddHours(1) Case PauseModes.H2 : _PauseValue = Now.AddHours(2) Case PauseModes.H3 : _PauseValue = Now.AddHours(3) Case PauseModes.H4 : _PauseValue = Now.AddHours(4) Case PauseModes.H6 : _PauseValue = Now.AddHours(6) Case PauseModes.H12 : _PauseValue = Now.AddHours(12) Case PauseModes.Until : _PauseValue = DateLimit Case Else : _PauseValue = Nothing End Select RaiseEvent PauseChanged(p) RaiseEvent PlanChanged(Me) End Set End Property Private ReadOnly Property IsPaused As Boolean Get If Not _Pause = PauseModes.Disabled Then If _PauseValue.HasValue Then If _PauseValue.Value > Now Then Return True Else _Pause = PauseModes.Disabled _PauseValue = Nothing RaiseEvent PauseChanged(_Pause) Return False End If Else Return True End If Else Return False End If End Get End Property Friend Sub [Stop]() If Working Then If Downloading Then _StopRequested = True Else _Working = False RaiseEvent PlanChanged(Me) End If End Sub Friend Overloads Sub Skip() If LastDownloadDate.AddMinutes(Timer) <= Now Then _LastDownloadDateSkip = Now.AddMinutes(Timer) Else _LastDownloadDateSkip = LastDownloadDate.AddMinutes(Timer) End If RaiseEvent PlanChanged(Me) End Sub Friend Overloads Sub Skip(ByVal Minutes As Integer) _LastDownloadDateSkip = If(_LastDownloadDateSkip, Now).AddMinutes(Minutes) RaiseEvent PlanChanged(Me) End Sub Friend Overloads Sub Skip(ByVal ToDate As Date) _LastDownloadDateSkip = ToDate RaiseEvent PlanChanged(Me) End Sub Friend Sub SkipReset() _LastDownloadDateSkip = Nothing RaiseEvent PlanChanged(Me) End Sub Friend Sub ForceStart() _ForceStartRequested = True If IsManual Then Start(False, True) RaiseEvent PlanChanged(Me) End Sub Private _ForceStartRequested As Boolean = False Friend ReadOnly Property ForceStartRequested As Boolean Get Return _ForceStartRequested End Get End Property Friend ReadOnly Property DownloadReady(Optional ByVal IgnorePause As Boolean = False, Optional ByVal IgnoreDownloaderWorking As Boolean = False) As Boolean Get If _StopRequested Then _Working = False Return (Working Or IsManual) And ((IsManual And _ForceStartRequested) Or (Not IsManual And NextExecutionDate < Now And (Not IsPaused Or IgnorePause)) Or _ForceStartRequested) And Not _StopRequested And Enabled And (Not Downloader.Working Or IgnoreDownloaderWorking) End Get End Property Friend ReadOnly Property NextDate As Date? Get If Not _StopRequested And Enabled Then If IsManual Or _ForceStartRequested Then Return Now.AddYears(-10) ElseIf Not IsPaused And Not IsManual And Working Then Return NextExecutionDate End If End If Return Nothing End Get End Property Private _Downloading As Boolean = False Friend ReadOnly Property Downloading As Boolean Get Return _Downloading End Get End Property Friend Sub Download() _Downloading = True RaiseEvent PlanChanged(Me) Dim Keys As New List(Of String) Try Dim users As New List(Of IUserData) Dim doRound% = -1, doLim% = Settings.Plugins.Count Dim DownloadedUsersCount% = 0 Dim DownloadedSubscriptionsCount% = 0 Dim simple As Boolean = ShowSimpleNotification And ShowNotifications Dim notify As Action = Sub() Try With Downloader.Downloaded If ShowNotifications And .Count > 0 Then For indx% = 0 To .Count - 1 With .Item(indx) If Keys.Contains(.Key) Then If simple Then If .IsSubscription Then DownloadedSubscriptionsCount += 1 Else DownloadedUsersCount += 1 End If Else ShowNotification(.Self) End If Keys.Remove(.Key) End If End With Next End If End With Catch n_ex As Exception End Try End Sub If Enabled Then users.ListAddList(DownloadGroup.GetUsers(Me)) If users.Count > 0 Then Keys.ListAddList(users.Select(Function(u) u.Key)) With Downloader .AutoDownloaderWorking = True If .Downloaded.Count > 0 Then .Downloaded.RemoveAll(Function(u) Keys.Contains(u.Key)) : .InvokeDownloadsChangeEvent() Do Try doRound += 1 .AddRange(users, True) Exit Do Catch iex As Exception If doRound = doLim Then Throw iex Else Thread.Sleep(200) End If End Try Loop While doRound <= doLim While .Working Or .Count > 0 : notify.Invoke() : Thread.Sleep(200) : End While .AutoDownloaderWorking = False notify.Invoke If simple And (DownloadedUsersCount + DownloadedSubscriptionsCount) > 0 Then Dim msg$ = String.Empty If DownloadedUsersCount > 0 Then msg = $"{DownloadedUsersCount} user(s) " If DownloadedSubscriptionsCount > 0 Then msg &= $"{IIf(DownloadedUsersCount > 0, "and ", String.Empty)}{DownloadedSubscriptionsCount} subscription(s) " msg &= $"downloaded with scheduler plan '{Name}'" MainFrameObj.ShowNotification(SettingsCLS.NotificationObjects.AutoDownloader, msg) End If End With End If Catch ex As Exception ErrorsDescriber.Execute(EDP.SendToLog, ex, $"[AutoDownloader.Download({Name})]") Finally Keys.Clear() LastDownloadDate = Now Update() _Downloading = False _ForceStartRequested = False _LastDownloadDateSkip = Nothing If _StopRequested Then _Working = False RaiseEvent PlanChanged(Me) End Try End Sub Private Sub ShowNotification(ByVal u As IUserData) Dim k$ = $"{Name}_{u.Key}" Dim i% = UserKeys.IndexOf(k) If i >= 0 Then UserKeys(i).ShowNotification() Else UserKeys.Add(New NotifiedUser(k, Settings.GetUser(u), Me)) UserKeys.Last.ShowNotification() End If End Sub Friend Function NotificationClicked(ByVal Key As String, ByRef Found As Boolean, ByRef ActivateForm As Boolean) As Boolean Dim i% = UserKeys.IndexOf(Key) If i >= 0 Then Found = True ActivateForm = UserKeys(i).Open(Key) MainFrameObj.FocusUser(UserKeys(i).IUserDataKey, ActivateForm) Return True Else Return False End If End Function #End Region #Region "IComparable Support" Private Function CompareTo(ByVal Other As AutoDownloader) As Integer Implements IComparable(Of AutoDownloader).CompareTo Return Index.CompareTo(Other.Index) End Function #End Region #Region "IDisposable Support" Protected Overrides Sub Dispose(ByVal disposing As Boolean) If Not disposedValue And disposing Then [Stop]() UserKeys.ListClearDispose() End If MyBase.Dispose(disposing) End Sub #End Region End Class End Namespace