LocalResourceLoaders.vb 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. Public Module LocalResourceLoaders
  2. '加载 Mod 列表
  3. Public LocalResourceLoader As New LoaderTask(Of String, List(Of LocalResourceFile))("LocalResourceLoader", AddressOf LocalResourceFileLoad)
  4. Private Sub LocalResourceFileLoad(Loader As LoaderTask(Of String, List(Of LocalResourceFile)))
  5. Try
  6. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
  7. '等待 Mod 更新完成
  8. If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
  9. Logger.Info($"等待 Mod 更新完成后才能继续加载 Mod 列表:{Loader.Input}")
  10. Try
  11. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
  12. Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
  13. If Loader.IsCanceled Then Return
  14. Thread.Sleep(100)
  15. Loop
  16. Finally
  17. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
  18. End Try
  19. FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
  20. End If
  21. '获取 Mod 文件夹下的可用文件列表
  22. Dim ModFileList As New List(Of String)
  23. If DirectoryUtils.Exists(Loader.Input) Then
  24. For Each File In DirectoryUtils.EnumerateFiles(Loader.Input, True)
  25. If Not PathUtils.IsSame(PathUtils.RemoveLastPart(File), Loader.Input) Then
  26. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  27. If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) AndAlso
  28. PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
  29. PathUtils.GetLastPart(PathUtils.RemoveLastPart(File)) = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
  30. Continue For
  31. End If
  32. End If
  33. Static IsModFile As Func(Of String, Boolean) =
  34. Function(FullName As String)
  35. If FullName Is Nothing Then Return False
  36. If {"disabled", "old"}.Contains(PathUtils.GetExtension(FullName)) Then FullName = PathUtils.GetFileNameWithoutExtension(FullName)
  37. Return {"jar", "zip", "litemod"}.Contains(PathUtils.GetExtension(FullName))
  38. End Function
  39. If IsModFile(File) Then ModFileList.Add(File)
  40. Next
  41. End If
  42. '获取本地文件缓存
  43. Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
  44. Dim Cache As New JObject
  45. Try
  46. Dim CacheContent As String = FileUtils.TryReadAsString(CachePath)
  47. If Not String.IsNullOrWhiteSpace(CacheContent) Then
  48. Cache = CacheContent.DeserializeJson()
  49. If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> Versions.LocalModCacheVersion Then
  50. Logger.Warn($"本地 Mod 信息缓存版本已过期,将弃用这些缓存信息")
  51. Cache = New JObject
  52. End If
  53. End If
  54. Catch ex As Exception
  55. Logger.Warn(ex, "读取本地 Mod 信息缓存失败,已重置")
  56. Cache = New JObject
  57. End Try
  58. Cache("version") = Versions.LocalModCacheVersion
  59. '加载 Mod 列表
  60. Dim ModList As New List(Of LocalResourceFile)
  61. For Each ModFile In ModFileList
  62. If Loader.IsCanceled Then Return
  63. Dim ModEntry As New LocalResourceFile(ModFile)
  64. Dim DumpMod As LocalResourceFile = ModList.FirstOrDefault(Function(m) m.EnabledName = ModEntry.EnabledName) '存在两个文件,名称相同,但一个启用一个禁用
  65. If DumpMod IsNot Nothing Then
  66. Dim DisabledMod As LocalResourceFile = If(DumpMod.IsEnabled, ModEntry, DumpMod)
  67. Logger.Warn($"重复的 Mod 文件:{DumpMod.File.Name} 与 {ModEntry.File.Name},已忽略 {DisabledMod.File.Name}")
  68. If DisabledMod Is ModEntry Then
  69. Continue For
  70. Else
  71. ModList.Remove(DisabledMod)
  72. End If
  73. End If
  74. ModList.Add(ModEntry)
  75. Next
  76. Logger.Info($"共发现 {ModList.Count} 个 Mod")
  77. '排序
  78. ModList = ModList.OrderBy(Function(m) m.File.Name).ToList
  79. '回设
  80. If Loader.IsCanceled Then Return
  81. Loader.Output = ModList
  82. '开始联网加载
  83. 'TODO: 添加信息获取中提示
  84. LocalResourceOnlineLoader.Start((ModList, Cache, PageInstanceLeft.Instance.Version), IsForceRestart:=True)
  85. Catch ex As Exception
  86. Logger.Warn(ex, "Mod 列表加载失败")
  87. Throw
  88. End Try
  89. End Sub
  90. '联网加载 Mod 详情
  91. Public LocalResourceOnlineLoader As New LoaderTask(Of (ModList As List(Of LocalResourceFile), Cache As JObject, Target As McVersion), Integer)("LocalResourceOnlineLoader", AddressOf LocalResourceOnlineLoad)
  92. Private Sub LocalResourceOnlineLoad(Loader As LoaderTask(Of (ModList As List(Of LocalResourceFile), Cache As JObject, Target As McVersion), Integer))
  93. Dim Mods As New List(Of LocalResourceFile)
  94. Dim Cache As JObject = Loader.Input.Cache
  95. Dim ModLoaders = Loader.Input.Target.ModLoaders
  96. '读取缓存,获取需要更新的 Mod 列表
  97. For Each ModEntry As LocalResourceFile In Loader.Input.ModList
  98. If Loader.IsCanceled Then Return
  99. Dim CacheKey = ModEntry.ModrinthHash & Loader.Input.Target.VanillaName & ModLoaders
  100. If Cache.ContainsKey(CacheKey) Then
  101. ModEntry.FromJson(Cache(CacheKey))
  102. '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
  103. If ModEntry.OnlineDataLoaded AndAlso Date.Now - Cache(CacheKey)("Project")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
  104. End If
  105. Mods.Add(ModEntry)
  106. Next
  107. Logger.Info($"有 {Mods.Where(Function(m) m.Project Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Project IsNot Nothing).Count} 个 Mod 需要更新信息")
  108. If Not Mods.Any Then Return
  109. '获取作为检查目标的版本
  110. '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
  111. Dim TargetMcVersion As McVersion = Loader.Input.Target
  112. Dim VanillaVersion = TargetMcVersion.VanillaName
  113. '开始网络获取
  114. Logger.Info($"正在检查 Mod 信息的版本:{VanillaVersion},加载器 {ModLoaders}")
  115. Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
  116. Dim CurrentTaskThread As Thread = Thread.CurrentThread
  117. '从 Modrinth 获取信息
  118. RunInNewThread(
  119. Sub()
  120. Try
  121. '步骤 1:获取 Hash 与对应的工程 ID
  122. Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
  123. Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
  124. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
  125. Logger.Info($"从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
  126. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  127. If ModrinthVersion.Count = 0 Then Return
  128. Dim ModrinthMapping As New Dictionary(Of String, List(Of LocalResourceFile))
  129. Dim ModrinthLoaders As New Dictionary(Of LocalResourceFile, List(Of String))
  130. For Each Entry In Mods
  131. If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
  132. If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
  133. Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
  134. If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
  135. If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of LocalResourceFile)
  136. ModrinthMapping(ProjectId).Add(Entry)
  137. '记录对应的 ProjectVersion
  138. Dim File = ResourceVersion.FromPlatformJson(ModrinthVersion(Entry.ModrinthHash), ResourceTypes.Mod)
  139. ModrinthLoaders(Entry) = ModrinthVersion(Entry.ModrinthHash)("loaders").
  140. Select(Function(l) l.ToString.Lower).Distinct.OrderBy(Function(l) l).ToList
  141. If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then
  142. Entry.ProjectVersion = File
  143. Else
  144. Entry.ProjectVersion.Version = File.Version '使用来自 Modrinth 的版本号
  145. End If
  146. Next
  147. If Loader.IsCanceledWithThread(CurrentTaskThread) Then Return
  148. Logger.Info($"需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
  149. '步骤 3:获取工程信息
  150. If Not ModrinthMapping.Any() Then Return
  151. Dim ModrinthProject As JArray = DlModRequest(
  152. $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
  153. For Each ProjectJson In ModrinthProject
  154. Dim Project As New ResourceProject(ProjectJson)
  155. For Each Entry In ModrinthMapping(Project.Id)
  156. Entry.Project = Project
  157. Next
  158. Next
  159. Logger.Info($"已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
  160. '步骤 4:获取更新信息
  161. Dim ModsToUpdate = ModrinthMapping.SelectMany(Function(l) l.Value).ToList
  162. If Not ModsToUpdate.Any Then Return
  163. Dim UpdateData As String = ModsToUpdate.
  164. Select(Function(m) $"{{""hash"": ""{m.ModrinthHash}"", ""loaders"": [""{ModrinthLoaders(m).Join(""",""")}""], ""game_versions"": [""{VanillaVersion}""]}}").
  165. Join(",")
  166. Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update_individual", HttpMethod.Post,
  167. $"{{""algorithm"": ""sha1"", ""hashes"": [{UpdateData}]}}", "application/json")
  168. For Each Entry In Mods
  169. If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.ProjectVersion Is Nothing Then Continue For
  170. Dim UpdateFile = ResourceVersion.FromPlatformJson(ModrinthUpdate(Entry.ModrinthHash), ResourceTypes.Mod)
  171. If Not UpdateFile.DownloadAvailable Then Continue For
  172. Dim UpdateLoaders = ModrinthUpdate(Entry.ModrinthHash)("loaders").
  173. Select(Function(l) l.ToString.Lower).Distinct.OrderBy(Function(l) l).ToList
  174. If Not UpdateLoaders.SequenceEqual(ModrinthLoaders(Entry)) Then Continue For
  175. Logger.Trace(Function() $"本地文件 {Entry.ProjectVersion.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
  176. If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
  177. '设置更新日志与更新文件
  178. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  179. Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}")
  180. UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
  181. Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
  182. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
  183. Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}"}
  184. Entry.UpdateFile = UpdateFile
  185. End If
  186. Next
  187. Logger.Info($"从 Modrinth 获取本地 Mod 信息结束")
  188. Catch ex As Exception
  189. Logger.Warn(ex, "从 Modrinth 获取本地 Mod 信息失败")
  190. IsFailed = True
  191. Finally
  192. EndedThreadCount += 1
  193. End Try
  194. End Sub, "Mod List Detail Loader Modrinth")
  195. '从 CurseForge 获取信息
  196. RunInNewThread(
  197. Sub()
  198. Try
  199. '步骤 1:获取 Hash 与对应的工程 ID
  200. Dim Hashes As New List(Of UInteger)
  201. For Each Entry In Mods
  202. Hashes.Add(Entry.CurseForgeHash)
  203. If Loader.IsCanceledWithThread(CurrentTaskThread) Then Return
  204. Next
  205. Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
  206. $"{{""fingerprints"": [{Hashes.Join(",")}]}}", "application/json")("data")("exactMatches")
  207. Logger.Info($"从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
  208. If Not CurseForgeRaw.Any() Then Return
  209. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  210. Dim ProjectIdToLocalFiles As New Dictionary(Of Integer, List(Of LocalResourceFile))
  211. Dim CurseForgeLoaders As New Dictionary(Of LocalResourceFile, ModLoaders)
  212. For Each Project In CurseForgeRaw
  213. Dim ProjectId = Project("id").ToString
  214. Dim Hash As UInteger = Project("file")("fileFingerprint")
  215. For Each Entry In Mods
  216. If Entry.CurseForgeHash <> Hash Then Continue For
  217. If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
  218. ProjectIdToLocalFiles.AddIntoValueCollection(ProjectId, Entry)
  219. '记录对应的 ProjectVersion
  220. Dim File = ResourceVersion.FromPlatformJson(Project("file"), ResourceTypes.Mod)
  221. CurseForgeLoaders(Entry) = File.ModLoaders
  222. If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File
  223. Next
  224. Next
  225. If Loader.IsCanceledWithThread(CurrentTaskThread) Then Return
  226. Logger.Info($"需要从 CurseForge 获取 {ProjectIdToLocalFiles.Count} 个本地 Mod 的工程信息")
  227. If Not ProjectIdToLocalFiles.Any() Then Return
  228. '步骤 3:获取工程信息
  229. Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
  230. $"{{""modIds"": [{ProjectIdToLocalFiles.Keys.Join(",")}]}}", "application/json")("data")
  231. Dim UpdateFileIds As New Dictionary(Of Integer, List(Of LocalResourceFile)) 'FileId -> 本地 Mod 文件列表
  232. Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
  233. For Each ProjectJson In CurseForgeProject
  234. If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
  235. '设置 Entry 中的工程信息
  236. Dim Project As New ResourceProject(ProjectJson)
  237. For Each LocalFile In ProjectIdToLocalFiles(Project.Id) '倒查防止 CurseForge 返回的内容有漏
  238. If LocalFile.Project?.Platform = ResourcePlatforms.Modrinth Then
  239. LocalFile.Project = LocalFile.Project '再次触发修改事件
  240. Else
  241. LocalFile.Project = Project
  242. End If
  243. '查找可能有版本更新的文件列表
  244. Dim LatestFiles = ProjectJson("latestFilesIndexes").Where(
  245. Function(Entry)
  246. 'Mod Loader 匹配
  247. Dim EntryLoader As ModLoaders = If(Entry("modLoader") Is Nothing, ModLoaders.None,
  248. Resource.FromCurseForgeModLoaderType(Entry("modLoader").ToObject(Of Integer)))
  249. If CurseForgeLoaders(LocalFile) = ModLoaders.None Then
  250. If EntryLoader <> ModLoaders.None Then Return False
  251. ElseIf Not CurseForgeLoaders(LocalFile).HasFlagF(EntryLoader) Then
  252. Return False
  253. End If
  254. 'MC 版本匹配
  255. Dim EntryVersion As String = Entry("gameVersion")
  256. Return EntryVersion IsNot Nothing AndAlso EntryVersion = VanillaVersion
  257. End Function).
  258. MaxByAll(Function(Entry) Entry("gameVersion").ToString, Comparer(Of String).Create(Function(x, y) McUtils.CompareVersion(x, y))) '只保留最新的 MC 版本
  259. For Each Entry In LatestFiles
  260. Dim FileId = Entry("fileId").ToObject(Of Integer)
  261. If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of LocalResourceFile)
  262. UpdateFileIds(FileId).Add(LocalFile)
  263. FileIdToProjectSlug(FileId) = Project.Slug
  264. Next
  265. Next
  266. Next
  267. Logger.Info($"已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
  268. If Not UpdateFileIds.Any() Then Return
  269. '步骤 4:获取更新文件信息
  270. Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
  271. $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
  272. Dim UpdateFiles As New Dictionary(Of LocalResourceFile, ResourceVersion)
  273. For Each FileJson In CurseForgeFiles
  274. Dim File = ResourceVersion.FromPlatformJson(FileJson, ResourceTypes.Mod)
  275. If Not File.DownloadAvailable Then Continue For
  276. For Each Entry As LocalResourceFile In UpdateFileIds(File.Id)
  277. If File.ModLoaders <> CurseForgeLoaders(Entry) Then
  278. Logger.Warn($"从 CurseForge 获取到的更新文件 {File.FileName} 加载器为 {File.ModLoaders},与本地文件 {Entry.File.Name} 的加载器 {CurseForgeLoaders(Entry)} 不一致,已忽略", LogBehavior.None)
  279. Continue For
  280. End If
  281. If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
  282. UpdateFiles(Entry) = File
  283. Next
  284. Next
  285. For Each Pair In UpdateFiles
  286. Dim Entry As LocalResourceFile = Pair.Key
  287. Dim UpdateFile As ResourceVersion = Pair.Value
  288. Logger.Trace(Function() $"本地文件 {Entry.ProjectVersion.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
  289. If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
  290. '设置更新日志与更新文件
  291. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  292. Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
  293. Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
  294. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
  295. Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
  296. Entry.UpdateFile = UpdateFile
  297. End If
  298. Next
  299. Logger.Info($"从 CurseForge 获取 Mod 更新信息结束")
  300. Catch ex As Exception
  301. Logger.Warn(ex, "从 CurseForge 获取本地 Mod 信息失败")
  302. IsFailed = True
  303. Finally
  304. EndedThreadCount += 1
  305. End Try
  306. End Sub, "Mod List Detail Loader CurseForge")
  307. '等待线程结束
  308. Do Until EndedThreadCount = 2
  309. Thread.Sleep(10)
  310. If Loader.IsCanceled Then Return
  311. Loop
  312. '保存缓存
  313. Mods = Mods.Where(Function(m) m.Project IsNot Nothing).ToList()
  314. Logger.Info($"联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
  315. If Not Mods.Any() Then Return
  316. If Loader.IsCanceled Then Return
  317. For Each Entry In Mods
  318. Entry.OnlineDataLoaded = Not IsFailed
  319. Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders) = Entry.ToJson()
  320. Next
  321. FileUtils.Write(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
  322. '刷新边栏
  323. If Loader.IsCanceled Then Return
  324. If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
  325. RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
  326. Else
  327. RunInUi(Sub() FrmInstanceMod?.RefreshBars())
  328. End If
  329. End Sub
  330. End Module