PageVersionMod.xaml.vb 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. Public Class PageVersionMod
  2. #Region "初始化"
  3. Private IsLoad As Boolean = False
  4. Public Sub PageOther_Loaded() Handles Me.Loaded
  5. If FrmMain.PageLast.Page <> FormMain.PageType.CompDetail Then PanBack.ScrollToHome()
  6. AniControlEnabled += 1
  7. SelectedMods.Clear()
  8. ReloadModList()
  9. ChangeAllSelected(False)
  10. AniControlEnabled -= 1
  11. '非重复加载部分
  12. If IsLoad Then Exit Sub
  13. IsLoad = True
  14. '调整按钮边距(这玩意儿没法从 XAML 改)
  15. For Each Btn As MyRadioButton In PanFilter.Children
  16. Btn.LabText.Margin = New Thickness(-2, 0, 8, 0)
  17. Next
  18. #If DEBUG Then
  19. BtnManageCheck.Visibility = Visibility.Visible
  20. #End If
  21. End Sub
  22. ''' <summary>
  23. ''' 刷新 Mod 列表。
  24. ''' </summary>
  25. Public Sub ReloadModList(Optional ForceReload As Boolean = False)
  26. If LoaderRun(If(ForceReload, LoaderFolderRunType.ForceRun, LoaderFolderRunType.RunOnUpdated)) Then
  27. Log("[System] 已刷新 Mod 列表")
  28. Filter = FilterType.All
  29. PanBack.ScrollToHome()
  30. SearchBox.Text = ""
  31. End If
  32. End Sub
  33. Private Sub LoaderInit() Handles Me.Initialized
  34. PageLoaderInit(Load, PanLoad, PanAllBack, Nothing, McModLoader, AddressOf LoadUIFromLoaderOutput, AutoRun:=False)
  35. End Sub
  36. Private Sub Load_Click(sender As Object, e As MouseButtonEventArgs) Handles Load.Click
  37. If McModLoader.State = LoadState.Failed Then
  38. LoaderRun(LoaderFolderRunType.ForceRun)
  39. End If
  40. End Sub
  41. Public Function LoaderRun(Type As LoaderFolderRunType) As Boolean
  42. Return LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", Type)
  43. End Function
  44. #End Region
  45. #Region "UI 化"
  46. ''' <summary>
  47. ''' 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawFileName。
  48. ''' </summary>
  49. Public ModItems As New Dictionary(Of String, MyLocalModItem)
  50. ''' <summary>
  51. ''' 将加载器结果的 Mod 列表加载为 UI。
  52. ''' </summary>
  53. Private Sub LoadUIFromLoaderOutput()
  54. Try
  55. '判断应该显示哪一个页面
  56. If McModLoader.Output.Any() Then
  57. PanBack.Visibility = Visibility.Visible
  58. PanEmpty.Visibility = Visibility.Collapsed
  59. Else
  60. PanEmpty.Visibility = Visibility.Visible
  61. PanBack.Visibility = Visibility.Collapsed
  62. Exit Sub
  63. End If
  64. '修改缓存
  65. ModItems.Clear()
  66. For Each ModEntity As McMod In McModLoader.Output
  67. ModItems(ModEntity.RawFileName) = McModListItem(ModEntity)
  68. Next
  69. '显示结果
  70. Filter = FilterType.All
  71. SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
  72. RefreshUI()
  73. Catch ex As Exception
  74. Log(ex, "加载 Mod 列表 UI 失败", LogLevel.Feedback)
  75. End Try
  76. End Sub
  77. Private Function McModListItem(Entry As McMod) As MyLocalModItem
  78. AniControlEnabled += 1
  79. Dim NewItem As New MyLocalModItem With {.SnapsToDevicePixels = True, .Entry = Entry,
  80. .ButtonHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.RawFileName)}
  81. AddHandler Entry.OnCompUpdate, AddressOf NewItem.Refresh
  82. NewItem.Refresh()
  83. AniControlEnabled -= 1
  84. Return NewItem
  85. End Function
  86. Private Sub McModContent(sender As MyLocalModItem, e As EventArgs)
  87. '点击事件
  88. AddHandler sender.Changed, AddressOf CheckChanged
  89. AddHandler sender.Click, Sub(ss As MyLocalModItem, ee As EventArgs) ss.Checked = Not ss.Checked
  90. '图标按钮
  91. Dim BtnOpen As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonOpen, .Tag = sender}
  92. BtnOpen.ToolTip = "打开文件位置"
  93. ToolTipService.SetPlacement(BtnOpen, Primitives.PlacementMode.Center)
  94. ToolTipService.SetVerticalOffset(BtnOpen, 30)
  95. ToolTipService.SetHorizontalOffset(BtnOpen, 2)
  96. AddHandler BtnOpen.Click, AddressOf Open_Click
  97. Dim BtnCont As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonInfo, .Tag = sender}
  98. BtnCont.ToolTip = "详情"
  99. ToolTipService.SetPlacement(BtnCont, Primitives.PlacementMode.Center)
  100. ToolTipService.SetVerticalOffset(BtnCont, 30)
  101. ToolTipService.SetHorizontalOffset(BtnCont, 2)
  102. AddHandler BtnCont.Click, AddressOf Info_Click
  103. AddHandler sender.MouseRightButtonUp, AddressOf Info_Click
  104. Dim BtnDelete As New MyIconButton With {.LogoScale = 1, .Logo = Logo.IconButtonDelete, .Tag = sender}
  105. BtnDelete.ToolTip = "删除"
  106. ToolTipService.SetPlacement(BtnDelete, Primitives.PlacementMode.Center)
  107. ToolTipService.SetVerticalOffset(BtnDelete, 30)
  108. ToolTipService.SetHorizontalOffset(BtnDelete, 2)
  109. AddHandler BtnDelete.Click, AddressOf Delete_Click
  110. Dim BtnED As New MyIconButton With {.LogoScale = 1, .Logo = If(sender.Entry.State = McMod.McModState.Fine, Logo.IconButtonStop, Logo.IconButtonCheck), .Tag = sender}
  111. BtnED.ToolTip = If(sender.Entry.State = McMod.McModState.Fine, "禁用", "启用")
  112. ToolTipService.SetPlacement(BtnED, Primitives.PlacementMode.Center)
  113. ToolTipService.SetVerticalOffset(BtnED, 30)
  114. ToolTipService.SetHorizontalOffset(BtnED, 2)
  115. AddHandler BtnED.Click, AddressOf ED_Click
  116. If sender.Entry.State = McMod.McModState.Unavailable Then
  117. sender.Buttons = {BtnCont, BtnOpen, BtnDelete}
  118. Else
  119. sender.Buttons = {BtnCont, BtnOpen, BtnED, BtnDelete}
  120. End If
  121. End Sub
  122. ''' <summary>
  123. ''' 刷新整个 UI。
  124. ''' </summary>
  125. Public Sub RefreshUI()
  126. If PanList Is Nothing Then Exit Sub
  127. Dim ShowMods = GetShowingMods(True).ToList()
  128. '重新列出列表
  129. AniControlEnabled += 1
  130. If ShowMods.Any() Then
  131. PanList.Visibility = Visibility.Visible
  132. PanList.Children.Clear()
  133. For Each TargetMod In ShowMods
  134. Dim Item As MyLocalModItem = ModItems(TargetMod.RawFileName)
  135. Item.Checked = SelectedMods.Contains(TargetMod.RawFileName) '更新选中状态
  136. PanList.Children.Add(Item)
  137. Next
  138. Else
  139. PanList.Visibility = Visibility.Collapsed
  140. End If
  141. AniControlEnabled -= 1
  142. SelectedMods = SelectedMods.Where(Function(m) ShowMods.Any(Function(s) s.RawFileName = m)).ToList '取消选中已经不显示的 Mod
  143. RefreshBars()
  144. End Sub
  145. ''' <summary>
  146. ''' 刷新顶栏和底栏显示。
  147. ''' </summary>
  148. Public Sub RefreshBars()
  149. '-----------------
  150. ' 顶部栏
  151. '-----------------
  152. '计数
  153. Dim AnyCount As Integer = 0
  154. Dim EnabledCount As Integer = 0
  155. Dim DisabledCount As Integer = 0
  156. Dim UpdateCount As Integer = 0
  157. Dim UnavalialeCount As Integer = 0
  158. For Each ModItem In GetShowingMods(False)
  159. AnyCount += 1
  160. If ModItem.CanUpdate Then UpdateCount += 1
  161. If ModItem.State.Equals(McMod.McModState.Fine) Then EnabledCount += 1
  162. If ModItem.State.Equals(McMod.McModState.Disabled) Then DisabledCount += 1
  163. If ModItem.State.Equals(McMod.McModState.Unavailable) Then UnavalialeCount += 1
  164. Next
  165. '显示
  166. BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
  167. BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
  168. BtnFilterCanUpdate.Visibility = If(Filter = FilterType.CanUpdate OrElse UpdateCount > 0, Visibility.Visible, Visibility.Collapsed)
  169. BtnFilterEnabled.Text = $"启用 ({EnabledCount})"
  170. BtnFilterEnabled.Visibility = If(Filter = FilterType.Enabled OrElse (EnabledCount > 0 AndAlso EnabledCount < AnyCount), Visibility.Visible, Visibility.Collapsed)
  171. BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"
  172. BtnFilterDisabled.Visibility = If(Filter = FilterType.Disabled OrElse DisabledCount > 0, Visibility.Visible, Visibility.Collapsed)
  173. BtnFilterError.Text = $"错误 ({UnavalialeCount})"
  174. BtnFilterError.Visibility = If(Filter = FilterType.Unavailable OrElse UnavalialeCount > 0, Visibility.Visible, Visibility.Collapsed)
  175. '-----------------
  176. ' 底部栏
  177. '-----------------
  178. '计数
  179. Dim NewCount As Integer = SelectedMods.Count
  180. Dim Selected = NewCount > 0
  181. If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字
  182. '按钮可用性
  183. If Selected Then
  184. Dim HasUpdate As Boolean = False
  185. Dim HasEnabled As Boolean = False
  186. Dim HasDisabled As Boolean = False
  187. For Each ModEntity In McModLoader.Output
  188. If SelectedMods.Contains(ModEntity.RawFileName) Then
  189. If ModEntity.CanUpdate Then HasUpdate = True
  190. If ModEntity.State = McMod.McModState.Fine Then
  191. HasEnabled = True
  192. ElseIf ModEntity.State = McMod.McModState.Disabled Then
  193. HasDisabled = True
  194. End If
  195. End If
  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. Directory.CreateDirectory(PageVersionLeft.Version.PathIndie & "mods\")
  254. OpenExplorer("""" & PageVersionLeft.Version.PathIndie & "mods\""")
  255. Catch ex As Exception
  256. Log(ex, "打开 Mods 文件夹失败", LogLevel.Msgbox)
  257. End Try
  258. End Sub
  259. #If DEBUG Then
  260. ''' <summary>
  261. ''' 检查 Mod。
  262. ''' </summary>
  263. Private Sub BtnManageCheck_Click(sender As Object, e As EventArgs) Handles BtnManageCheck.Click
  264. Try
  265. Dim Result = McModCheck(PageVersionLeft.Version, McModLoader.Output)
  266. If Result.Any Then
  267. MyMsgBox(Join(Result, vbCrLf & vbCrLf), "Mod 检查结果")
  268. Else
  269. Hint("Mod 检查完成,未发现任何问题!", HintType.Finish)
  270. End If
  271. Catch ex As Exception
  272. Log(ex, "进行 Mod 检查时出错", LogLevel.Feedback)
  273. End Try
  274. End Sub
  275. #End If
  276. ''' <summary>
  277. ''' 全选。
  278. ''' </summary>
  279. Private Sub BtnManageSelectAll_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageSelectAll.Click
  280. ChangeAllSelected(SelectedMods.Count < GetShowingMods(True).Count)
  281. End Sub
  282. ''' <summary>
  283. ''' 安装 Mod。
  284. ''' </summary>
  285. Private Sub BtnManageInstall_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageInstall.Click
  286. Hint("将 Mod 文件直接拖入 PCL 窗口即可安装!")
  287. End Sub
  288. #End Region
  289. #Region "选择"
  290. '选择的 Mod 的路径(不含 .disabled)
  291. Public SelectedMods As New List(Of String)
  292. '单项切换选择状态
  293. Public Sub CheckChanged(sender As MyLocalModItem, e As RouteEventArgs)
  294. If AniControlEnabled <> 0 Then Return
  295. '更新选择了的内容
  296. Dim SelectedKey As String = sender.Entry.RawFileName
  297. If sender.Checked Then
  298. If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey)
  299. Else
  300. SelectedMods.Remove(SelectedKey)
  301. End If
  302. RefreshBars()
  303. End Sub
  304. '切换所有项的选择状态
  305. Private Sub ChangeAllSelected(Value As Boolean)
  306. AniControlEnabled += 1
  307. SelectedMods.Clear()
  308. For Each Item As MyLocalModItem In GetShowingMods(True).Where(Function(m) ModItems.ContainsKey(m.RawFileName)).Select(Function(m) ModItems(m.RawFileName))
  309. Item.Checked = Value
  310. If Value Then SelectedMods.Add(Item.Entry.RawFileName)
  311. Next
  312. AniControlEnabled -= 1
  313. RefreshBars()
  314. End Sub
  315. Private Sub UnselectedAllWithAnimation() Handles Load.StateChanged, Me.PageExit
  316. Dim CacheAniControlEnabled = AniControlEnabled
  317. AniControlEnabled = 0
  318. ChangeAllSelected(False)
  319. AniControlEnabled += CacheAniControlEnabled
  320. End Sub
  321. Private Sub PageVersionMod_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
  322. If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeAllSelected(True)
  323. End Sub
  324. #End Region
  325. #Region "筛选"
  326. Private _Filter As FilterType = FilterType.All
  327. Private Property Filter As FilterType
  328. Get
  329. Return _Filter
  330. End Get
  331. Set(value As FilterType)
  332. If _Filter = value Then Return
  333. _Filter = value
  334. Select Case value
  335. Case FilterType.All
  336. BtnFilterAll.Checked = True
  337. Case FilterType.Enabled
  338. BtnFilterEnabled.Checked = True
  339. Case FilterType.Disabled
  340. BtnFilterDisabled.Checked = True
  341. Case FilterType.CanUpdate
  342. BtnFilterCanUpdate.Checked = True
  343. Case Else
  344. BtnFilterError.Checked = True
  345. End Select
  346. RefreshUI()
  347. End Set
  348. End Property
  349. Private Enum FilterType As Integer
  350. All = 0
  351. Enabled = 1
  352. Disabled = 2
  353. CanUpdate = 3
  354. Unavailable = 4
  355. End Enum
  356. ''' <summary>
  357. ''' 获取所有应该显示在 UI 中的 Mod。
  358. ''' </summary>
  359. Private Function GetShowingMods(ApplyFilter As Boolean) As IEnumerable(Of McMod)
  360. If McModLoader.Output Is Nothing Then Return New List(Of McMod)
  361. Return If(IsSearching, SearchResult, McModLoader.Output).Where(Function(m) Not ApplyFilter OrElse CanPassFilter(m))
  362. End Function
  363. ''' <summary>
  364. ''' 检查该 Mod 项是否符合当前筛选的类别。
  365. ''' </summary>
  366. Private Function CanPassFilter(CheckingMod As McMod) As Boolean
  367. Select Case Filter
  368. Case FilterType.All
  369. Return True
  370. Case FilterType.Enabled
  371. Return CheckingMod.State = McMod.McModState.Fine
  372. Case FilterType.Disabled
  373. Return CheckingMod.State = McMod.McModState.Disabled
  374. Case FilterType.CanUpdate
  375. Return CheckingMod.CanUpdate
  376. Case FilterType.Unavailable
  377. Return CheckingMod.State = McMod.McModState.Unavailable
  378. Case Else
  379. Return False
  380. End Select
  381. End Function
  382. '点击筛选项触发的改变
  383. Private Sub ChangeFilter(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnFilterAll.Check, BtnFilterCanUpdate.Check, BtnFilterDisabled.Check, BtnFilterEnabled.Check, BtnFilterError.Check
  384. Filter = sender.Tag
  385. RefreshUI()
  386. End Sub
  387. #End Region
  388. #Region "下边栏"
  389. '启用 / 禁用
  390. Private Sub BtnSelectED_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click
  391. EDMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)),
  392. Not sender.Equals(BtnSelectDisable))
  393. ChangeAllSelected(False)
  394. End Sub
  395. Private Sub EDMods(ModList As IEnumerable(Of McMod), IsEnable As Boolean)
  396. Dim IsSuccessful As Boolean = True
  397. For Each ModE In ModList.ToList
  398. Dim ModEntity = ModE '仅用于去除迭代变量无法修改的限制
  399. Dim NewPath As String = Nothing
  400. If ModEntity.State = McMod.McModState.Fine AndAlso Not IsEnable Then
  401. '禁用
  402. NewPath = ModEntity.Path & If(File.Exists(ModEntity.Path & ".old"), ".old", ".disabled")
  403. ElseIf ModEntity.State = McMod.McModState.Disabled AndAlso IsEnable Then
  404. '启用
  405. NewPath = ModEntity.RawPath
  406. Else
  407. Continue For
  408. End If
  409. '重命名
  410. Try
  411. If File.Exists(NewPath) Then
  412. If File.Exists(ModEntity.Path) Then
  413. '同时存在两个名称的 Mod
  414. If GetFileMD5(ModEntity.Path) <> GetFileMD5(NewPath) Then
  415. MyMsgBox($"目前同时存在启用和禁用的两个 Mod 文件:{vbCrLf} - {NewPath}{vbCrLf} - {ModEntity.Path}{vbCrLf}{vbCrLf}注意,这两个文件的内容并不相同。{vbCrLf}在手动删除或重命名其中一个文件后,才能继续操作。", "存在文件冲突")
  416. Continue For
  417. End If
  418. Else
  419. '已经重命名过了
  420. Log("[Mod] Mod 的状态已被切换", LogLevel.Debug)
  421. Continue For
  422. End If
  423. End If
  424. File.Delete(NewPath)
  425. FileSystem.Rename(ModEntity.Path, NewPath)
  426. Catch ex As FileNotFoundException
  427. Log(ex, $"未找到需要重命名的 Mod({If(ModEntity.Path, "null")})", LogLevel.Feedback)
  428. ReloadModList(True)
  429. Return
  430. Catch ex As Exception
  431. Log(ex, $"重命名 Mod 失败({If(ModEntity.Path, "null")})")
  432. IsSuccessful = False
  433. End Try
  434. '更改 Loader 中的列表
  435. Dim NewModEntity As New McMod(NewPath)
  436. NewModEntity.FromJson(ModEntity.ToJson)
  437. If McModLoader.Output.Contains(ModEntity) Then
  438. Dim IndexOfLoader As Integer = McModLoader.Output.IndexOf(ModEntity)
  439. McModLoader.Output.RemoveAt(IndexOfLoader)
  440. McModLoader.Output.Insert(IndexOfLoader, NewModEntity)
  441. End If
  442. If SearchResult IsNot Nothing AndAlso SearchResult.Contains(ModEntity) Then '#4862
  443. Dim IndexOfResult As Integer = SearchResult.IndexOf(ModEntity)
  444. SearchResult.Remove(ModEntity)
  445. SearchResult.Insert(IndexOfResult, NewModEntity)
  446. End If
  447. '更改 UI 中的列表
  448. Dim NewItem As MyLocalModItem = McModListItem(NewModEntity)
  449. ModItems(ModEntity.RawFileName) = NewItem
  450. Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry Is ModEntity))
  451. If IndexOfUi = -1 Then Continue For '因为未知原因 Mod 的状态已经切换完了
  452. PanList.Children.RemoveAt(IndexOfUi)
  453. PanList.Children.Insert(IndexOfUi, NewItem)
  454. Next
  455. If IsSuccessful Then
  456. RefreshBars()
  457. Else
  458. Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
  459. ReloadModList(True)
  460. End If
  461. LoaderRun(LoaderFolderRunType.UpdateOnly)
  462. End Sub
  463. '更新
  464. Private Sub BtnSelectUpdate_Click() Handles BtnSelectUpdate.Click
  465. Dim UpdateList As List(Of McMod) = McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName) AndAlso m.CanUpdate).ToList()
  466. If Not UpdateList.Any() Then Return
  467. UpdateMods(UpdateList)
  468. ChangeAllSelected(False)
  469. End Sub
  470. ''' <summary>
  471. ''' 记录正在进行 Mod 更新的 mods 文件夹路径。
  472. ''' </summary>
  473. Public Shared UpdatingVersions As New List(Of String)
  474. Public Sub UpdateMods(ModList As IEnumerable(Of McMod))
  475. '更新前警告
  476. If Not Setup.Get("HintUpdateMod") Then
  477. If MyMsgBox($"新版本 Mod 可能不兼容老版本的存档或者其他 Mod,这可能导致游戏崩溃,甚至存档损坏!{vbCrLf}除非整合包作者要求你更新,否则不要私自更新整合包里的 Mod!{vbCrLf}在更新 Mod 前,请先备份存档,并检查它的更新日志!{vbCrLf}更新时,老版本的 Mod 会被移动到回收站,以防万一。{vbCrLf}{vbCrLf}请在认真阅读上述警告后再继续!", "Mod 更新警告", "我已了解上述风险,继续更新", "取消", IsWarn:=True) = 1 Then
  478. Setup.Set("HintUpdateMod", True)
  479. Else
  480. Exit Sub
  481. End If
  482. End If
  483. Try
  484. '构造下载信息
  485. ModList = ModList.ToList() '防止刷新影响迭代器
  486. Dim FileList As New List(Of NetFile)
  487. Dim FileCopyList As New Dictionary(Of String, String)
  488. For Each Entry As McMod In ModList
  489. Dim File As CompFile = Entry.UpdateFile
  490. If Not File.Available Then Continue For
  491. '确认更新后的文件名
  492. Dim CurrentReplaceName = Entry.CompFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
  493. Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
  494. Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
  495. Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
  496. Dim Shortened As Boolean = False
  497. For Each Seg In CurrentSegs.ToList()
  498. If Not NewestSegs.Contains(Seg) Then Continue For
  499. CurrentSegs.Remove(Seg)
  500. NewestSegs.Remove(Seg)
  501. Shortened = True
  502. Next
  503. If Shortened AndAlso CurrentSegs.Any() AndAlso NewestSegs.Any() Then
  504. CurrentReplaceName = Join(CurrentSegs, "-")
  505. NewestReplaceName = Join(NewestSegs, "-")
  506. End If
  507. '添加到下载列表
  508. Dim TempAddress As String = PathTemp & "DownloadedMods\" & Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName)
  509. Dim RealAddress As String = GetPathFromFullPath(Entry.Path) & Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName)
  510. FileList.Add(File.ToNetFile(TempAddress))
  511. FileCopyList(TempAddress) = RealAddress
  512. Next
  513. '构造加载器
  514. Dim InstallLoaders As New List(Of LoaderBase)
  515. Dim FinishedFileNames As New List(Of String)
  516. InstallLoaders.Add(New LoaderDownload("下载新版 Mod 文件", FileList) With {.ProgressWeight = ModList.Count * 1.5}) '每个 Mod 需要 1.5s
  517. InstallLoaders.Add(New LoaderTask(Of Integer, Integer)("替换旧版 Mod 文件",
  518. Sub()
  519. Try
  520. For Each Entry As McMod In ModList
  521. If File.Exists(Entry.Path) Then
  522. My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
  523. Else
  524. Log($"[Mod] 未找到更新前的 Mod 文件,跳过对它的删除:{Entry.Path}", LogLevel.Debug)
  525. End If
  526. Next
  527. For Each Entry As KeyValuePair(Of String, String) In FileCopyList
  528. If File.Exists(Entry.Value) Then
  529. My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
  530. Log($"[Mod] 更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}", LogLevel.Debug)
  531. End If
  532. If Directory.Exists(GetPathFromFullPath(Entry.Value)) Then
  533. File.Move(Entry.Key, Entry.Value)
  534. FinishedFileNames.Add(GetFileNameFromPath(Entry.Value))
  535. Else
  536. Log($"[Mod] 更新后的目标文件夹已被删除:{Entry.Value}", LogLevel.Debug)
  537. End If
  538. Next
  539. Catch ex As OperationCanceledException
  540. Log(ex, "替换旧版 Mod 文件时被主动取消")
  541. End Try
  542. End Sub))
  543. '结束处理
  544. Dim Loader As New LoaderCombo(Of IEnumerable(Of McMod))("Mod 更新:" & PageVersionLeft.Version.Name, InstallLoaders)
  545. Dim PathMods As String = PageVersionLeft.Version.PathIndie & "mods\"
  546. Loader.OnStateChanged =
  547. Sub()
  548. '结果提示
  549. Select Case Loader.State
  550. Case LoadState.Finished
  551. Select Case FinishedFileNames.Count
  552. Case 0 '一般是由于 Mod 文件被占用,然后玩家主动取消
  553. Log($"[Mod] 没有 Mod 被成功更新")
  554. Case 1
  555. Hint($"已成功更新 {FinishedFileNames.Single}!", HintType.Finish)
  556. Case Else
  557. Hint($"已成功更新 {FinishedFileNames.Count} 个 Mod!", HintType.Finish)
  558. End Select
  559. Case LoadState.Failed
  560. Hint("Mod 更新失败:" & GetExceptionSummary(Loader.Error), HintType.Critical)
  561. Case LoadState.Aborted
  562. Hint("Mod 更新已中止!", HintType.Info)
  563. Case Else
  564. Exit Sub
  565. End Select
  566. Log($"[Mod] 已从正在进行 Mod 更新的文件夹列表移除:{PathMods}")
  567. UpdatingVersions.Remove(PathMods)
  568. '清理缓存
  569. RunInNewThread(
  570. Sub()
  571. Try
  572. For Each TempFile In FileCopyList.Keys
  573. If File.Exists(TempFile) Then File.Delete(TempFile)
  574. Next
  575. Catch ex As Exception
  576. Log(ex, "清理 Mod 更新缓存失败")
  577. End Try
  578. End Sub, "Clean Mod Update Cache", ThreadPriority.BelowNormal)
  579. End Sub
  580. '启动加载器
  581. Log($"[Mod] 开始更新 {ModList.Count} 个 Mod:{PathMods}")
  582. UpdatingVersions.Add(PathMods)
  583. Loader.Start()
  584. LoaderTaskbarAdd(Loader)
  585. FrmMain.BtnExtraDownload.ShowRefresh()
  586. FrmMain.BtnExtraDownload.Ribble()
  587. ReloadModList(True)
  588. Catch ex As Exception
  589. Log(ex, "初始化 Mod 更新失败")
  590. End Try
  591. End Sub
  592. '删除
  593. Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click
  594. DeleteMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)))
  595. ChangeAllSelected(False)
  596. End Sub
  597. Private Sub DeleteMods(ModList As IEnumerable(Of McMod))
  598. Try
  599. Dim IsSuccessful As Boolean = True
  600. Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
  601. '确认需要删除的文件
  602. ModList = ModList.SelectMany(
  603. Function(Target As McMod)
  604. If Target.State = McMod.McModState.Fine Then
  605. Return {Target.Path, Target.Path & If(File.Exists(Target.Path & ".old"), ".old", ".disabled")}
  606. Else
  607. Return {Target.Path, Target.RawPath}
  608. End If
  609. End Function).Distinct.Where(Function(m) File.Exists(m)).Select(Function(m) New McMod(m)).ToList()
  610. '实际删除文件
  611. For Each ModEntity In ModList
  612. '删除
  613. Try
  614. If IsShiftPressed Then
  615. File.Delete(ModEntity.Path)
  616. Else
  617. My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
  618. End If
  619. Catch ex As OperationCanceledException
  620. Log(ex, "删除 Mod 被主动取消")
  621. ReloadModList(True)
  622. Return
  623. Catch ex As Exception
  624. Log(ex, $"删除 Mod 失败({ModEntity.Path})", LogLevel.Msgbox)
  625. IsSuccessful = False
  626. End Try
  627. '取消选中
  628. SelectedMods.Remove(ModEntity.RawFileName)
  629. '更改 Loader 和 UI 中的列表
  630. McModLoader.Output.Remove(ModEntity)
  631. SearchResult?.Remove(ModEntity)
  632. ModItems.Remove(ModEntity.RawFileName)
  633. Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
  634. If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
  635. Next
  636. RefreshBars()
  637. If Not IsSuccessful Then
  638. Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
  639. ReloadModList(True)
  640. ElseIf PanList.Children.Count = 0 Then
  641. ReloadModList(True) '删除了全部文件
  642. Else
  643. RefreshBars()
  644. End If
  645. '显示结果提示
  646. If Not IsSuccessful Then Exit Sub
  647. If IsShiftPressed Then
  648. If ModList.Count = 1 Then
  649. Hint($"已彻底删除 {ModList.Single.FileName}!", HintType.Finish)
  650. Else
  651. Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Finish)
  652. End If
  653. Else
  654. If ModList.Count = 1 Then
  655. Hint($"已将 {ModList.Single.FileName} 删除到回收站!", HintType.Finish)
  656. Else
  657. Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Finish)
  658. End If
  659. End If
  660. Catch ex As OperationCanceledException
  661. Log(ex, "删除 Mod 被主动取消")
  662. ReloadModList(True)
  663. Catch ex As Exception
  664. Log(ex, "删除 Mod 出现未知错误", LogLevel.Feedback)
  665. ReloadModList(True)
  666. End Try
  667. LoaderRun(LoaderFolderRunType.UpdateOnly)
  668. End Sub
  669. '取消选择
  670. Private Sub BtnSelectCancel_Click() Handles BtnSelectCancel.Click
  671. ChangeAllSelected(False)
  672. End Sub
  673. #End Region
  674. #Region "单个 Mod 项"
  675. '详情
  676. Public Sub Info_Click(sender As Object, e As EventArgs)
  677. Try
  678. Dim ModEntry As McMod = CType(If(TypeOf sender Is MyIconButton, sender.Tag, sender), MyLocalModItem).Entry
  679. '加载失败信息
  680. If ModEntry.State = McMod.McModState.Unavailable Then
  681. MyMsgBox("无法读取此 Mod 的信息。" & vbCrLf & vbCrLf & "详细的错误信息:" & GetExceptionDetail(ModEntry.FileUnavailableReason), "Mod 读取失败")
  682. Return
  683. End If
  684. If ModEntry.Comp IsNot Nothing Then
  685. '跳转到 Mod 下载页面
  686. FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.CompDetail,
  687. .Additional = {ModEntry.Comp, New List(Of String), PageVersionLeft.Version.Version.McName,
  688. If(PageVersionLeft.Version.Version.HasForge, CompModLoaderType.Forge,
  689. If(PageVersionLeft.Version.Version.HasNeoForge, CompModLoaderType.NeoForge,
  690. If(PageVersionLeft.Version.Version.HasFabric, CompModLoaderType.Fabric, CompModLoaderType.Any)))}})
  691. Else
  692. '获取信息
  693. Dim ContentLines As New List(Of String)
  694. If ModEntry.Description IsNot Nothing Then ContentLines.Add(ModEntry.Description & vbCrLf)
  695. If ModEntry.Authors IsNot Nothing Then ContentLines.Add("作者:" & ModEntry.Authors)
  696. ContentLines.Add("文件:" & ModEntry.FileName & "(" & GetString(New FileInfo(ModEntry.Path).Length) & ")")
  697. If ModEntry.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntry.Version)
  698. Dim DebugInfo As New List(Of String)
  699. If ModEntry.ModId IsNot Nothing Then
  700. DebugInfo.Add("Mod ID:" & ModEntry.ModId)
  701. End If
  702. If ModEntry.Dependencies.Any Then
  703. DebugInfo.Add("依赖于:")
  704. For Each Dep In ModEntry.Dependencies
  705. DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value))
  706. Next
  707. End If
  708. If DebugInfo.Any Then
  709. ContentLines.Add("")
  710. ContentLines.AddRange(DebugInfo)
  711. End If
  712. '获取用于搜索的 Mod 名称
  713. Dim ModOriginalName As String = ModEntry.Name.Replace(" ", "+")
  714. Dim ModSearchName As String = ModOriginalName.Substring(0, 1)
  715. For i = 1 To ModOriginalName.Count - 1
  716. Dim IsLastLower As Boolean = ModOriginalName(i - 1).ToString.ToLower.Equals(ModOriginalName(i - 1).ToString)
  717. Dim IsCurrentLower As Boolean = ModOriginalName(i).ToString.ToLower.Equals(ModOriginalName(i).ToString)
  718. If IsLastLower AndAlso Not IsCurrentLower Then
  719. '上一个字母为小写,这一个字母为大写
  720. ModSearchName += "+"
  721. End If
  722. ModSearchName += ModOriginalName(i)
  723. Next
  724. ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine")
  725. '显示
  726. If ModEntry.Url Is Nothing Then
  727. If MyMsgBox(Join(ContentLines, vbCrLf), ModEntry.Name, "百科搜索", "返回") = 1 Then
  728. OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
  729. End If
  730. Else
  731. Select Case MyMsgBox(Join(ContentLines, vbCrLf), ModEntry.Name, "打开官网", "百科搜索", "返回")
  732. Case 1
  733. OpenWebsite(ModEntry.Url)
  734. Case 2
  735. OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
  736. End Select
  737. End If
  738. End If
  739. Catch ex As Exception
  740. Log(ex, "获取 Mod 详情失败", LogLevel.Feedback)
  741. End Try
  742. End Sub
  743. '打开文件所在的位置
  744. Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
  745. Try
  746. Dim ListItem As MyLocalModItem = sender.Tag
  747. OpenExplorer("/select,""" & ListItem.Entry.Path & """")
  748. Catch ex As Exception
  749. Log(ex, "打开 Mod 文件位置失败", LogLevel.Feedback)
  750. End Try
  751. End Sub
  752. '删除
  753. Public Sub Delete_Click(sender As MyIconButton, e As EventArgs)
  754. Dim ListItem As MyLocalModItem = sender.Tag
  755. DeleteMods({ListItem.Entry})
  756. End Sub
  757. '启用 / 禁用
  758. Public Sub ED_Click(sender As MyIconButton, e As EventArgs)
  759. Dim ListItem As MyLocalModItem = sender.Tag
  760. EDMods({ListItem.Entry}, ListItem.Entry.State = McMod.McModState.Disabled)
  761. End Sub
  762. #End Region
  763. #Region "搜索"
  764. Public ReadOnly Property IsSearching As Boolean
  765. Get
  766. Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
  767. End Get
  768. End Property
  769. Private SearchResult As List(Of McMod)
  770. Public Sub SearchRun() Handles SearchBox.TextChanged
  771. If IsSearching Then
  772. '构造请求
  773. Dim QueryList As New List(Of SearchEntry(Of McMod))
  774. For Each Entry As McMod In McModLoader.Output
  775. Dim SearchSource As New List(Of KeyValuePair(Of String, Double))
  776. SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Name, 1))
  777. SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.FileName, 1))
  778. If Entry.Version IsNot Nothing Then
  779. SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Version, 0.2))
  780. End If
  781. If Entry.Description IsNot Nothing AndAlso Entry.Description <> "" Then
  782. SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Description, 0.4))
  783. End If
  784. If Entry.Comp IsNot Nothing Then
  785. If Entry.Comp.RawName <> Entry.Name Then SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Comp.RawName, 1))
  786. If Entry.Comp.TranslatedName <> Entry.Comp.RawName Then SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Comp.TranslatedName, 1))
  787. If Entry.Comp.Description <> Entry.Description Then SearchSource.Add(New KeyValuePair(Of String, Double)(Entry.Comp.Description, 0.4))
  788. SearchSource.Add(New KeyValuePair(Of String, Double)(String.Join("", Entry.Comp.Tags), 0.2))
  789. End If
  790. QueryList.Add(New SearchEntry(Of McMod) With {.Item = Entry, .SearchSource = SearchSource})
  791. Next
  792. '进行搜索
  793. SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList
  794. End If
  795. RefreshUI()
  796. End Sub
  797. #End Region
  798. End Class