PageInstanceMod.xaml.vb 42 KB

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