837 lines
40 KiB
VB.net
837 lines
40 KiB
VB.net
Public Class PageInstanceMod
|
||
Implements IRefreshable
|
||
|
||
#Region "初始化"
|
||
|
||
Private IsLoad As Boolean = False
|
||
Public Sub PageOther_Loaded() Handles Me.Loaded
|
||
|
||
If FrmMain.PageLast.Page <> FormMain.PageType.ResourceDetail Then PanBack.ScrollToHome()
|
||
AniControlEnabled += 1
|
||
SelectedMods.Clear()
|
||
ReloadModList()
|
||
ChangeAllSelected(False)
|
||
AniControlEnabled -= 1
|
||
|
||
'非重复加载部分
|
||
If IsLoad Then Return
|
||
IsLoad = True
|
||
|
||
AddHandler FrmMain.KeyDown, AddressOf FrmMain_KeyDown
|
||
'调整按钮边距(这玩意儿没法从 XAML 改)
|
||
For Each Btn As MyRadioButton In PanFilter.Children
|
||
Btn.LabText.Margin = New Thickness(-2, 0, 8, 0)
|
||
Next
|
||
|
||
End Sub
|
||
''' <summary>
|
||
''' 刷新 Mod 列表。
|
||
''' </summary>
|
||
Public Sub ReloadModList(Optional ForceReload As Boolean = False)
|
||
If Not LoaderRun(If(ForceReload, LoaderFolderRunType.ForceRun, LoaderFolderRunType.RunOnUpdated)) Then Return
|
||
Logger.Info("已刷新 Mod 列表")
|
||
Filter = FilterType.All
|
||
PanBack.ScrollToHome()
|
||
SearchBox.Text = ""
|
||
End Sub
|
||
'强制刷新
|
||
Private Sub RefreshSelf() Implements IRefreshable.Refresh
|
||
Refresh()
|
||
End Sub
|
||
Public Shared Sub Refresh()
|
||
Try
|
||
ResourceProject.Cache.Clear()
|
||
ResourceVersion.ProjectFilesCache.Clear()
|
||
FileUtils.Delete(PathTemp & "Cache\LocalMod.json")
|
||
Logger.Info("由于点击刷新按钮,清理本地 Mod 信息缓存")
|
||
Catch ex As Exception
|
||
Logger.Warn(ex, "强制刷新时清理本地 Mod 信息缓存失败")
|
||
End Try
|
||
If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.ReloadModList(True) '无需 Else,还没加载刷个鬼的新
|
||
FrmInstanceLeft.ItemMod.Checked = True
|
||
Hint("正在刷新……", Log:=False)
|
||
End Sub
|
||
|
||
Private Sub LoaderInit() Handles Me.Initialized
|
||
PageLoaderInit(Load, PanLoad, PanAllBack, Nothing, LocalResourceLoader, AddressOf LoadUIFromLoaderOutput, AutoRun:=False)
|
||
End Sub
|
||
Private Sub Load_Click(sender As Object, e As MouseButtonEventArgs) Handles Load.Click
|
||
If LocalResourceLoader.State = LoadState.Failed Then LoaderRun(LoaderFolderRunType.ForceRun)
|
||
End Sub
|
||
Public Function LoaderRun(Type As LoaderFolderRunType) As Boolean
|
||
Return LoaderFolderRun(LocalResourceLoader, PageInstanceLeft.Instance.PathIndie & "mods\", Type)
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
#Region "UI 化"
|
||
|
||
''' <summary>
|
||
''' 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawFileName。
|
||
''' </summary>
|
||
Public ModItems As New ConcurrentDictionary(Of String, MyLocalModItem)
|
||
''' <summary>
|
||
''' 将加载器结果的 Mod 列表加载为 UI。
|
||
''' </summary>
|
||
Private Sub LoadUIFromLoaderOutput()
|
||
Try
|
||
'判断应该显示哪一个页面
|
||
If LocalResourceLoader.Output.Any() Then
|
||
PanBack.Visibility = Visibility.Visible
|
||
PanEmpty.Visibility = Visibility.Collapsed
|
||
Else
|
||
PanEmpty.Visibility = Visibility.Visible
|
||
PanBack.Visibility = Visibility.Collapsed
|
||
Return
|
||
End If
|
||
'修改缓存
|
||
ModItems.Clear()
|
||
For Each ModEntity As LocalResourceFile In LocalResourceLoader.Output
|
||
ModItems(ModEntity.EnabledName) = McModListItem(ModEntity)
|
||
Next
|
||
'显示结果
|
||
Filter = FilterType.All
|
||
SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
|
||
RefreshUI()
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "加载 Mod 列表 UI 失败")
|
||
End Try
|
||
End Sub
|
||
Private Function McModListItem(Entry As LocalResourceFile) As MyLocalModItem
|
||
AniControlEnabled += 1
|
||
Dim NewItem As New MyLocalModItem With {.SnapsToDevicePixels = True, .Entry = Entry,
|
||
.ButtonHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.EnabledName)}
|
||
AddHandler Entry.OnUpdate, Sub() RunInUi(AddressOf NewItem.Refresh)
|
||
AniControlEnabled -= 1
|
||
Return NewItem
|
||
End Function
|
||
Private Sub McModContent(sender As MyLocalModItem, e As EventArgs)
|
||
'点击事件
|
||
AddHandler sender.Changed, AddressOf CheckChanged
|
||
AddHandler sender.Click, Sub(ss As MyLocalModItem, ee As EventArgs) ss.Checked = Not ss.Checked
|
||
'图标按钮
|
||
Dim BtnOpen As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonOpen, .Tag = sender}
|
||
BtnOpen.ToolTip = "打开文件位置"
|
||
AddHandler BtnOpen.Click, AddressOf Open_Click
|
||
Dim BtnCont As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonInfo, .Tag = sender}
|
||
BtnCont.ToolTip = "详情"
|
||
AddHandler BtnCont.Click, AddressOf Info_Click
|
||
AddHandler sender.MouseRightButtonUp, AddressOf Info_Click
|
||
Dim BtnDelete As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonDelete, .Tag = sender}
|
||
BtnDelete.ToolTip = "删除"
|
||
AddHandler BtnDelete.Click, AddressOf Delete_Click
|
||
Dim BtnED As New MyIconButton With {.LogoScale = 1, .Logo = If(sender.Entry.IsEnabled, Logo.IconButtonStop, Logo.IconButtonCheck), .Tag = sender}
|
||
BtnED.ToolTip = If(sender.Entry.IsEnabled, "禁用", "启用")
|
||
AddHandler BtnED.Click, AddressOf ED_Click
|
||
sender.Buttons = {BtnCont, BtnOpen, BtnED, BtnDelete}
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 刷新整个 UI。
|
||
''' </summary>
|
||
Public Sub RefreshUI()
|
||
If PanList Is Nothing Then Return
|
||
Dim ShowingMods = If(IsSearching, SearchResult, If(LocalResourceLoader.Output, New List(Of LocalResourceFile))).Where(Function(m) CanPassFilter(m)).ToList
|
||
'重新列出列表
|
||
AniControlEnabled += 1
|
||
If ShowingMods.Any() Then
|
||
PanList.Visibility = Visibility.Visible
|
||
PanList.Children.Clear()
|
||
For Each TargetMod In ShowingMods
|
||
Dim Item As MyLocalModItem = Nothing
|
||
If Not ModItems.TryGetValue(TargetMod.EnabledName, Item) Then Continue For
|
||
Item.Checked = SelectedMods.Contains(TargetMod.EnabledName) '更新选中状态
|
||
PanList.Children.Add(Item)
|
||
Next
|
||
Else
|
||
PanList.Visibility = Visibility.Collapsed
|
||
End If
|
||
AniControlEnabled -= 1
|
||
SelectedMods = SelectedMods.Where(Function(m) ShowingMods.Any(Function(s) s.EnabledName = m)).ToList '取消选中已经不显示的 Mod
|
||
RefreshBars()
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 刷新顶栏和底栏显示。
|
||
''' </summary>
|
||
Public Sub RefreshBars()
|
||
|
||
'-----------------
|
||
' 顶部栏
|
||
'-----------------
|
||
|
||
'计数
|
||
Dim AnyCount As Integer = 0
|
||
Dim EnabledCount As Integer = 0
|
||
Dim UpdateCount As Integer = 0
|
||
For Each ModItem In If(IsSearching, SearchResult, If(LocalResourceLoader.Output, New List(Of LocalResourceFile)))
|
||
AnyCount += 1
|
||
If ModItem.HasUpdate Then UpdateCount += 1
|
||
If ModItem.IsEnabled Then EnabledCount += 1
|
||
Next
|
||
Dim DisabledCount As Integer = AnyCount - EnabledCount
|
||
'显示
|
||
BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
|
||
BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
|
||
BtnFilterCanUpdate.Visibility = If(Filter = FilterType.CanUpdate OrElse UpdateCount > 0, Visibility.Visible, Visibility.Collapsed)
|
||
BtnFilterEnabled.Text = $"启用 ({EnabledCount})"
|
||
BtnFilterEnabled.Visibility = If(Filter = FilterType.Enabled OrElse (EnabledCount > 0 AndAlso EnabledCount < AnyCount), Visibility.Visible, Visibility.Collapsed)
|
||
BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"
|
||
BtnFilterDisabled.Visibility = If(Filter = FilterType.Disabled OrElse DisabledCount > 0, Visibility.Visible, Visibility.Collapsed)
|
||
|
||
'-----------------
|
||
' 底部栏
|
||
'-----------------
|
||
|
||
'计数
|
||
Dim NewCount As Integer = SelectedMods.Count
|
||
Dim Selected = NewCount > 0
|
||
If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字
|
||
'按钮可用性
|
||
If Selected Then
|
||
Dim HasUpdate As Boolean = False
|
||
Dim HasEnabled As Boolean = False
|
||
Dim HasDisabled As Boolean = False
|
||
For Each ModEntity In LocalResourceLoader.Output
|
||
If Not SelectedMods.Contains(ModEntity.EnabledName) Then Continue For
|
||
If ModEntity.HasUpdate Then HasUpdate = True
|
||
If ModEntity.IsEnabled Then HasEnabled = True
|
||
If Not ModEntity.IsEnabled Then HasDisabled = True
|
||
Next
|
||
BtnSelectDisable.IsEnabled = HasEnabled
|
||
BtnSelectEnable.IsEnabled = HasDisabled
|
||
BtnSelectUpdate.IsEnabled = HasUpdate
|
||
End If
|
||
'更新显示状态
|
||
If AniControlEnabled = 0 Then
|
||
PanListBack.Margin = New Thickness(0, 0, 0, If(Selected, 95, 15))
|
||
If Selected Then
|
||
'仅在数量增加时播放出现/跳跃动画
|
||
If BottomBarShownCount >= NewCount Then
|
||
BottomBarShownCount = NewCount
|
||
Return
|
||
Else
|
||
BottomBarShownCount = NewCount
|
||
End If
|
||
'出现/跳跃动画
|
||
CardSelect.Visibility = Visibility.Visible
|
||
AniStart({
|
||
AaOpacity(CardSelect, 1 - CardSelect.Opacity, 60),
|
||
AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, Ease:=New AniEaseOutFluent(AniEasePower.Weak)),
|
||
AaTranslateY(CardSelect, 3, 150, 120, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)),
|
||
AaTranslateY(CardSelect, -1, 90, 270, Ease:=New AniEaseInoutFluent(AniEasePower.Weak))
|
||
}, "Mod Sidebar")
|
||
Else
|
||
'不重复播放隐藏动画
|
||
If BottomBarShownCount = 0 Then Return
|
||
BottomBarShownCount = 0
|
||
'隐藏动画
|
||
AniStart({
|
||
AaOpacity(CardSelect, -CardSelect.Opacity, 90),
|
||
AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, Ease:=New AniEaseInFluent(AniEasePower.Weak)),
|
||
AaCode(Sub() CardSelect.Visibility = Visibility.Collapsed, After:=True)
|
||
}, "Mod Sidebar")
|
||
End If
|
||
Else
|
||
AniStop("Mod Sidebar")
|
||
BottomBarShownCount = NewCount
|
||
If Selected Then
|
||
CardSelect.Visibility = Visibility.Visible
|
||
CardSelect.Opacity = 1
|
||
TransSelect.Y = -25
|
||
Else
|
||
CardSelect.Visibility = Visibility.Collapsed
|
||
CardSelect.Opacity = 0
|
||
TransSelect.Y = -10
|
||
End If
|
||
End If
|
||
End Sub
|
||
Private BottomBarShownCount As Integer = 0
|
||
|
||
#End Region
|
||
|
||
#Region "管理"
|
||
|
||
''' <summary>
|
||
''' 打开 Mods 文件夹。
|
||
''' </summary>
|
||
Private Sub BtnManageOpen_Click(sender As Object, e As EventArgs) Handles BtnManageOpen.Click, BtnHintOpen.Click
|
||
Try
|
||
DirectoryUtils.Create(PageInstanceLeft.Instance.PathIndie & "mods\")
|
||
OpenExplorer(PageInstanceLeft.Instance.PathIndie & "mods\")
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "打开 Mods 文件夹失败", LogBehavior.Alert)
|
||
End Try
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 全选。
|
||
''' </summary>
|
||
Private Sub BtnManageSelectAll_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageSelectAll.Click
|
||
ChangeAllSelected(SelectedMods.Count < PanList.Children.Count)
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 安装 Mod。
|
||
''' </summary>
|
||
Private Sub BtnManageInstall_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageInstall.Click, BtnHintInstall.Click
|
||
Dim FileList = Dialogs.SelectFile("选择要安装的 Mod", True, filter:={({"jar", "litemod", "disabled", "old"}, "Mod 文件")})
|
||
If Not FileList.Any Then Return
|
||
InstallMods(FileList)
|
||
End Sub
|
||
''' <summary>
|
||
''' 尝试安装 Mod。
|
||
''' 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。
|
||
''' </summary>
|
||
Public Shared Function InstallMods(FilePathList As IEnumerable(Of String)) As Boolean
|
||
Dim Extension As String = FilePathList.First.AfterLast(".").Lower
|
||
'检查文件扩展名
|
||
If Not {"jar", "litemod", "disabled", "old"}.Any(Function(t) t = Extension) Then Return False
|
||
Logger.Info("文件为 jar/litemod 格式,尝试作为 Mod 安装")
|
||
'检查回收站:回收站中的文件有错误的文件名
|
||
If FilePathList.First.Contains(":\$RECYCLE.BIN\") Then
|
||
Hint("请先将文件从回收站还原,再尝试安装!", HintType.Red)
|
||
Return True
|
||
End If
|
||
'获取并检查目标版本
|
||
Dim Instance As McInstance = McInstanceSelected
|
||
If FrmMain.PageCurrent = FormMain.PageType.InstanceSetup Then Instance = PageInstanceLeft.Instance
|
||
If FrmMain.PageCurrent = FormMain.PageType.InstanceSelect OrElse Instance Is Nothing OrElse Not Instance.Modable Then
|
||
'正在选择版本,或当前版本不能安装 Mod
|
||
Hint("若要安装 Mod,请先选择一个可以安装 Mod 的版本!")
|
||
ElseIf Not (FrmMain.PageCurrent = FormMain.PageType.InstanceSetup AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.InstanceMod) Then
|
||
'未处于 Mod 管理页面
|
||
If MyMsgBox($"是否要将这{If(FilePathList.IsSingle, "个", "些")}文件作为 Mod 安装到 {Instance.Name}?", "Mod 安装确认", "确定", "取消") = 1 Then GoTo Install
|
||
Else
|
||
'处于 Mod 管理页面
|
||
Install:
|
||
Try
|
||
For Each ModFile In FilePathList
|
||
Dim NewFileName = PathUtils.GetLastPart(ModFile).Replace(".disabled", "").Replace(".old", "")
|
||
If Not NewFileName.Contains(".") Then NewFileName += ".jar" '#4227
|
||
FileUtils.Copy(ModFile, Instance.PathIndie & "mods\" & NewFileName)
|
||
Next
|
||
If FilePathList.IsSingle Then
|
||
Hint($"已安装 {PathUtils.GetLastPart(FilePathList.First).Replace(".disabled", "").Replace(".old", "")}!", HintType.Green)
|
||
Else
|
||
Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Green)
|
||
End If
|
||
'刷新列表
|
||
If FrmMain.PageCurrent = FormMain.PageType.InstanceSetup AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.InstanceMod Then
|
||
LoaderFolderRun(LocalResourceLoader, Instance.PathIndie & "mods\", LoaderFolderRunType.ForceRun)
|
||
End If
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "复制 Mod 文件失败", LogBehavior.Alert)
|
||
End Try
|
||
End If
|
||
Return True
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 下载 Mod。
|
||
''' </summary>
|
||
Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click
|
||
'预设置当前版本和搜索框内容
|
||
PageResource.TargetInstance = PageInstanceLeft.Instance
|
||
PageResource.TargetName = SearchBox.Text
|
||
FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod)
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "选择"
|
||
|
||
'选择的 Mod 的路径(不含 .disabled 和 .old)
|
||
Public SelectedMods As New List(Of String)
|
||
|
||
'单项切换选择状态
|
||
Public Sub CheckChanged(sender As MyLocalModItem, e As RouteEventArgs)
|
||
If AniControlEnabled <> 0 Then Return
|
||
'更新选择了的内容
|
||
Dim SelectedKey As String = sender.Entry.EnabledName
|
||
If sender.Checked Then
|
||
If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey)
|
||
Else
|
||
SelectedMods.Remove(SelectedKey)
|
||
End If
|
||
RefreshBars()
|
||
End Sub
|
||
|
||
'切换所有项的选择状态
|
||
Private Sub ChangeAllSelected(Value As Boolean)
|
||
AniControlEnabled += 1
|
||
SelectedMods.Clear()
|
||
For Each Item As MyLocalModItem In ModItems.Values
|
||
'#4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断
|
||
Dim ShouldSelected As Boolean = Value AndAlso PanList.Children.Contains(Item)
|
||
Item.Checked = ShouldSelected
|
||
If ShouldSelected Then SelectedMods.Add(Item.Entry.EnabledName)
|
||
Next
|
||
AniControlEnabled -= 1
|
||
RefreshBars()
|
||
End Sub
|
||
Private Sub UnselectedAllWithAnimation() Handles Load.StateChanged, Me.PageExit
|
||
Dim CacheAniControlEnabled = AniControlEnabled
|
||
AniControlEnabled = 0
|
||
ChangeAllSelected(False)
|
||
AniControlEnabled += CacheAniControlEnabled
|
||
End Sub
|
||
Private Sub FrmMain_KeyDown(sender As Object, e As KeyEventArgs) '监听自己的事件的话进入页面后不点击右侧控件就没办法监听到事件 (#4311)
|
||
If FrmMain.PageRight IsNot Me Then Return
|
||
If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeAllSelected(True)
|
||
End Sub
|
||
Private Sub SearchBox_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles SearchBox.PreviewKeyDown
|
||
'Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获
|
||
If SearchBox.Text.Any Then Return
|
||
If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then PanBack.Focus()
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "筛选"
|
||
|
||
Private _Filter As FilterType = FilterType.All
|
||
Public Property Filter As FilterType
|
||
Get
|
||
Return _Filter
|
||
End Get
|
||
Set(value As FilterType)
|
||
If _Filter = value Then Return
|
||
_Filter = value
|
||
Select Case value
|
||
Case FilterType.All
|
||
BtnFilterAll.Checked = True
|
||
Case FilterType.Enabled
|
||
BtnFilterEnabled.Checked = True
|
||
Case FilterType.Disabled
|
||
BtnFilterDisabled.Checked = True
|
||
Case Else
|
||
BtnFilterCanUpdate.Checked = True
|
||
End Select
|
||
RefreshUI()
|
||
End Set
|
||
End Property
|
||
Public Enum FilterType As Integer
|
||
All = 0
|
||
Enabled = 1
|
||
Disabled = 2
|
||
CanUpdate = 3
|
||
End Enum
|
||
|
||
''' <summary>
|
||
''' 检查该 Mod 项是否符合当前筛选的类别。
|
||
''' </summary>
|
||
Private Function CanPassFilter(CheckingMod As LocalResourceFile) As Boolean
|
||
Select Case Filter
|
||
Case FilterType.All
|
||
Return True
|
||
Case FilterType.Enabled
|
||
Return CheckingMod.IsEnabled
|
||
Case FilterType.Disabled
|
||
Return Not CheckingMod.IsEnabled
|
||
Case FilterType.CanUpdate
|
||
Return CheckingMod.HasUpdate
|
||
Case Else
|
||
Return False
|
||
End Select
|
||
End Function
|
||
|
||
'点击筛选项触发的改变
|
||
Private Sub ChangeFilter(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnFilterAll.Check, BtnFilterCanUpdate.Check, BtnFilterDisabled.Check, BtnFilterEnabled.Check
|
||
Filter = sender.Tag
|
||
RefreshUI()
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "下边栏"
|
||
|
||
'启用 / 禁用
|
||
Private Sub BtnSelectED_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click
|
||
EDMods(LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)),
|
||
Not sender.Equals(BtnSelectDisable))
|
||
ChangeAllSelected(False)
|
||
End Sub
|
||
Private Sub EDMods(ModList As IEnumerable(Of LocalResourceFile), IsEnable As Boolean)
|
||
Dim IsSuccessful As Boolean = True
|
||
For Each ModE In ModList.ToList
|
||
Dim ModEntity = ModE '仅用于去除迭代变量无法修改的限制
|
||
Dim NewPath As String = Nothing
|
||
If ModEntity.IsEnabled AndAlso Not IsEnable Then
|
||
'禁用
|
||
NewPath = PathUtils.RemoveExtendedPrefix(ModEntity.File.FullName) & If(FileUtils.Exists(ModEntity.File.FullName & ".old"), ".old", ".disabled")
|
||
ElseIf Not ModEntity.IsEnabled AndAlso IsEnable Then
|
||
'启用
|
||
NewPath = ModEntity.EnabledFullName
|
||
Else
|
||
Continue For
|
||
End If
|
||
'重命名
|
||
Try
|
||
If FileUtils.Exists(NewPath) Then
|
||
If FileUtils.Exists(ModEntity.File.FullName) Then
|
||
'同时存在两个名称的 Mod
|
||
If CryptographyUtils.ComputeFileHash(ModEntity.File.FullName) <> CryptographyUtils.ComputeFileHash(NewPath) Then
|
||
MyMsgBox($"目前同时存在启用和禁用的两个 Mod 文件:{vbCrLf} - {PathUtils.GetLastPart(NewPath)}{vbCrLf} - {ModEntity.File.Name}{vbCrLf}{vbCrLf}注意,这两个文件的内容并不相同。{vbCrLf}在手动删除或重命名其中一个文件后,才能继续操作。", "存在文件冲突")
|
||
Continue For
|
||
End If
|
||
Else
|
||
'已经重命名过了
|
||
Logger.Warn("Mod 的状态已被切换")
|
||
Continue For
|
||
End If
|
||
End If
|
||
FileUtils.Move(ModEntity.File.FullName, NewPath)
|
||
Catch ex As FileNotFoundException
|
||
Logger.Error(ex, $"未找到需要重命名的 Mod({If(ModEntity.File.FullName, "null")})")
|
||
ReloadModList(True)
|
||
Return
|
||
Catch ex As Exception
|
||
Logger.Warn(ex, $"重命名 Mod 失败({If(ModEntity.File.FullName, "null")})")
|
||
IsSuccessful = False
|
||
End Try
|
||
'更改 Loader 中的列表
|
||
Dim NewModEntity As New LocalResourceFile(NewPath)
|
||
NewModEntity.FromJson(ModEntity.ToJson)
|
||
If LocalResourceLoader.Output.Contains(ModEntity) Then
|
||
Dim IndexOfLoader As Integer = LocalResourceLoader.Output.IndexOf(ModEntity)
|
||
LocalResourceLoader.Output.RemoveAt(IndexOfLoader)
|
||
LocalResourceLoader.Output.Insert(IndexOfLoader, NewModEntity)
|
||
End If
|
||
If SearchResult IsNot Nothing AndAlso SearchResult.Contains(ModEntity) Then '#4862
|
||
Dim IndexOfResult As Integer = SearchResult.IndexOf(ModEntity)
|
||
SearchResult.Remove(ModEntity)
|
||
SearchResult.Insert(IndexOfResult, NewModEntity)
|
||
End If
|
||
'更改 UI 中的列表
|
||
Dim NewItem As MyLocalModItem = McModListItem(NewModEntity)
|
||
ModItems(ModEntity.EnabledName) = NewItem
|
||
Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry Is ModEntity))
|
||
If IndexOfUi = -1 Then Continue For '因为未知原因 Mod 的状态已经切换完了
|
||
PanList.Children.RemoveAt(IndexOfUi)
|
||
PanList.Children.Insert(IndexOfUi, NewItem)
|
||
NewItem.UpdateLayout() '#7861
|
||
Next
|
||
If IsSuccessful Then
|
||
RefreshBars()
|
||
Else
|
||
Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
|
||
ReloadModList(True)
|
||
End If
|
||
LoaderRun(LoaderFolderRunType.UpdateOnly)
|
||
End Sub
|
||
|
||
'更新
|
||
Private Sub BtnSelectUpdate_Click() Handles BtnSelectUpdate.Click
|
||
Dim UpdateList As List(Of LocalResourceFile) = LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName) AndAlso m.HasUpdate).ToList()
|
||
If Not UpdateList.Any() Then Return
|
||
UpdateMods(UpdateList)
|
||
ChangeAllSelected(False)
|
||
End Sub
|
||
''' <summary>
|
||
''' 记录正在进行 Mod 更新的 mods 文件夹路径。
|
||
''' </summary>
|
||
Public Shared UpdatingInstanceModFolders As New List(Of String)
|
||
Public Sub UpdateMods(ModList As IEnumerable(Of LocalResourceFile))
|
||
'更新前警告
|
||
If Not Settings.Get(Of Boolean)("HintUpdateMod") OrElse ModList.Count >= 15 Then
|
||
If MyMsgBox($"新版本 Mod 可能不兼容旧存档或者其他 Mod,这可能导致游戏崩溃,甚至永久损坏存档!{vbCrLf}如果你在游玩整合包,请千万不要自行更新 Mod!{vbCrLf}{vbCrLf}在更新前,请先备份存档,并检查 Mod 的更新日志。{vbCrLf}如果更新后出现问题,你也可以在回收站找回更新前的 Mod。", "Mod 更新警告", "我已了解风险,继续更新", "取消", IsWarn:=True) = 1 Then
|
||
Settings.Set("HintUpdateMod", True)
|
||
Else
|
||
Return
|
||
End If
|
||
End If
|
||
Try
|
||
'构造下载信息
|
||
ModList = ModList.ToList() '防止刷新影响迭代器
|
||
Dim FileList As New List(Of NetFile)
|
||
Dim FileCopyList As New Dictionary(Of String, String)
|
||
For Each Entry As LocalResourceFile In ModList
|
||
Dim File As ResourceVersion = Entry.UpdateFile
|
||
If Not File.DownloadAvailable Then Continue For
|
||
'确认更新后的文件名
|
||
Dim CurrentReplaceName = Entry.ProjectVersion.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
|
||
Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
|
||
Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
|
||
Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
|
||
Dim Shortened As Boolean = False
|
||
Do While True '移除前导相同部分(不能移除所有相同项,这会导致例如 1.2-forge-2 和 1.3-forge-3 中间的 forge 被去掉,导致尝试替换 1.2-2)
|
||
If Not CurrentSegs.Any() OrElse Not NewestSegs.Any() Then Exit Do
|
||
If CurrentSegs.First <> NewestSegs.First Then Exit Do
|
||
CurrentSegs.RemoveAt(0)
|
||
NewestSegs.RemoveAt(0)
|
||
Shortened = True
|
||
Loop
|
||
Do While True '移除后导相同部分
|
||
If Not CurrentSegs.Any() OrElse Not NewestSegs.Any() Then Exit Do
|
||
If CurrentSegs.Last <> NewestSegs.Last Then Exit Do
|
||
CurrentSegs.RemoveAt(CurrentSegs.Count - 1)
|
||
NewestSegs.RemoveAt(NewestSegs.Count - 1)
|
||
Shortened = True
|
||
Loop
|
||
If Shortened AndAlso CurrentSegs.Any() AndAlso NewestSegs.Any() Then
|
||
CurrentReplaceName = CurrentSegs.Join("-")
|
||
NewestReplaceName = NewestSegs.Join("-")
|
||
End If
|
||
'添加到下载列表
|
||
Dim TempAddress As String = PathTemp & "mods\" & Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName) '需要下载到 mods 文件夹,以便重复文件检查时识别
|
||
Dim RealAddress As String = Path.Combine(PathUtils.RemoveLastPart(Entry.File.FullName), Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName))
|
||
FileList.Add(File.ToNetFile(TempAddress, ResourceVersion.DownloadReason.Update, PageInstanceLeft.Instance.Version.VanillaName, PageInstanceLeft.Instance.Version.ModLoaders))
|
||
FileCopyList(TempAddress) = RealAddress
|
||
Next
|
||
'构造加载器
|
||
Dim InstallLoaders As New List(Of LoaderBase)
|
||
Dim FinishedFileNames As New List(Of String)
|
||
InstallLoaders.Add(New LoaderDownload("下载新版 Mod 文件", FileList) With {.ProgressWeight = ModList.Count * 1.5}) '每个 Mod 需要 1.5s
|
||
InstallLoaders.Add(New LoaderTask(Of Integer, Integer)("替换旧版 Mod 文件",
|
||
Sub()
|
||
Try
|
||
For Each Entry As LocalResourceFile In ModList
|
||
If FileUtils.Exists(Entry.File.FullName) Then
|
||
FileUtils.Delete(Entry.File.FullName, True)
|
||
Else
|
||
Logger.Warn($"未找到更新前的 Mod 文件,跳过对它的删除:{Entry.File.FullName}")
|
||
End If
|
||
Next
|
||
For Each Entry As KeyValuePair(Of String, String) In FileCopyList
|
||
If DirectoryUtils.Exists(PathUtils.RemoveLastPart(Entry.Value)) Then
|
||
FileUtils.Move(Entry.Key, Entry.Value)
|
||
FinishedFileNames.Add(PathUtils.GetLastPart(Entry.Value))
|
||
Else
|
||
Logger.Warn($"更新后的目标文件夹已被删除:{Entry.Value}")
|
||
End If
|
||
Next
|
||
Catch ex As OperationCanceledException
|
||
Logger.Warn(ex, "替换旧版 Mod 文件时被主动取消")
|
||
End Try
|
||
End Sub))
|
||
'结束处理
|
||
Dim Loader As New LoaderCombo(Of IEnumerable(Of LocalResourceFile))("Mod 更新:" & PageInstanceLeft.Instance.Name, InstallLoaders)
|
||
Dim PathMods As String = PageInstanceLeft.Instance.PathIndie & "mods\"
|
||
Loader.OnStateChanged =
|
||
Sub()
|
||
'结果提示
|
||
Select Case Loader.State
|
||
Case LoadState.Finished
|
||
Select Case FinishedFileNames.Count
|
||
Case 0 '一般是由于 Mod 文件被占用,然后玩家主动取消
|
||
Logger.Info($"没有 Mod 被成功更新")
|
||
Case 1
|
||
Hint($"已成功更新 {FinishedFileNames.Single}!", HintType.Green)
|
||
Case Else
|
||
Hint($"已成功更新 {FinishedFileNames.Count} 个 Mod!", HintType.Green)
|
||
End Select
|
||
Case LoadState.Failed
|
||
Hint("Mod 更新失败:" & Loader.Error.GetDisplay(False), HintType.Red)
|
||
Case LoadState.Canceled
|
||
Hint("Mod 更新已中止!", HintType.Blue)
|
||
Case Else
|
||
Return
|
||
End Select
|
||
Logger.Info($"已从正在进行 Mod 更新的文件夹列表移除:{PathMods}")
|
||
UpdatingInstanceModFolders.Remove(PathMods)
|
||
'清理缓存
|
||
RunInNewThread(
|
||
Sub()
|
||
Try
|
||
For Each TempFile In FileCopyList.Keys
|
||
FileUtils.Delete(TempFile)
|
||
Next
|
||
Catch ex As Exception
|
||
Logger.Warn(ex, "清理 Mod 更新缓存失败")
|
||
End Try
|
||
End Sub, "Clean Mod Update Cache", ThreadPriority.BelowNormal)
|
||
End Sub
|
||
'启动加载器
|
||
Logger.Info($"开始更新 {ModList.Count} 个 Mod:{ModList.Select(Function(m) m.File.Name).Join(",")}")
|
||
UpdatingInstanceModFolders.Add(PathMods)
|
||
Loader.Start()
|
||
LoaderTaskbarAdd(Loader)
|
||
FrmMain.BtnExtraDownload.ShowRefresh()
|
||
FrmMain.BtnExtraDownload.Ribble()
|
||
ReloadModList(True)
|
||
Catch ex As Exception
|
||
Logger.Warn(ex, "初始化 Mod 更新失败")
|
||
End Try
|
||
End Sub
|
||
|
||
'删除
|
||
Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click
|
||
DeleteMods(LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)))
|
||
ChangeAllSelected(False)
|
||
End Sub
|
||
Private Sub DeleteMods(ModsToDelete As IEnumerable(Of LocalResourceFile))
|
||
Try
|
||
Dim IsSuccessful As Boolean = True
|
||
Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
|
||
'确认需要删除的文件
|
||
Dim ModList = ModsToDelete.SelectMany(
|
||
Function(Target As LocalResourceFile)
|
||
If Target.IsEnabled Then
|
||
Return {Target.File.FullName, Target.File.FullName & If(FileUtils.Exists(Target.File.FullName & ".old"), ".old", ".disabled")}
|
||
Else
|
||
Return {Target.File.FullName, Target.EnabledFullName}
|
||
End If
|
||
End Function).Distinct.Where(Function(m) FileUtils.Exists(m)).Select(Function(m) New LocalResourceFile(m)).ToList()
|
||
'实际删除文件
|
||
For Each ModEntity In ModList
|
||
'删除
|
||
Try
|
||
FileUtils.Delete(ModEntity.File.FullName, Not IsShiftPressed)
|
||
Catch ex As OperationCanceledException
|
||
Logger.Warn(ex, "删除 Mod 被主动取消")
|
||
ReloadModList(True)
|
||
Return
|
||
Catch ex As Exception
|
||
Logger.Error(ex, $"删除 Mod 失败({ModEntity.File.FullName})", LogBehavior.Alert)
|
||
IsSuccessful = False
|
||
End Try
|
||
'取消选中
|
||
SelectedMods.Remove(ModEntity.EnabledName)
|
||
'更改 Loader 和 UI 中的列表
|
||
LocalResourceLoader.Output.Remove(ModEntity)
|
||
SearchResult?.Remove(ModEntity)
|
||
ModItems.Remove(ModEntity.EnabledName)
|
||
Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
|
||
If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
|
||
Next
|
||
RefreshBars()
|
||
If Not IsSuccessful Then
|
||
Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
|
||
ReloadModList(True)
|
||
ElseIf PanList.Children.Count = 0 Then
|
||
ReloadModList(True) '删除了全部文件
|
||
Else
|
||
RefreshBars()
|
||
End If
|
||
'显示结果提示
|
||
If Not IsSuccessful Then Return
|
||
If ModList.IsSingle Then
|
||
Hint($"已删除 {ModList.Single.File.Name}!", HintType.Green)
|
||
Else
|
||
Hint($"已删除 {ModList.Count} 个文件!", HintType.Green)
|
||
End If
|
||
Catch ex As OperationCanceledException
|
||
Logger.Warn(ex, "删除 Mod 被主动取消")
|
||
ReloadModList(True)
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "删除 Mod 出现未知错误")
|
||
ReloadModList(True)
|
||
End Try
|
||
LoaderRun(LoaderFolderRunType.UpdateOnly)
|
||
End Sub
|
||
|
||
'取消选择
|
||
Private Sub BtnSelectCancel_Click() Handles BtnSelectCancel.Click
|
||
ChangeAllSelected(False)
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "单个 Mod 项"
|
||
|
||
'详情
|
||
Public Sub Info_Click(sender As Object, e As EventArgs)
|
||
Try
|
||
|
||
Dim ModEntry As LocalResourceFile = CType(If(TypeOf sender Is MyIconButton, sender.Tag, sender), MyLocalModItem).Entry
|
||
'加载失败信息
|
||
If ModEntry.Project IsNot Nothing Then
|
||
'跳转到 Mod 下载页面
|
||
FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.ResourceDetail,
|
||
.Additional = {ModEntry.Project, New List(Of String), PageInstanceLeft.Instance.Version.VanillaName, PageInstanceLeft.Instance.Version.ModLoaders, ResourceTypes.Mod}})
|
||
Else
|
||
'获取信息
|
||
Dim ContentLines As New List(Of String)
|
||
If ModEntry.Description IsNot Nothing Then ContentLines.Add(ModEntry.Description & vbCrLf)
|
||
ContentLines.Add("文件:" & ModEntry.File.Name & "(" & StringUtils.FormatByteSize(ModEntry.File.Length) & ")")
|
||
If ModEntry.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntry.Version)
|
||
Dim DebugInfo As New List(Of String)
|
||
If DebugInfo.Any Then
|
||
ContentLines.Add("")
|
||
ContentLines.AddRange(DebugInfo)
|
||
End If
|
||
'获取用于搜索的 Mod 名称
|
||
Dim ModOriginalName As String = ModEntry.Display.Replace(" ", "+")
|
||
Dim ModSearchName As String = ModOriginalName.Substring(0, 1)
|
||
For i = 1 To ModOriginalName.Count - 1
|
||
Dim IsLastLower As Boolean = ModOriginalName(i - 1).ToString.Lower.Equals(ModOriginalName(i - 1).ToString)
|
||
Dim IsCurrentLower As Boolean = ModOriginalName(i).ToString.Lower.Equals(ModOriginalName(i).ToString)
|
||
If IsLastLower AndAlso Not IsCurrentLower Then
|
||
'上一个字母为小写,这一个字母为大写
|
||
ModSearchName += "+"
|
||
End If
|
||
ModSearchName += ModOriginalName(i)
|
||
Next
|
||
ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine")
|
||
'百科搜索链接
|
||
If MyMsgBox(ContentLines.Join(vbCrLf), ModEntry.Display, "百科搜索", "返回") = 1 Then
|
||
OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
|
||
End If
|
||
End If
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "获取 Mod 详情失败")
|
||
End Try
|
||
End Sub
|
||
'打开文件所在的位置
|
||
Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
|
||
Try
|
||
Dim ListItem As MyLocalModItem = sender.Tag
|
||
OpenExplorer(ListItem.Entry.File.FullName)
|
||
Catch ex As Exception
|
||
Logger.Error(ex, "打开 Mod 文件位置失败")
|
||
End Try
|
||
End Sub
|
||
'删除
|
||
Public Sub Delete_Click(sender As MyIconButton, e As EventArgs)
|
||
Dim ListItem As MyLocalModItem = sender.Tag
|
||
DeleteMods({ListItem.Entry})
|
||
End Sub
|
||
'启用 / 禁用
|
||
Public Sub ED_Click(sender As MyIconButton, e As EventArgs)
|
||
Dim ListItem As MyLocalModItem = sender.Tag
|
||
EDMods({ListItem.Entry}, Not ListItem.Entry.IsEnabled)
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "搜索"
|
||
|
||
Public ReadOnly Property IsSearching As Boolean
|
||
Get
|
||
Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
|
||
End Get
|
||
End Property
|
||
Private SearchResult As List(Of LocalResourceFile)
|
||
Public Sub SearchRun() Handles SearchBox.TextChanged
|
||
If IsSearching Then
|
||
'构造请求
|
||
Dim QueryList As New List(Of SearchEntry(Of LocalResourceFile))
|
||
For Each Entry As LocalResourceFile In LocalResourceLoader.Output
|
||
Dim SearchSources As New List(Of SearchSource)
|
||
SearchSources.Add(New SearchSource(Entry.Display, 1))
|
||
SearchSources.Add(New SearchSource(Entry.File.Name, 1))
|
||
If Entry.Version IsNot Nothing Then
|
||
SearchSources.Add(New SearchSource(Entry.Version, 0.2))
|
||
End If
|
||
If Entry.Description IsNot Nothing AndAlso Entry.Description <> "" Then
|
||
SearchSources.Add(New SearchSource(Entry.Description, 0.4))
|
||
End If
|
||
If Entry.Project IsNot Nothing Then
|
||
If Entry.Project.RawName <> Entry.Display Then SearchSources.Add(New SearchSource(Entry.Project.RawName, 1))
|
||
If Entry.Project.TranslatedName <> Entry.Project.RawName Then SearchSources.Add(New SearchSource(Entry.Project.TranslatedName, 1))
|
||
If Entry.Project.Description <> Entry.Description Then SearchSources.Add(New SearchSource(Entry.Project.Description, 0.4))
|
||
SearchSources.Add(New SearchSource(String.Join("", Entry.Project.Tags), 0.2))
|
||
End If
|
||
QueryList.Add(New SearchEntry(Of LocalResourceFile) With {.Item = Entry, .SearchSource = SearchSources})
|
||
Next
|
||
'进行搜索
|
||
SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList
|
||
End If
|
||
RefreshUI()
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
End Class
|