PageInstanceMod.xaml.vb 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  1. Public Class PageInstanceMod
  2. Implements IRefreshable
  3. #Region "初始化"
  4. Private IsLoad As Boolean = False
  5. Public Sub PageOther_Loaded() Handles Me.Loaded
  6. If FrmMain.PageLast.Page <> FormMain.PageType.ResourceDetail Then PanBack.ScrollToHome()
  7. AniControlEnabled += 1
  8. SelectedMods.Clear()
  9. ReloadModList()
  10. ChangeAllSelected(False)
  11. AniControlEnabled -= 1
  12. '非重复加载部分
  13. If IsLoad Then Return
  14. IsLoad = True
  15. AddHandler FrmMain.KeyDown, AddressOf FrmMain_KeyDown
  16. '调整按钮边距(这玩意儿没法从 XAML 改)
  17. For Each Btn As MyRadioButton In PanFilter.Children
  18. Btn.LabText.Margin = New Thickness(-2, 0, 8, 0)
  19. Next
  20. End Sub
  21. ''' <summary>
  22. ''' 刷新 Mod 列表。
  23. ''' </summary>
  24. Public Sub ReloadModList(Optional ForceReload As Boolean = False)
  25. If Not LoaderRun(If(ForceReload, LoaderFolderRunType.ForceRun, LoaderFolderRunType.RunOnUpdated)) Then Return
  26. Logger.Info("已刷新 Mod 列表")
  27. Filter = FilterType.All
  28. PanBack.ScrollToHome()
  29. SearchBox.Text = ""
  30. End Sub
  31. '强制刷新
  32. Private Sub RefreshSelf() Implements IRefreshable.Refresh
  33. Refresh()
  34. End Sub
  35. Public Shared Sub Refresh()
  36. Try
  37. ResourceProject.Cache.Clear()
  38. ResourceVersion.ProjectFilesCache.Clear()
  39. FileUtils.Delete(PathTemp & "Cache\LocalMod.json")
  40. Logger.Info("由于点击刷新按钮,清理本地 Mod 信息缓存")
  41. Catch ex As Exception
  42. Logger.Warn(ex, "强制刷新时清理本地 Mod 信息缓存失败")
  43. End Try
  44. If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.ReloadModList(True) '无需 Else,还没加载刷个鬼的新
  45. FrmInstanceLeft.ItemMod.Checked = True
  46. Hint("正在刷新……", Log:=False)
  47. End Sub
  48. Private Sub LoaderInit() Handles Me.Initialized
  49. PageLoaderInit(Load, PanLoad, PanAllBack, Nothing, LocalResourceLoader, AddressOf LoadUIFromLoaderOutput, AutoRun:=False)
  50. End Sub
  51. Private Sub Load_Click(sender As Object, e As MouseButtonEventArgs) Handles Load.Click
  52. If LocalResourceLoader.State = LoadState.Failed Then LoaderRun(LoaderFolderRunType.ForceRun)
  53. End Sub
  54. Public Function LoaderRun(Type As LoaderFolderRunType) As Boolean
  55. Return LoaderFolderRun(LocalResourceLoader, PageInstanceLeft.Instance.PathIndie & "mods\", Type)
  56. End Function
  57. #End Region
  58. #Region "UI 化"
  59. ''' <summary>
  60. ''' 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawFileName。
  61. ''' </summary>
  62. Public ModItems As New ConcurrentDictionary(Of String, MyLocalModItem)
  63. ''' <summary>
  64. ''' 将加载器结果的 Mod 列表加载为 UI。
  65. ''' </summary>
  66. Private Sub LoadUIFromLoaderOutput()
  67. Try
  68. '判断应该显示哪一个页面
  69. If LocalResourceLoader.Output.Any() Then
  70. PanBack.Visibility = Visibility.Visible
  71. PanEmpty.Visibility = Visibility.Collapsed
  72. Else
  73. PanEmpty.Visibility = Visibility.Visible
  74. PanBack.Visibility = Visibility.Collapsed
  75. Return
  76. End If
  77. '修改缓存
  78. ModItems.Clear()
  79. For Each ModEntity As LocalResourceFile In LocalResourceLoader.Output
  80. ModItems(ModEntity.EnabledName) = McModListItem(ModEntity)
  81. Next
  82. '显示结果
  83. Filter = FilterType.All
  84. SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
  85. RefreshUI()
  86. Catch ex As Exception
  87. Logger.Error(ex, "加载 Mod 列表 UI 失败")
  88. End Try
  89. End Sub
  90. Private Function McModListItem(Entry As LocalResourceFile) As MyLocalModItem
  91. AniControlEnabled += 1
  92. Dim NewItem As New MyLocalModItem With {.SnapsToDevicePixels = True, .Entry = Entry,
  93. .ButtonHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.EnabledName)}
  94. AddHandler Entry.OnUpdate, Sub() RunInUi(AddressOf NewItem.Refresh)
  95. AniControlEnabled -= 1
  96. Return NewItem
  97. End Function
  98. Private Sub McModContent(sender As MyLocalModItem, e As EventArgs)
  99. '点击事件
  100. AddHandler sender.Changed, AddressOf CheckChanged
  101. AddHandler sender.Click, Sub(ss As MyLocalModItem, ee As EventArgs) ss.Checked = Not ss.Checked
  102. '图标按钮
  103. Dim BtnOpen As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonOpen, .Tag = sender}
  104. BtnOpen.ToolTip = "打开文件位置"
  105. ToolTipService.SetPlacement(BtnOpen, Primitives.PlacementMode.Center)
  106. ToolTipService.SetVerticalOffset(BtnOpen, 30)
  107. ToolTipService.SetHorizontalOffset(BtnOpen, 2)
  108. AddHandler BtnOpen.Click, AddressOf Open_Click
  109. Dim BtnCont As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonInfo, .Tag = sender}
  110. BtnCont.ToolTip = "详情"
  111. ToolTipService.SetPlacement(BtnCont, Primitives.PlacementMode.Center)
  112. ToolTipService.SetVerticalOffset(BtnCont, 30)
  113. ToolTipService.SetHorizontalOffset(BtnCont, 2)
  114. AddHandler BtnCont.Click, AddressOf Info_Click
  115. AddHandler sender.MouseRightButtonUp, AddressOf Info_Click
  116. Dim BtnDelete As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonDelete, .Tag = sender}
  117. BtnDelete.ToolTip = "删除"
  118. ToolTipService.SetPlacement(BtnDelete, Primitives.PlacementMode.Center)
  119. ToolTipService.SetVerticalOffset(BtnDelete, 30)
  120. ToolTipService.SetHorizontalOffset(BtnDelete, 2)
  121. AddHandler BtnDelete.Click, AddressOf Delete_Click
  122. Dim BtnED As New MyIconButton With {.LogoScale = 1, .Logo = If(sender.Entry.IsEnabled, Logo.IconButtonStop, Logo.IconButtonCheck), .Tag = sender}
  123. BtnED.ToolTip = If(sender.Entry.IsEnabled, "禁用", "启用")
  124. ToolTipService.SetPlacement(BtnED, Primitives.PlacementMode.Center)
  125. ToolTipService.SetVerticalOffset(BtnED, 30)
  126. ToolTipService.SetHorizontalOffset(BtnED, 2)
  127. AddHandler BtnED.Click, AddressOf ED_Click
  128. sender.Buttons = {BtnCont, BtnOpen, BtnED, BtnDelete}
  129. End Sub
  130. ''' <summary>
  131. ''' 刷新整个 UI。
  132. ''' </summary>
  133. Public Sub RefreshUI()
  134. If PanList Is Nothing Then Return
  135. Dim ShowingMods = If(IsSearching, SearchResult, If(LocalResourceLoader.Output, New List(Of LocalResourceFile))).Where(Function(m) CanPassFilter(m)).ToList
  136. '重新列出列表
  137. AniControlEnabled += 1
  138. If ShowingMods.Any() Then
  139. PanList.Visibility = Visibility.Visible
  140. PanList.Children.Clear()
  141. For Each TargetMod In ShowingMods
  142. Dim Item As MyLocalModItem = Nothing
  143. If Not ModItems.TryGetValue(TargetMod.EnabledName, Item) Then Continue For
  144. Item.Checked = SelectedMods.Contains(TargetMod.EnabledName) '更新选中状态
  145. PanList.Children.Add(Item)
  146. Next
  147. Else
  148. PanList.Visibility = Visibility.Collapsed
  149. End If
  150. AniControlEnabled -= 1
  151. SelectedMods = SelectedMods.Where(Function(m) ShowingMods.Any(Function(s) s.EnabledName = m)).ToList '取消选中已经不显示的 Mod
  152. RefreshBars()
  153. End Sub
  154. ''' <summary>
  155. ''' 刷新顶栏和底栏显示。
  156. ''' </summary>
  157. Public Sub RefreshBars()
  158. '-----------------
  159. ' 顶部栏
  160. '-----------------
  161. '计数
  162. Dim AnyCount As Integer = 0
  163. Dim EnabledCount As Integer = 0
  164. Dim UpdateCount As Integer = 0
  165. For Each ModItem In If(IsSearching, SearchResult, If(LocalResourceLoader.Output, New List(Of LocalResourceFile)))
  166. AnyCount += 1
  167. If ModItem.HasUpdate Then UpdateCount += 1
  168. If ModItem.IsEnabled Then EnabledCount += 1
  169. Next
  170. Dim DisabledCount As Integer = AnyCount - EnabledCount
  171. '显示
  172. BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
  173. BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
  174. BtnFilterCanUpdate.Visibility = If(Filter = FilterType.CanUpdate OrElse UpdateCount > 0, Visibility.Visible, Visibility.Collapsed)
  175. BtnFilterEnabled.Text = $"启用 ({EnabledCount})"
  176. BtnFilterEnabled.Visibility = If(Filter = FilterType.Enabled OrElse (EnabledCount > 0 AndAlso EnabledCount < AnyCount), Visibility.Visible, Visibility.Collapsed)
  177. BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"
  178. BtnFilterDisabled.Visibility = If(Filter = FilterType.Disabled OrElse DisabledCount > 0, Visibility.Visible, Visibility.Collapsed)
  179. '-----------------
  180. ' 底部栏
  181. '-----------------
  182. '计数
  183. Dim NewCount As Integer = SelectedMods.Count
  184. Dim Selected = NewCount > 0
  185. If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字
  186. '按钮可用性
  187. If Selected Then
  188. Dim HasUpdate As Boolean = False
  189. Dim HasEnabled As Boolean = False
  190. Dim HasDisabled As Boolean = False
  191. For Each ModEntity In LocalResourceLoader.Output
  192. If Not SelectedMods.Contains(ModEntity.EnabledName) Then Continue For
  193. If ModEntity.HasUpdate Then HasUpdate = True
  194. If ModEntity.IsEnabled Then HasEnabled = True
  195. If Not ModEntity.IsEnabled Then HasDisabled = True
  196. Next
  197. BtnSelectDisable.IsEnabled = HasEnabled
  198. BtnSelectEnable.IsEnabled = HasDisabled
  199. BtnSelectUpdate.IsEnabled = HasUpdate
  200. End If
  201. '更新显示状态
  202. If AniControlEnabled = 0 Then
  203. PanListBack.Margin = New Thickness(0, 0, 0, If(Selected, 95, 15))
  204. If Selected Then
  205. '仅在数量增加时播放出现/跳跃动画
  206. If BottomBarShownCount >= NewCount Then
  207. BottomBarShownCount = NewCount
  208. Return
  209. Else
  210. BottomBarShownCount = NewCount
  211. End If
  212. '出现/跳跃动画
  213. CardSelect.Visibility = Visibility.Visible
  214. AniStart({
  215. AaOpacity(CardSelect, 1 - CardSelect.Opacity, 60),
  216. AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, Ease:=New AniEaseOutFluent(AniEasePower.Weak)),
  217. AaTranslateY(CardSelect, 3, 150, 120, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)),
  218. AaTranslateY(CardSelect, -1, 90, 270, Ease:=New AniEaseInoutFluent(AniEasePower.Weak))
  219. }, "Mod Sidebar")
  220. Else
  221. '不重复播放隐藏动画
  222. If BottomBarShownCount = 0 Then Return
  223. BottomBarShownCount = 0
  224. '隐藏动画
  225. AniStart({
  226. AaOpacity(CardSelect, -CardSelect.Opacity, 90),
  227. AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, Ease:=New AniEaseInFluent(AniEasePower.Weak)),
  228. AaCode(Sub() CardSelect.Visibility = Visibility.Collapsed, After:=True)
  229. }, "Mod Sidebar")
  230. End If
  231. Else
  232. AniStop("Mod Sidebar")
  233. BottomBarShownCount = NewCount
  234. If Selected Then
  235. CardSelect.Visibility = Visibility.Visible
  236. CardSelect.Opacity = 1
  237. TransSelect.Y = -25
  238. Else
  239. CardSelect.Visibility = Visibility.Collapsed
  240. CardSelect.Opacity = 0
  241. TransSelect.Y = -10
  242. End If
  243. End If
  244. End Sub
  245. Private BottomBarShownCount As Integer = 0
  246. #End Region
  247. #Region "管理"
  248. ''' <summary>
  249. ''' 打开 Mods 文件夹。
  250. ''' </summary>
  251. Private Sub BtnManageOpen_Click(sender As Object, e As EventArgs) Handles BtnManageOpen.Click, BtnHintOpen.Click
  252. Try
  253. DirectoryUtils.Create(PageInstanceLeft.Instance.PathIndie & "mods\")
  254. OpenExplorer(PageInstanceLeft.Instance.PathIndie & "mods\")
  255. Catch ex As Exception
  256. Logger.Error(ex, "打开 Mods 文件夹失败", LogBehavior.Alert)
  257. End Try
  258. End Sub
  259. ''' <summary>
  260. ''' 全选。
  261. ''' </summary>
  262. Private Sub BtnManageSelectAll_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageSelectAll.Click
  263. ChangeAllSelected(SelectedMods.Count < PanList.Children.Count)
  264. End Sub
  265. ''' <summary>
  266. ''' 安装 Mod。
  267. ''' </summary>
  268. Private Sub BtnManageInstall_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageInstall.Click, BtnHintInstall.Click
  269. Dim FileList = Dialogs.SelectFile("选择要安装的 Mod", True, filter:={({"jar", "litemod", "disabled", "old"}, "Mod 文件")})
  270. If Not FileList.Any Then Return
  271. InstallMods(FileList)
  272. End Sub
  273. ''' <summary>
  274. ''' 尝试安装 Mod。
  275. ''' 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。
  276. ''' </summary>
  277. Public Shared Function InstallMods(FilePathList As IEnumerable(Of String)) As Boolean
  278. Dim Extension As String = FilePathList.First.AfterLast(".").Lower
  279. '检查文件扩展名
  280. If Not {"jar", "litemod", "disabled", "old"}.Any(Function(t) t = Extension) Then Return False
  281. Logger.Info("文件为 jar/litemod 格式,尝试作为 Mod 安装")
  282. '检查回收站:回收站中的文件有错误的文件名
  283. If FilePathList.First.Contains(":\$RECYCLE.BIN\") Then
  284. Hint("请先将文件从回收站还原,再尝试安装!", HintType.Red)
  285. Return True
  286. End If
  287. '获取并检查目标版本
  288. Dim Instance As McInstance = McInstanceSelected
  289. If FrmMain.PageCurrent = FormMain.PageType.InstanceSetup Then Instance = PageInstanceLeft.Instance
  290. If FrmMain.PageCurrent = FormMain.PageType.InstanceSelect OrElse Instance Is Nothing OrElse Not Instance.Modable Then
  291. '正在选择版本,或当前版本不能安装 Mod
  292. Hint("若要安装 Mod,请先选择一个可以安装 Mod 的版本!")
  293. ElseIf Not (FrmMain.PageCurrent = FormMain.PageType.InstanceSetup AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.InstanceMod) Then
  294. '未处于 Mod 管理页面
  295. If MyMsgBox($"是否要将这{If(FilePathList.IsSingle, "个", "些")}文件作为 Mod 安装到 {Instance.Name}?", "Mod 安装确认", "确定", "取消") = 1 Then GoTo Install
  296. Else
  297. '处于 Mod 管理页面
  298. Install:
  299. Try
  300. For Each ModFile In FilePathList
  301. Dim NewFileName = PathUtils.GetLastPart(ModFile).Replace(".disabled", "").Replace(".old", "")
  302. If Not NewFileName.Contains(".") Then NewFileName += ".jar" '#4227
  303. FileUtils.Copy(ModFile, Instance.PathIndie & "mods\" & NewFileName)
  304. Next
  305. If FilePathList.IsSingle Then
  306. Hint($"已安装 {PathUtils.GetLastPart(FilePathList.First).Replace(".disabled", "").Replace(".old", "")}!", HintType.Green)
  307. Else
  308. Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Green)
  309. End If
  310. '刷新列表
  311. If FrmMain.PageCurrent = FormMain.PageType.InstanceSetup AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.InstanceMod Then
  312. LoaderFolderRun(LocalResourceLoader, Instance.PathIndie & "mods\", LoaderFolderRunType.ForceRun)
  313. End If
  314. Catch ex As Exception
  315. Logger.Error(ex, "复制 Mod 文件失败", LogBehavior.Alert)
  316. End Try
  317. End If
  318. Return True
  319. End Function
  320. ''' <summary>
  321. ''' 下载 Mod。
  322. ''' </summary>
  323. Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click
  324. '预设置当前版本和搜索框内容
  325. PageResource.TargetInstance = PageInstanceLeft.Instance
  326. PageResource.TargetName = SearchBox.Text
  327. FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod)
  328. End Sub
  329. #End Region
  330. #Region "选择"
  331. '选择的 Mod 的路径(不含 .disabled 和 .old)
  332. Public SelectedMods As New List(Of String)
  333. '单项切换选择状态
  334. Public Sub CheckChanged(sender As MyLocalModItem, e As RouteEventArgs)
  335. If AniControlEnabled <> 0 Then Return
  336. '更新选择了的内容
  337. Dim SelectedKey As String = sender.Entry.EnabledName
  338. If sender.Checked Then
  339. If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey)
  340. Else
  341. SelectedMods.Remove(SelectedKey)
  342. End If
  343. RefreshBars()
  344. End Sub
  345. '切换所有项的选择状态
  346. Private Sub ChangeAllSelected(Value As Boolean)
  347. AniControlEnabled += 1
  348. SelectedMods.Clear()
  349. For Each Item As MyLocalModItem In ModItems.Values
  350. '#4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断
  351. Dim ShouldSelected As Boolean = Value AndAlso PanList.Children.Contains(Item)
  352. Item.Checked = ShouldSelected
  353. If ShouldSelected Then SelectedMods.Add(Item.Entry.EnabledName)
  354. Next
  355. AniControlEnabled -= 1
  356. RefreshBars()
  357. End Sub
  358. Private Sub UnselectedAllWithAnimation() Handles Load.StateChanged, Me.PageExit
  359. Dim CacheAniControlEnabled = AniControlEnabled
  360. AniControlEnabled = 0
  361. ChangeAllSelected(False)
  362. AniControlEnabled += CacheAniControlEnabled
  363. End Sub
  364. Private Sub FrmMain_KeyDown(sender As Object, e As KeyEventArgs) '监听自己的事件的话进入页面后不点击右侧控件就没办法监听到事件 (#4311)
  365. If FrmMain.PageRight IsNot Me Then Return
  366. If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeAllSelected(True)
  367. End Sub
  368. Private Sub SearchBox_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles SearchBox.PreviewKeyDown
  369. 'Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获
  370. If SearchBox.Text.Any Then Return
  371. If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then PanBack.Focus()
  372. End Sub
  373. #End Region
  374. #Region "筛选"
  375. Private _Filter As FilterType = FilterType.All
  376. Public Property Filter As FilterType
  377. Get
  378. Return _Filter
  379. End Get
  380. Set(value As FilterType)
  381. If _Filter = value Then Return
  382. _Filter = value
  383. Select Case value
  384. Case FilterType.All
  385. BtnFilterAll.Checked = True
  386. Case FilterType.Enabled
  387. BtnFilterEnabled.Checked = True
  388. Case FilterType.Disabled
  389. BtnFilterDisabled.Checked = True
  390. Case Else
  391. BtnFilterCanUpdate.Checked = True
  392. End Select
  393. RefreshUI()
  394. End Set
  395. End Property
  396. Public Enum FilterType As Integer
  397. All = 0
  398. Enabled = 1
  399. Disabled = 2
  400. CanUpdate = 3
  401. End Enum
  402. ''' <summary>
  403. ''' 检查该 Mod 项是否符合当前筛选的类别。
  404. ''' </summary>
  405. Private Function CanPassFilter(CheckingMod As LocalResourceFile) As Boolean
  406. Select Case Filter
  407. Case FilterType.All
  408. Return True
  409. Case FilterType.Enabled
  410. Return CheckingMod.IsEnabled
  411. Case FilterType.Disabled
  412. Return Not CheckingMod.IsEnabled
  413. Case FilterType.CanUpdate
  414. Return CheckingMod.HasUpdate
  415. Case Else
  416. Return False
  417. End Select
  418. End Function
  419. '点击筛选项触发的改变
  420. Private Sub ChangeFilter(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnFilterAll.Check, BtnFilterCanUpdate.Check, BtnFilterDisabled.Check, BtnFilterEnabled.Check
  421. Filter = sender.Tag
  422. RefreshUI()
  423. End Sub
  424. #End Region
  425. #Region "下边栏"
  426. '启用 / 禁用
  427. Private Sub BtnSelectED_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click
  428. EDMods(LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)),
  429. Not sender.Equals(BtnSelectDisable))
  430. ChangeAllSelected(False)
  431. End Sub
  432. Private Sub EDMods(ModList As IEnumerable(Of LocalResourceFile), IsEnable As Boolean)
  433. Dim IsSuccessful As Boolean = True
  434. For Each ModE In ModList.ToList
  435. Dim ModEntity = ModE '仅用于去除迭代变量无法修改的限制
  436. Dim NewPath As String = Nothing
  437. If ModEntity.IsEnabled AndAlso Not IsEnable Then
  438. '禁用
  439. NewPath = PathUtils.RemoveExtendedPrefix(ModEntity.File.FullName) & If(FileUtils.Exists(ModEntity.File.FullName & ".old"), ".old", ".disabled")
  440. ElseIf Not ModEntity.IsEnabled AndAlso IsEnable Then
  441. '启用
  442. NewPath = ModEntity.EnabledFullName
  443. Else
  444. Continue For
  445. End If
  446. '重命名
  447. Try
  448. If FileUtils.Exists(NewPath) Then
  449. If FileUtils.Exists(ModEntity.File.FullName) Then
  450. '同时存在两个名称的 Mod
  451. If HashUtils.ComputeFromFile(ModEntity.File.FullName, HashUtils.HashMethod.Md5) <> HashUtils.ComputeFromFile(NewPath, HashUtils.HashMethod.Md5) Then
  452. MyMsgBox($"目前同时存在启用和禁用的两个 Mod 文件:{vbCrLf} - {PathUtils.GetLastPart(NewPath)}{vbCrLf} - {ModEntity.File.Name}{vbCrLf}{vbCrLf}注意,这两个文件的内容并不相同。{vbCrLf}在手动删除或重命名其中一个文件后,才能继续操作。", "存在文件冲突")
  453. Continue For
  454. End If
  455. Else
  456. '已经重命名过了
  457. Logger.Warn("Mod 的状态已被切换")
  458. Continue For
  459. End If
  460. End If
  461. FileUtils.Delete(NewPath)
  462. FileUtils.Move(ModEntity.File.FullName, NewPath)
  463. Catch ex As FileNotFoundException
  464. Logger.Error(ex, $"未找到需要重命名的 Mod({If(ModEntity.File.FullName, "null")})")
  465. ReloadModList(True)
  466. Return
  467. Catch ex As Exception
  468. Logger.Warn(ex, $"重命名 Mod 失败({If(ModEntity.File.FullName, "null")})")
  469. IsSuccessful = False
  470. End Try
  471. '更改 Loader 中的列表
  472. Dim NewModEntity As New LocalResourceFile(NewPath)
  473. NewModEntity.FromJson(ModEntity.ToJson)
  474. If LocalResourceLoader.Output.Contains(ModEntity) Then
  475. Dim IndexOfLoader As Integer = LocalResourceLoader.Output.IndexOf(ModEntity)
  476. LocalResourceLoader.Output.RemoveAt(IndexOfLoader)
  477. LocalResourceLoader.Output.Insert(IndexOfLoader, NewModEntity)
  478. End If
  479. If SearchResult IsNot Nothing AndAlso SearchResult.Contains(ModEntity) Then '#4862
  480. Dim IndexOfResult As Integer = SearchResult.IndexOf(ModEntity)
  481. SearchResult.Remove(ModEntity)
  482. SearchResult.Insert(IndexOfResult, NewModEntity)
  483. End If
  484. '更改 UI 中的列表
  485. Dim NewItem As MyLocalModItem = McModListItem(NewModEntity)
  486. ModItems(ModEntity.EnabledName) = NewItem
  487. Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry Is ModEntity))
  488. If IndexOfUi = -1 Then Continue For '因为未知原因 Mod 的状态已经切换完了
  489. PanList.Children.RemoveAt(IndexOfUi)
  490. PanList.Children.Insert(IndexOfUi, NewItem)
  491. NewItem.UpdateLayout() '#7861
  492. Next
  493. If IsSuccessful Then
  494. RefreshBars()
  495. Else
  496. Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
  497. ReloadModList(True)
  498. End If
  499. LoaderRun(LoaderFolderRunType.UpdateOnly)
  500. End Sub
  501. '更新
  502. Private Sub BtnSelectUpdate_Click() Handles BtnSelectUpdate.Click
  503. Dim UpdateList As List(Of LocalResourceFile) = LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName) AndAlso m.HasUpdate).ToList()
  504. If Not UpdateList.Any() Then Return
  505. UpdateMods(UpdateList)
  506. ChangeAllSelected(False)
  507. End Sub
  508. ''' <summary>
  509. ''' 记录正在进行 Mod 更新的 mods 文件夹路径。
  510. ''' </summary>
  511. Public Shared UpdatingInstanceModFolders As New List(Of String)
  512. Public Sub UpdateMods(ModList As IEnumerable(Of LocalResourceFile))
  513. '更新前警告
  514. If Not Settings.Get(Of Boolean)("HintUpdateMod") OrElse ModList.Count >= 15 Then
  515. If MyMsgBox($"新版本 Mod 可能不兼容旧存档或者其他 Mod,这可能导致游戏崩溃,甚至永久损坏存档!{vbCrLf}如果你在游玩整合包,请千万不要自行更新 Mod!{vbCrLf}{vbCrLf}在更新前,请先备份存档,并检查 Mod 的更新日志。{vbCrLf}如果更新后出现问题,你也可以在回收站找回更新前的 Mod。", "Mod 更新警告", "我已了解风险,继续更新", "取消", IsWarn:=True) = 1 Then
  516. Settings.Set("HintUpdateMod", True)
  517. Else
  518. Return
  519. End If
  520. End If
  521. Try
  522. '构造下载信息
  523. ModList = ModList.ToList() '防止刷新影响迭代器
  524. Dim FileList As New List(Of NetFile)
  525. Dim FileCopyList As New Dictionary(Of String, String)
  526. For Each Entry As LocalResourceFile In ModList
  527. Dim File As ResourceVersion = Entry.UpdateFile
  528. If Not File.DownloadAvailable Then Continue For
  529. '确认更新后的文件名
  530. Dim CurrentReplaceName = Entry.ProjectVersion.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
  531. Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
  532. Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
  533. Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
  534. Dim Shortened As Boolean = False
  535. Do While True '移除前导相同部分(不能移除所有相同项,这会导致例如 1.2-forge-2 和 1.3-forge-3 中间的 forge 被去掉,导致尝试替换 1.2-2)
  536. If Not CurrentSegs.Any() OrElse Not NewestSegs.Any() Then Exit Do
  537. If CurrentSegs.First <> NewestSegs.First Then Exit Do
  538. CurrentSegs.RemoveAt(0)
  539. NewestSegs.RemoveAt(0)
  540. Shortened = True
  541. Loop
  542. Do While True '移除后导相同部分
  543. If Not CurrentSegs.Any() OrElse Not NewestSegs.Any() Then Exit Do
  544. If CurrentSegs.Last <> NewestSegs.Last Then Exit Do
  545. CurrentSegs.RemoveAt(CurrentSegs.Count - 1)
  546. NewestSegs.RemoveAt(NewestSegs.Count - 1)
  547. Shortened = True
  548. Loop
  549. If Shortened AndAlso CurrentSegs.Any() AndAlso NewestSegs.Any() Then
  550. CurrentReplaceName = CurrentSegs.Join("-")
  551. NewestReplaceName = NewestSegs.Join("-")
  552. End If
  553. '添加到下载列表
  554. Dim TempAddress As String = PathTemp & "mods\" & Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName) '需要下载到 mods 文件夹,以便重复文件检查时识别
  555. Dim RealAddress As String = Path.Combine(PathUtils.RemoveLastPart(Entry.File.FullName), Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName))
  556. FileList.Add(File.ToNetFile(TempAddress, ResourceVersion.DownloadReason.Update, PageInstanceLeft.Instance.Version.VanillaName, PageInstanceLeft.Instance.Version.ModLoaders))
  557. FileCopyList(TempAddress) = RealAddress
  558. Next
  559. '构造加载器
  560. Dim InstallLoaders As New List(Of LoaderBase)
  561. Dim FinishedFileNames As New List(Of String)
  562. InstallLoaders.Add(New LoaderDownload("下载新版 Mod 文件", FileList) With {.ProgressWeight = ModList.Count * 1.5}) '每个 Mod 需要 1.5s
  563. InstallLoaders.Add(New LoaderTask(Of Integer, Integer)("替换旧版 Mod 文件",
  564. Sub()
  565. Try
  566. For Each Entry As LocalResourceFile In ModList
  567. If FileUtils.Exists(Entry.File.FullName) Then
  568. FileUtils.Delete(Entry.File.FullName, True)
  569. Else
  570. Logger.Warn($"未找到更新前的 Mod 文件,跳过对它的删除:{Entry.File.FullName}")
  571. End If
  572. Next
  573. For Each Entry As KeyValuePair(Of String, String) In FileCopyList
  574. If FileUtils.Exists(Entry.Value) Then
  575. FileUtils.Delete(Entry.Value, True)
  576. Logger.Warn($"更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}")
  577. End If
  578. If DirectoryUtils.Exists(PathUtils.RemoveLastPart(Entry.Value)) Then
  579. FileUtils.Move(Entry.Key, Entry.Value)
  580. FinishedFileNames.Add(PathUtils.GetLastPart(Entry.Value))
  581. Else
  582. Logger.Warn($"更新后的目标文件夹已被删除:{Entry.Value}")
  583. End If
  584. Next
  585. Catch ex As OperationCanceledException
  586. Logger.Warn(ex, "替换旧版 Mod 文件时被主动取消")
  587. End Try
  588. End Sub))
  589. '结束处理
  590. Dim Loader As New LoaderCombo(Of IEnumerable(Of LocalResourceFile))("Mod 更新:" & PageInstanceLeft.Instance.Name, InstallLoaders)
  591. Dim PathMods As String = PageInstanceLeft.Instance.PathIndie & "mods\"
  592. Loader.OnStateChanged =
  593. Sub()
  594. '结果提示
  595. Select Case Loader.State
  596. Case LoadState.Finished
  597. Select Case FinishedFileNames.Count
  598. Case 0 '一般是由于 Mod 文件被占用,然后玩家主动取消
  599. Logger.Info($"没有 Mod 被成功更新")
  600. Case 1
  601. Hint($"已成功更新 {FinishedFileNames.Single}!", HintType.Green)
  602. Case Else
  603. Hint($"已成功更新 {FinishedFileNames.Count} 个 Mod!", HintType.Green)
  604. End Select
  605. Case LoadState.Failed
  606. Hint("Mod 更新失败:" & Loader.Error.GetDisplay(False), HintType.Red)
  607. Case LoadState.Interrupted
  608. Hint("Mod 更新已中止!", HintType.Blue)
  609. Case Else
  610. Return
  611. End Select
  612. Logger.Info($"已从正在进行 Mod 更新的文件夹列表移除:{PathMods}")
  613. UpdatingInstanceModFolders.Remove(PathMods)
  614. '清理缓存
  615. RunInNewThread(
  616. Sub()
  617. Try
  618. For Each TempFile In FileCopyList.Keys
  619. FileUtils.Delete(TempFile)
  620. Next
  621. Catch ex As Exception
  622. Logger.Warn(ex, "清理 Mod 更新缓存失败")
  623. End Try
  624. End Sub, "Clean Mod Update Cache", ThreadPriority.BelowNormal)
  625. End Sub
  626. '启动加载器
  627. Logger.Info($"开始更新 {ModList.Count} 个 Mod:{PathMods}")
  628. UpdatingInstanceModFolders.Add(PathMods)
  629. Loader.Start()
  630. LoaderTaskbarAdd(Loader)
  631. FrmMain.BtnExtraDownload.ShowRefresh()
  632. FrmMain.BtnExtraDownload.Ribble()
  633. ReloadModList(True)
  634. Catch ex As Exception
  635. Logger.Warn(ex, "初始化 Mod 更新失败")
  636. End Try
  637. End Sub
  638. '删除
  639. Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click
  640. DeleteMods(LocalResourceLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)))
  641. ChangeAllSelected(False)
  642. End Sub
  643. Private Sub DeleteMods(ModsToDelete As IEnumerable(Of LocalResourceFile))
  644. Try
  645. Dim IsSuccessful As Boolean = True
  646. Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
  647. '确认需要删除的文件
  648. Dim ModList = ModsToDelete.SelectMany(
  649. Function(Target As LocalResourceFile)
  650. If Target.IsEnabled Then
  651. Return {Target.File.FullName, Target.File.FullName & If(FileUtils.Exists(Target.File.FullName & ".old"), ".old", ".disabled")}
  652. Else
  653. Return {Target.File.FullName, Target.EnabledFullName}
  654. End If
  655. End Function).Distinct.Where(Function(m) FileUtils.Exists(m)).Select(Function(m) New LocalResourceFile(m)).ToList()
  656. '实际删除文件
  657. For Each ModEntity In ModList
  658. '删除
  659. Try
  660. FileUtils.Delete(ModEntity.File.FullName, Not IsShiftPressed)
  661. Catch ex As OperationCanceledException
  662. Logger.Warn(ex, "删除 Mod 被主动取消")
  663. ReloadModList(True)
  664. Return
  665. Catch ex As Exception
  666. Logger.Error(ex, $"删除 Mod 失败({ModEntity.File.FullName})", LogBehavior.Alert)
  667. IsSuccessful = False
  668. End Try
  669. '取消选中
  670. SelectedMods.Remove(ModEntity.EnabledName)
  671. '更改 Loader 和 UI 中的列表
  672. LocalResourceLoader.Output.Remove(ModEntity)
  673. SearchResult?.Remove(ModEntity)
  674. ModItems.Remove(ModEntity.EnabledName)
  675. Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
  676. If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
  677. Next
  678. RefreshBars()
  679. If Not IsSuccessful Then
  680. Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
  681. ReloadModList(True)
  682. ElseIf PanList.Children.Count = 0 Then
  683. ReloadModList(True) '删除了全部文件
  684. Else
  685. RefreshBars()
  686. End If
  687. '显示结果提示
  688. If Not IsSuccessful Then Return
  689. If IsShiftPressed Then
  690. If ModList.IsSingle Then
  691. Hint($"已彻底删除 {ModList.Single.File.Name}!", HintType.Green)
  692. Else
  693. Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Green)
  694. End If
  695. Else
  696. If ModList.IsSingle Then
  697. Hint($"已将 {ModList.Single.File.Name} 删除到回收站!", HintType.Green)
  698. Else
  699. Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Green)
  700. End If
  701. End If
  702. Catch ex As OperationCanceledException
  703. Logger.Warn(ex, "删除 Mod 被主动取消")
  704. ReloadModList(True)
  705. Catch ex As Exception
  706. Logger.Error(ex, "删除 Mod 出现未知错误")
  707. ReloadModList(True)
  708. End Try
  709. LoaderRun(LoaderFolderRunType.UpdateOnly)
  710. End Sub
  711. '取消选择
  712. Private Sub BtnSelectCancel_Click() Handles BtnSelectCancel.Click
  713. ChangeAllSelected(False)
  714. End Sub
  715. #End Region
  716. #Region "单个 Mod 项"
  717. '详情
  718. Public Sub Info_Click(sender As Object, e As EventArgs)
  719. Try
  720. Dim ModEntry As LocalResourceFile = CType(If(TypeOf sender Is MyIconButton, sender.Tag, sender), MyLocalModItem).Entry
  721. '加载失败信息
  722. If ModEntry.Project IsNot Nothing Then
  723. '跳转到 Mod 下载页面
  724. FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.ResourceDetail,
  725. .Additional = {ModEntry.Project, New List(Of String), PageInstanceLeft.Instance.Version.VanillaName, PageInstanceLeft.Instance.Version.ModLoaders, ResourceTypes.Mod}})
  726. Else
  727. '获取信息
  728. Dim ContentLines As New List(Of String)
  729. If ModEntry.Description IsNot Nothing Then ContentLines.Add(ModEntry.Description & vbCrLf)
  730. ContentLines.Add("文件:" & ModEntry.File.Name & "(" & FormatFileSize(ModEntry.File.Length) & ")")
  731. If ModEntry.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntry.Version)
  732. Dim DebugInfo As New List(Of String)
  733. If DebugInfo.Any Then
  734. ContentLines.Add("")
  735. ContentLines.AddRange(DebugInfo)
  736. End If
  737. '获取用于搜索的 Mod 名称
  738. Dim ModOriginalName As String = ModEntry.Display.Replace(" ", "+")
  739. Dim ModSearchName As String = ModOriginalName.Substring(0, 1)
  740. For i = 1 To ModOriginalName.Count - 1
  741. Dim IsLastLower As Boolean = ModOriginalName(i - 1).ToString.Lower.Equals(ModOriginalName(i - 1).ToString)
  742. Dim IsCurrentLower As Boolean = ModOriginalName(i).ToString.Lower.Equals(ModOriginalName(i).ToString)
  743. If IsLastLower AndAlso Not IsCurrentLower Then
  744. '上一个字母为小写,这一个字母为大写
  745. ModSearchName += "+"
  746. End If
  747. ModSearchName += ModOriginalName(i)
  748. Next
  749. ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine")
  750. '百科搜索链接
  751. If MyMsgBox(ContentLines.Join(vbCrLf), ModEntry.Display, "百科搜索", "返回") = 1 Then
  752. OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
  753. End If
  754. End If
  755. Catch ex As Exception
  756. Logger.Error(ex, "获取 Mod 详情失败")
  757. End Try
  758. End Sub
  759. '打开文件所在的位置
  760. Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
  761. Try
  762. Dim ListItem As MyLocalModItem = sender.Tag
  763. OpenExplorer(ListItem.Entry.File.FullName)
  764. Catch ex As Exception
  765. Logger.Error(ex, "打开 Mod 文件位置失败")
  766. End Try
  767. End Sub
  768. '删除
  769. Public Sub Delete_Click(sender As MyIconButton, e As EventArgs)
  770. Dim ListItem As MyLocalModItem = sender.Tag
  771. DeleteMods({ListItem.Entry})
  772. End Sub
  773. '启用 / 禁用
  774. Public Sub ED_Click(sender As MyIconButton, e As EventArgs)
  775. Dim ListItem As MyLocalModItem = sender.Tag
  776. EDMods({ListItem.Entry}, Not ListItem.Entry.IsEnabled)
  777. End Sub
  778. #End Region
  779. #Region "搜索"
  780. Public ReadOnly Property IsSearching As Boolean
  781. Get
  782. Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
  783. End Get
  784. End Property
  785. Private SearchResult As List(Of LocalResourceFile)
  786. Public Sub SearchRun() Handles SearchBox.TextChanged
  787. If IsSearching Then
  788. '构造请求
  789. Dim QueryList As New List(Of SearchEntry(Of LocalResourceFile))
  790. For Each Entry As LocalResourceFile In LocalResourceLoader.Output
  791. Dim SearchSources As New List(Of SearchSource)
  792. SearchSources.Add(New SearchSource(Entry.Display, 1))
  793. SearchSources.Add(New SearchSource(Entry.File.Name, 1))
  794. If Entry.Version IsNot Nothing Then
  795. SearchSources.Add(New SearchSource(Entry.Version, 0.2))
  796. End If
  797. If Entry.Description IsNot Nothing AndAlso Entry.Description <> "" Then
  798. SearchSources.Add(New SearchSource(Entry.Description, 0.4))
  799. End If
  800. If Entry.Project IsNot Nothing Then
  801. If Entry.Project.RawName <> Entry.Display Then SearchSources.Add(New SearchSource(Entry.Project.RawName, 1))
  802. If Entry.Project.TranslatedName <> Entry.Project.RawName Then SearchSources.Add(New SearchSource(Entry.Project.TranslatedName, 1))
  803. If Entry.Project.Description <> Entry.Description Then SearchSources.Add(New SearchSource(Entry.Project.Description, 0.4))
  804. SearchSources.Add(New SearchSource(String.Join("", Entry.Project.Tags), 0.2))
  805. End If
  806. QueryList.Add(New SearchEntry(Of LocalResourceFile) With {.Item = Entry, .SearchSource = SearchSources})
  807. Next
  808. '进行搜索
  809. SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList
  810. End If
  811. RefreshUI()
  812. End Sub
  813. #End Region
  814. End Class