PageInstanceMod.xaml.vb 40 KB

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