@rynt/sdk 0.9.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +122 -0
  2. package/REGISTRIES.md +189 -0
  3. package/env.d.ts +11 -0
  4. package/host-shims.d.ts +30 -0
  5. package/package.json +88 -0
  6. package/src/extension-marketplace/api-types.ts +141 -0
  7. package/src/extension-marketplace/client.ts +296 -0
  8. package/src/extension-marketplace/index.ts +22 -0
  9. package/src/extension-marketplace/schemas.ts +178 -0
  10. package/src/extensions/ExtensionRoutePage.vue +17 -0
  11. package/src/extensions/context.ts +37 -0
  12. package/src/extensions/disabled-folder.ts +21 -0
  13. package/src/extensions/extension-expose-map.ts +5 -0
  14. package/src/extensions/extension-expose.ts +48 -0
  15. package/src/extensions/graph.ts +67 -0
  16. package/src/extensions/index.ts +251 -0
  17. package/src/extensions/invite-handler/types.ts +20 -0
  18. package/src/extensions/launcher-entities/create-launcher-entity.ts +25 -0
  19. package/src/extensions/launcher-entities/keys.ts +46 -0
  20. package/src/extensions/launcher-entities/launcher-entity-components.ts +177 -0
  21. package/src/extensions/launcher-entities/props-map.ts +69 -0
  22. package/src/extensions/launcher-entities/registry.ts +32 -0
  23. package/src/extensions/launcher-models/apis/accounts-contracts.ts +102 -0
  24. package/src/extensions/launcher-models/apis/launcher-model-apis.ts +553 -0
  25. package/src/extensions/launcher-models/keys.ts +23 -0
  26. package/src/extensions/launcher-models/public.ts +9 -0
  27. package/src/extensions/launcher-models/registry-core.ts +34 -0
  28. package/src/extensions/manifest-types.ts +22 -0
  29. package/src/extensions/manifest.ts +46 -0
  30. package/src/extensions/marketplace-open-key.ts +26 -0
  31. package/src/extensions/plugin-types.ts +44 -0
  32. package/src/extensions/plugin.ts +62 -0
  33. package/src/extensions/registries/bootstrap.ts +11 -0
  34. package/src/extensions/registries/builtins/account-provider.ts +6 -0
  35. package/src/extensions/registries/builtins/app-topbar-left-widgets.ts +6 -0
  36. package/src/extensions/registries/builtins/app-topbar-right-widgets.ts +6 -0
  37. package/src/extensions/registries/builtins/app-topbar-status-widgets.ts +6 -0
  38. package/src/extensions/registries/builtins/build-card-actions.ts +6 -0
  39. package/src/extensions/registries/builtins/build-card-after-meta.ts +6 -0
  40. package/src/extensions/registries/builtins/build-card-before-media.ts +6 -0
  41. package/src/extensions/registries/builtins/build-card-before-meta.ts +6 -0
  42. package/src/extensions/registries/builtins/build-card-footer-actions.ts +6 -0
  43. package/src/extensions/registries/builtins/build-detail-after-content.ts +6 -0
  44. package/src/extensions/registries/builtins/build-detail-before-content.ts +6 -0
  45. package/src/extensions/registries/builtins/build-detail-before-hero.ts +6 -0
  46. package/src/extensions/registries/builtins/build-detail-header-actions.ts +6 -0
  47. package/src/extensions/registries/builtins/build-detail-mod-row-actions.ts +6 -0
  48. package/src/extensions/registries/builtins/build-detail-resourcepack-row-actions.ts +6 -0
  49. package/src/extensions/registries/builtins/build-detail-right-column-bottom.ts +6 -0
  50. package/src/extensions/registries/builtins/build-detail-right-column-top.ts +6 -0
  51. package/src/extensions/registries/builtins/dialog-footer-actions.ts +6 -0
  52. package/src/extensions/registries/builtins/feed-after-content.ts +6 -0
  53. package/src/extensions/registries/builtins/feed-before-content.ts +6 -0
  54. package/src/extensions/registries/builtins/file-editor.ts +19 -0
  55. package/src/extensions/registries/builtins/friends-after-list.ts +6 -0
  56. package/src/extensions/registries/builtins/friends-before-list.ts +6 -0
  57. package/src/extensions/registries/builtins/index.ts +141 -0
  58. package/src/extensions/registries/builtins/invite-handler.ts +7 -0
  59. package/src/extensions/registries/builtins/library-after-content.ts +6 -0
  60. package/src/extensions/registries/builtins/library-before-content.ts +6 -0
  61. package/src/extensions/registries/builtins/loader.ts +8 -0
  62. package/src/extensions/registries/builtins/map-card-actions.ts +6 -0
  63. package/src/extensions/registries/builtins/map-card-after-meta.ts +6 -0
  64. package/src/extensions/registries/builtins/map-card-before-meta.ts +6 -0
  65. package/src/extensions/registries/builtins/map-card-footer-actions.ts +6 -0
  66. package/src/extensions/registries/builtins/map-detail-after-content.ts +6 -0
  67. package/src/extensions/registries/builtins/map-detail-before-content.ts +6 -0
  68. package/src/extensions/registries/builtins/map-detail-header-actions.ts +6 -0
  69. package/src/extensions/registries/builtins/markdown-editor-tiptap-extensions.ts +7 -0
  70. package/src/extensions/registries/builtins/markdown-editor-toolbar-actions.ts +6 -0
  71. package/src/extensions/registries/builtins/markdown-renderer-after-content.ts +6 -0
  72. package/src/extensions/registries/builtins/markdown-renderer-before-content.ts +6 -0
  73. package/src/extensions/registries/builtins/mod-details-footer-actions.ts +6 -0
  74. package/src/extensions/registries/builtins/mod-manage-actions.ts +6 -0
  75. package/src/extensions/registries/builtins/mod-provider.ts +5 -0
  76. package/src/extensions/registries/builtins/nav.ts +7 -0
  77. package/src/extensions/registries/builtins/page.ts +13 -0
  78. package/src/extensions/registries/builtins/projects-after-content.ts +6 -0
  79. package/src/extensions/registries/builtins/projects-before-content.ts +6 -0
  80. package/src/extensions/registries/builtins/resourcepack-manage-actions.ts +7 -0
  81. package/src/extensions/registries/builtins/server-card-actions.ts +6 -0
  82. package/src/extensions/registries/builtins/server-card-after-meta.ts +6 -0
  83. package/src/extensions/registries/builtins/server-card-before-meta.ts +6 -0
  84. package/src/extensions/registries/builtins/server-card-footer-actions.ts +6 -0
  85. package/src/extensions/registries/builtins/server-detail-after-content.ts +6 -0
  86. package/src/extensions/registries/builtins/server-detail-before-content.ts +6 -0
  87. package/src/extensions/registries/builtins/server-detail-header-actions.ts +6 -0
  88. package/src/extensions/registries/builtins/settings-after-sections.ts +6 -0
  89. package/src/extensions/registries/builtins/settings-before-sections.ts +6 -0
  90. package/src/extensions/registries/builtins/settings-section-widgets.ts +6 -0
  91. package/src/extensions/registries/builtins/shaderpack-manage-actions.ts +7 -0
  92. package/src/extensions/registries/builtins/shell.ts +5 -0
  93. package/src/extensions/registries/builtins/sidebar-after-content.ts +6 -0
  94. package/src/extensions/registries/builtins/sidebar-before-content.ts +6 -0
  95. package/src/extensions/registries/builtins/sidebar-footer-widgets.ts +6 -0
  96. package/src/extensions/registries/builtins/sidebar-header-widgets.ts +6 -0
  97. package/src/extensions/registries/builtins/sidebar.ts +11 -0
  98. package/src/extensions/registries/builtins/theme.ts +5 -0
  99. package/src/extensions/registries/builtins/user-card-after-meta.ts +6 -0
  100. package/src/extensions/registries/builtins/user-card-before-meta.ts +6 -0
  101. package/src/extensions/registries/builtins/user-menu-actions.ts +6 -0
  102. package/src/extensions/registries/builtins/user-menu-after-actions.ts +6 -0
  103. package/src/extensions/registries/builtins/user-menu-before-actions.ts +6 -0
  104. package/src/extensions/registries/builtins/user-strip.ts +5 -0
  105. package/src/extensions/registries/clear-extension-ui-registries.ts +15 -0
  106. package/src/extensions/registries/define-extension-registry.ts +58 -0
  107. package/src/extensions/registries/extension-host-api.ts +41 -0
  108. package/src/extensions/registries/extension-registry-api.ts +103 -0
  109. package/src/extensions/registries/extension-registry-payload-map.ts +9 -0
  110. package/src/extensions/registries/extension-scope.ts +41 -0
  111. package/src/extensions/registries/get-registry.ts +23 -0
  112. package/src/extensions/registries/index.ts +58 -0
  113. package/src/extensions/registries/manifest-rynt.ts +193 -0
  114. package/src/extensions/registries/registry-slot.ts +40 -0
  115. package/src/extensions/registries/registry-value-map.ts +89 -0
  116. package/src/extensions/registries/store.ts +206 -0
  117. package/src/extensions/resolve-extensions.ts +245 -0
  118. package/src/extensions/router-bridge.ts +103 -0
  119. package/src/extensions/session.ts +6 -0
  120. package/src/extensions/slug.ts +23 -0
  121. package/src/extensions/version.ts +147 -0
  122. package/src/host/extensions-composables.ts +33 -0
  123. package/src/host/extensions-init.ts +194 -0
  124. package/src/host/index.ts +11 -0
  125. package/src/host/launcher-models/index.ts +4 -0
  126. package/src/index.ts +229 -0
  127. package/src/minecraft-loader/base-loader.ts +102 -0
  128. package/src/minecraft-loader/index.ts +11 -0
  129. package/src/minecraft-loader/loader-registry.ts +72 -0
  130. package/src/shared/api/assets.ts +112 -0
  131. package/src/shared/api/auth.ts +283 -0
  132. package/src/shared/api/builds.ts +647 -0
  133. package/src/shared/api/config.ts +19 -0
  134. package/src/shared/api/download-stats.ts +103 -0
  135. package/src/shared/api/downloads.ts +36 -0
  136. package/src/shared/api/entity-authorship.ts +60 -0
  137. package/src/shared/api/events.ts +393 -0
  138. package/src/shared/api/friends.ts +140 -0
  139. package/src/shared/api/graphql.ts +87 -0
  140. package/src/shared/api/index.ts +23 -0
  141. package/src/shared/api/invites.ts +262 -0
  142. package/src/shared/api/library.ts +44 -0
  143. package/src/shared/api/maps.ts +385 -0
  144. package/src/shared/api/notify-websocket.ts +140 -0
  145. package/src/shared/api/posts.ts +357 -0
  146. package/src/shared/api/projectServers.ts +379 -0
  147. package/src/shared/api/serverMembers.ts +173 -0
  148. package/src/shared/api/users.ts +294 -0
  149. package/src/shared/composables/buildEditor/useBuildEditor.ts +66 -0
  150. package/src/shared/composables/buildManifest/buildManifest.ts +447 -0
  151. package/src/shared/composables/filesEditor/filesEditor.ts +346 -0
  152. package/src/shared/composables/index.ts +10 -0
  153. package/src/shared/composables/modsEditor/modsEditor.ts +1678 -0
  154. package/src/shared/composables/registrySlot/registry-slot-utils.ts +25 -0
  155. package/src/shared/composables/registrySlot/useRegistrySlotMissing.ts +35 -0
  156. package/src/shared/composables/resourcePacksEditor/resourcePacksEditor.ts +448 -0
  157. package/src/shared/composables/shaderPacksEditor/shaderPacksEditor.ts +395 -0
  158. package/src/shared/composables/useSkinRender.ts +70 -0
  159. package/src/shared/composables/useZlDeepLink.ts +178 -0
  160. package/src/shared/definitions/defineGraphCache.ts +216 -0
  161. package/src/shared/definitions/defineStore.ts +32 -0
  162. package/src/shared/definitions/index.ts +2 -0
  163. package/src/shared/minecraft-types/build-manifest.ts +611 -0
  164. package/src/shared/minecraft-types/index.ts +3 -0
  165. package/src/shared/minecraft-types/launcher-versions.ts +32 -0
  166. package/src/shared/minecraft-types/minecraft-launcher-types.ts +276 -0
  167. package/src/shared/mocks/index.ts +1 -0
  168. package/src/shared/mocks/navigation.ts +17 -0
  169. package/src/shared/mods/http.ts +45 -0
  170. package/src/shared/mods/index.ts +5 -0
  171. package/src/shared/mods/marketplace-editor-search.ts +266 -0
  172. package/src/shared/mods/marketplace-search-utils.ts +42 -0
  173. package/src/shared/mods/mod-marketplace-registry.ts +66 -0
  174. package/src/shared/mods/mod-marketplace-types.ts +28 -0
  175. package/src/shared/mods/providers/curseforge.ts +464 -0
  176. package/src/shared/mods/providers/index.ts +8 -0
  177. package/src/shared/mods/providers/modrinth.ts +402 -0
  178. package/src/shared/mods/resolve-mods-provider-loader-ids.ts +77 -0
  179. package/src/shared/mods/types.ts +76 -0
  180. package/src/shared/styles/index.css +713 -0
  181. package/src/shared/themes/index.ts +23 -0
  182. package/src/shared/themes/theme-tokens-black.json +126 -0
  183. package/src/shared/themes/theme-tokens-classic.json +126 -0
  184. package/src/shared/themes/theme-tokens-pink.json +126 -0
  185. package/src/shared/themes/theme-tokens.json +126 -0
  186. package/src/shared/themes/types.ts +85 -0
  187. package/src/shared/types/API_DOCUMENTATION.md +422 -0
  188. package/src/shared/types/account.ts +40 -0
  189. package/src/shared/types/build.ts +8 -0
  190. package/src/shared/types/entities.ts +181 -0
  191. package/src/shared/types/index.ts +6 -0
  192. package/src/shared/types/invite-payloads.ts +60 -0
  193. package/src/shared/types/navigation.ts +16 -0
  194. package/src/shared/types/running-build.ts +51 -0
  195. package/src/shared/types/serverMember.ts +17 -0
  196. package/src/shared/types/user.ts +55 -0
  197. package/src/shared/ui/base/Avatar.vue +262 -0
  198. package/src/shared/ui/base/Badge.vue +47 -0
  199. package/src/shared/ui/base/Button.vue +78 -0
  200. package/src/shared/ui/base/Divider.vue +42 -0
  201. package/src/shared/ui/base/Icon.vue +597 -0
  202. package/src/shared/ui/base/StatusIndicator.vue +44 -0
  203. package/src/shared/ui/base/index.ts +7 -0
  204. package/src/shared/ui/cards/InviteCard.vue +47 -0
  205. package/src/shared/ui/cards/index.ts +2 -0
  206. package/src/shared/ui/dialog/Dialog.vue +71 -0
  207. package/src/shared/ui/dialog/DialogContent.vue +31 -0
  208. package/src/shared/ui/dialog/DialogFooter.vue +14 -0
  209. package/src/shared/ui/dialog/DialogHeader.vue +41 -0
  210. package/src/shared/ui/dialog/index.ts +5 -0
  211. package/src/shared/ui/editors/AttachmentImagesEditor.vue +133 -0
  212. package/src/shared/ui/editors/ContentAttachmentsDisplay.vue +76 -0
  213. package/src/shared/ui/editors/MarkdownEditor.vue +956 -0
  214. package/src/shared/ui/editors/MarkdownRenderer.vue +299 -0
  215. package/src/shared/ui/editors/RichContentImageViewer.vue +85 -0
  216. package/src/shared/ui/editors/SocialPostMediaZone.vue +320 -0
  217. package/src/shared/ui/editors/index.ts +6 -0
  218. package/src/shared/ui/editors/markdown-editor-gallery.ts +234 -0
  219. package/src/shared/ui/editors/markdown-editor-image.ts +178 -0
  220. package/src/shared/ui/form/Checkbox.vue +38 -0
  221. package/src/shared/ui/form/FormField.vue +30 -0
  222. package/src/shared/ui/form/FormGrid.vue +38 -0
  223. package/src/shared/ui/form/ImageEditor.vue +598 -0
  224. package/src/shared/ui/form/Input.vue +72 -0
  225. package/src/shared/ui/form/Range.vue +65 -0
  226. package/src/shared/ui/form/Select.vue +76 -0
  227. package/src/shared/ui/form/Switch.vue +38 -0
  228. package/src/shared/ui/form/Textarea.vue +144 -0
  229. package/src/shared/ui/form/index.ts +9 -0
  230. package/src/shared/ui/index.ts +9 -0
  231. package/src/shared/ui/layout/BusyOverlay.vue +31 -0
  232. package/src/shared/ui/layout/Callout.vue +44 -0
  233. package/src/shared/ui/layout/Card.vue +38 -0
  234. package/src/shared/ui/layout/Container.vue +36 -0
  235. package/src/shared/ui/layout/EmptyState.vue +99 -0
  236. package/src/shared/ui/layout/EntityMediaRow.vue +54 -0
  237. package/src/shared/ui/layout/FilterResultsLayout.vue +22 -0
  238. package/src/shared/ui/layout/FloatingPanel.vue +37 -0
  239. package/src/shared/ui/layout/FullscreenDimmer.vue +11 -0
  240. package/src/shared/ui/layout/Grid.vue +40 -0
  241. package/src/shared/ui/layout/Inline.vue +59 -0
  242. package/src/shared/ui/layout/LoadingState.vue +39 -0
  243. package/src/shared/ui/layout/MediaBox.vue +47 -0
  244. package/src/shared/ui/layout/OverlayPanel.vue +28 -0
  245. package/src/shared/ui/layout/OverlayWaitPanel.vue +22 -0
  246. package/src/shared/ui/layout/PageSection.vue +43 -0
  247. package/src/shared/ui/layout/PageToolbar.vue +29 -0
  248. package/src/shared/ui/layout/Panel.vue +39 -0
  249. package/src/shared/ui/layout/ProgressBar.vue +49 -0
  250. package/src/shared/ui/layout/Section.vue +30 -0
  251. package/src/shared/ui/layout/SegmentedControl.vue +43 -0
  252. package/src/shared/ui/layout/SelectableCard.vue +46 -0
  253. package/src/shared/ui/layout/SelectableRow.vue +41 -0
  254. package/src/shared/ui/layout/Skeleton.vue +25 -0
  255. package/src/shared/ui/layout/SkeletonAvatar.vue +30 -0
  256. package/src/shared/ui/layout/SkeletonEntityCard.vue +20 -0
  257. package/src/shared/ui/layout/SkeletonFeedPost.vue +22 -0
  258. package/src/shared/ui/layout/SkeletonGrid.vue +18 -0
  259. package/src/shared/ui/layout/SkeletonListRow.vue +31 -0
  260. package/src/shared/ui/layout/SkeletonText.vue +25 -0
  261. package/src/shared/ui/layout/Stack.vue +42 -0
  262. package/src/shared/ui/layout/StateBlock.vue +44 -0
  263. package/src/shared/ui/layout/TwoPaneLayout.vue +35 -0
  264. package/src/shared/ui/layout/VirtualList.vue +160 -0
  265. package/src/shared/ui/layout/index.ts +35 -0
  266. package/src/shared/ui/layout/skeletonSurfaceStyles.ts +24 -0
  267. package/src/shared/ui/navigation/NavItem.vue +139 -0
  268. package/src/shared/ui/navigation/Tab.vue +61 -0
  269. package/src/shared/ui/navigation/Tabs.vue +37 -0
  270. package/src/shared/ui/navigation/index.ts +4 -0
  271. package/src/shared/ui/primitives/Action.vue +19 -0
  272. package/src/shared/ui/primitives/Block.vue +28 -0
  273. package/src/shared/ui/primitives/CanvasView.vue +19 -0
  274. package/src/shared/ui/primitives/Control.vue +24 -0
  275. package/src/shared/ui/primitives/ControlSelect.vue +19 -0
  276. package/src/shared/ui/primitives/ControlTextarea.vue +17 -0
  277. package/src/shared/ui/primitives/FieldLabel.vue +19 -0
  278. package/src/shared/ui/primitives/Form.vue +19 -0
  279. package/src/shared/ui/primitives/Heading.vue +29 -0
  280. package/src/shared/ui/primitives/Image.vue +17 -0
  281. package/src/shared/ui/primitives/LineBreak.vue +3 -0
  282. package/src/shared/ui/primitives/Link.vue +19 -0
  283. package/src/shared/ui/primitives/List.vue +28 -0
  284. package/src/shared/ui/primitives/ListItem.vue +19 -0
  285. package/src/shared/ui/primitives/OptionItem.vue +19 -0
  286. package/src/shared/ui/primitives/Text.vue +28 -0
  287. package/src/shared/ui/primitives/VideoView.vue +19 -0
  288. package/src/shared/ui/primitives/index.ts +19 -0
  289. package/src/shared/ui/primitives/resolveElement.ts +25 -0
  290. package/src/shared/ui/special/AngularAccent.vue +106 -0
  291. package/src/shared/ui/special/ExtensionRegistrySlotButton.vue +143 -0
  292. package/src/shared/ui/special/InfoRow.vue +39 -0
  293. package/src/shared/ui/special/LogViewer.vue +53 -0
  294. package/src/shared/ui/special/PageHeader.vue +23 -0
  295. package/src/shared/ui/special/RegistrySlotMissingCallout.vue +48 -0
  296. package/src/shared/ui/special/WelcomeCard.vue +32 -0
  297. package/src/shared/ui/special/index.ts +9 -0
  298. package/src/shared/utils/app-paths.ts +50 -0
  299. package/src/shared/utils/attachments.ts +16 -0
  300. package/src/shared/utils/autostart.ts +213 -0
  301. package/src/shared/utils/build-files.ts +439 -0
  302. package/src/shared/utils/build-manifest-init.ts +176 -0
  303. package/src/shared/utils/cloudinary.ts +67 -0
  304. package/src/shared/utils/cn.ts +7 -0
  305. package/src/shared/utils/download-stats-week.ts +165 -0
  306. package/src/shared/utils/entity-api-to-cache.ts +84 -0
  307. package/src/shared/utils/entity-build-from-api.ts +1 -0
  308. package/src/shared/utils/entity-display.ts +27 -0
  309. package/src/shared/utils/entity-map-from-api.ts +1 -0
  310. package/src/shared/utils/file-hash.ts +65 -0
  311. package/src/shared/utils/formatSize.ts +5 -0
  312. package/src/shared/utils/formatTime.ts +157 -0
  313. package/src/shared/utils/getAccountSkinRender.ts +32 -0
  314. package/src/shared/utils/index.ts +34 -0
  315. package/src/shared/utils/local-mods.ts +678 -0
  316. package/src/shared/utils/local-settings.ts +217 -0
  317. package/src/shared/utils/member-join-stats.ts +35 -0
  318. package/src/shared/utils/platform.ts +86 -0
  319. package/src/shared/utils/play-host-slug.ts +92 -0
  320. package/src/shared/utils/rich-content.ts +294 -0
  321. package/src/shared/utils/safeRequest.ts +23 -0
  322. package/src/shared/utils/semver.ts +81 -0
  323. package/src/shared/utils/serverPermissions.ts +155 -0
  324. package/src/shared/utils/skin-render-cache.ts +372 -0
  325. package/src/shared/utils/stripMarkdown.ts +45 -0
  326. package/src/shared/utils/transliterate.ts +74 -0
  327. package/src/shared/utils/updateAccountSkinRender.ts +64 -0
  328. package/src/shared/utils/updater.ts +218 -0
  329. package/src/shared/utils/uploadImage.ts +195 -0
  330. package/src/shared/utils/user-status.ts +9 -0
  331. package/src/tiptap/index.ts +7 -0
  332. package/tsconfig.json +13 -0
@@ -0,0 +1,1678 @@
1
+ import { defineStore } from '../../definitions/defineStore';
2
+ import { computed, reactive, ref, watch, type Ref } from 'vue';
3
+ import { debounce } from 'lodash-es';
4
+ import type {
5
+ BuildVersionManifest,
6
+ Mod,
7
+ ResourcePack,
8
+ } from '../../minecraft-types/build-manifest';
9
+ import { useLocalBuildsModel } from '@/models';
10
+ import type {
11
+ MarketplaceInstallVersion,
12
+ MarketplaceItem,
13
+ MarketplaceSearchContext,
14
+ ProjectType,
15
+ InstallContext,
16
+ ProjectDetails,
17
+ } from '../../mods/types';
18
+ import {
19
+ getModMarketplaceProvider,
20
+ listModMarketplaceProviders,
21
+ } from '../../mods/mod-marketplace-registry';
22
+ import {
23
+ mergeMarketplaceItems,
24
+ runMarketplaceSearch,
25
+ } from '../../mods/marketplace-search-utils';
26
+ import { resolveModsProviderLoaderIds } from '../../mods/resolve-mods-provider-loader-ids';
27
+
28
+ function withMarketplaceLoaderIdsForProvider<
29
+ T extends { loader?: string },
30
+ >(ctx: T, providerId: string): T & { marketplaceLoaderIds: string[] } {
31
+ return {
32
+ ...ctx,
33
+ marketplaceLoaderIds: resolveModsProviderLoaderIds(ctx.loader, providerId),
34
+ };
35
+ }
36
+ import {
37
+ scanLocalMods,
38
+ toggleModDisabled,
39
+ deleteLocalMod,
40
+ } from '../../utils/local-mods';
41
+
42
+ /** Вкладки: встроенные + id зарегистрированных расширением провайдеров. */
43
+ export type ModsEditorMarketplaceTab = string;
44
+
45
+ export const useModsEditorModel = defineStore(
46
+ (_manifestRef: Ref<BuildVersionManifest | null>) => {
47
+ // Manifest ref - получаем напрямую при создании store
48
+ const manifest = _manifestRef;
49
+
50
+ const searchQuery = ref('');
51
+ const projectType = ref<ProjectType>('mod');
52
+ const marketplaceTab = ref<ModsEditorMarketplaceTab>('all');
53
+
54
+ const isSearching = ref(false);
55
+ const searchError = ref<string | null>(null);
56
+
57
+ const searchResultsByProvider = reactive<Record<string, MarketplaceItem[]>>(
58
+ {},
59
+ );
60
+ const isSearchingByProvider = reactive<Record<string, boolean>>({});
61
+ const isLoadingMoreByProvider = reactive<Record<string, boolean>>({});
62
+ const hasMoreByProvider = reactive<Record<string, boolean>>({});
63
+ const searchPageByProvider = reactive<Record<string, number>>({});
64
+ const marketplaceBrowseStarted = ref(false);
65
+
66
+ function syncMarketplaceProviderSearchKeys(): void {
67
+ for (const p of listModMarketplaceProviders()) {
68
+ if (!(p.id in searchResultsByProvider)) {
69
+ searchResultsByProvider[p.id] = [];
70
+ }
71
+ if (!(p.id in isSearchingByProvider)) {
72
+ isSearchingByProvider[p.id] = false;
73
+ }
74
+ if (!(p.id in isLoadingMoreByProvider)) {
75
+ isLoadingMoreByProvider[p.id] = false;
76
+ }
77
+ if (!(p.id in hasMoreByProvider)) {
78
+ hasMoreByProvider[p.id] = false;
79
+ }
80
+ if (!(p.id in searchPageByProvider)) {
81
+ searchPageByProvider[p.id] = -1;
82
+ }
83
+ }
84
+ }
85
+
86
+ function getProvidersForLoadMore(tabId: string): string[] {
87
+ syncMarketplaceProviderSearchKeys();
88
+ if (tabId === 'all') {
89
+ return listModMarketplaceProviders()
90
+ .map((p) => p.id)
91
+ .filter((id) => hasMoreByProvider[id]);
92
+ }
93
+ return hasMoreByProvider[tabId] ? [tabId] : [];
94
+ }
95
+
96
+ function canLoadMoreMarketplace(tabId: string): boolean {
97
+ return getProvidersForLoadMore(tabId).length > 0;
98
+ }
99
+
100
+ function isLoadingMoreMarketplace(tabId: string): boolean {
101
+ syncMarketplaceProviderSearchKeys();
102
+ if (tabId === 'all') {
103
+ return listModMarketplaceProviders().some(
104
+ (p) => isLoadingMoreByProvider[p.id],
105
+ );
106
+ }
107
+ return Boolean(isLoadingMoreByProvider[tabId]);
108
+ }
109
+
110
+ async function marketplaceItemForProvider(
111
+ providerId: string,
112
+ projectId: string,
113
+ projectType: ProjectType,
114
+ ): Promise<MarketplaceItem | null> {
115
+ const prov = getModMarketplaceProvider(providerId);
116
+ if (!prov) return null;
117
+ try {
118
+ const d = await prov.getProjectDetails(projectId);
119
+ return {
120
+ provider: d.provider,
121
+ projectType,
122
+ projectId: d.projectId,
123
+ slug: d.slug,
124
+ name: d.name,
125
+ summary: d.summary,
126
+ iconUrl: d.iconUrl,
127
+ downloads: d.downloads,
128
+ };
129
+ } catch (e) {
130
+ console.warn(
131
+ `[modsEditor] Could not load marketplace item ${providerId}:${projectId}:`,
132
+ e,
133
+ );
134
+ return null;
135
+ }
136
+ }
137
+
138
+ const selectedModDetails = ref<ProjectDetails | null>(null);
139
+ const isLoadingModDetails = ref(false);
140
+ const modDetailsError = ref<string | null>(null);
141
+
142
+ // Incompatibility dialog state
143
+ const incompatibilityDialog = ref<{
144
+ open: boolean;
145
+ incompatibleMod: Mod | null;
146
+ conflictingMod: Mod | null;
147
+ resolveCallback: ((proceed: boolean) => void) | null;
148
+ }>({
149
+ open: false,
150
+ incompatibleMod: null,
151
+ conflictingMod: null,
152
+ resolveCallback: null,
153
+ });
154
+
155
+ // Dependency removal dialog state
156
+ const dependencyRemovalDialog = ref<{
157
+ open: boolean;
158
+ modToRemove: Mod | null;
159
+ dependentMods: Mod[];
160
+ resolveCallback:
161
+ | ((action: 'cancel' | 'remove-only' | 'remove-all') => void)
162
+ | null;
163
+ }>({
164
+ open: false,
165
+ modToRemove: null,
166
+ dependentMods: [],
167
+ resolveCallback: null,
168
+ });
169
+
170
+ // Version selection dialog state
171
+ const versionSelectionDialog = ref<{
172
+ open: boolean;
173
+ item: MarketplaceItem | null;
174
+ versions: MarketplaceInstallVersion[];
175
+ selectedVersionId: string | null;
176
+ resolveCallback: ((selectedVersionId: string | null) => void) | null;
177
+ }>({
178
+ open: false,
179
+ item: null,
180
+ versions: [],
181
+ selectedVersionId: null,
182
+ resolveCallback: null,
183
+ });
184
+
185
+ // Dependency issues dialog state
186
+ type DependencyIssue =
187
+ | {
188
+ type: 'missing';
189
+ mod: Mod;
190
+ dependency: {
191
+ slug: string;
192
+ modId: string;
193
+ source: string;
194
+ name?: string;
195
+ };
196
+ }
197
+ | { type: 'incompatible'; mod: Mod; conflictingMod: Mod }
198
+ | { type: 'incompatible-version'; mod: Mod; reason: string }; // Mod incompatible with selected loader/version
199
+
200
+ const dependencyIssuesDialog = ref<{
201
+ open: boolean;
202
+ issues: DependencyIssue[];
203
+ isResolving: boolean;
204
+ isResolved: boolean;
205
+ }>({
206
+ open: false,
207
+ issues: [],
208
+ isResolving: false,
209
+ isResolved: false,
210
+ });
211
+
212
+ // Store original manifest for comparing with local mods
213
+ const originalManifest = ref<BuildVersionManifest | null>(null);
214
+
215
+ // Local mods (not in manifest)
216
+ const localMods = ref<Mod[]>([]);
217
+ const localResourcePacks = ref<ResourcePack[]>([]);
218
+
219
+ // Computed, которые читают напрямую из manifest (теперь работают, т.к. manifest установлен при создании)
220
+ const installedMods = computed<Mod[]>(
221
+ () => (manifest.value?.mods ?? []) as Mod[],
222
+ );
223
+ const installedResourcePacks = computed<ResourcePack[]>(
224
+ () => (manifest.value?.resourcePacks ?? []) as ResourcePack[],
225
+ );
226
+
227
+ // All mods including local ones
228
+ const allMods = computed<Mod[]>(() => [
229
+ ...installedMods.value,
230
+ ...localMods.value,
231
+ ]);
232
+ const allResourcePacks = computed<ResourcePack[]>(() => [
233
+ ...installedResourcePacks.value,
234
+ ...localResourcePacks.value,
235
+ ]);
236
+
237
+ function isInstalledBySlug(slug: string): boolean {
238
+ if (projectType.value === 'resourcepack') {
239
+ return installedResourcePacks.value.some(
240
+ (rp) => rp.projectSlug === slug || rp.id === slug,
241
+ );
242
+ }
243
+ return installedMods.value.some(
244
+ (m) => m.projectSlug === slug || m.id === slug,
245
+ );
246
+ }
247
+
248
+ async function searchProvider(
249
+ providerId: string,
250
+ reset: boolean,
251
+ baseSearchCtx: MarketplaceSearchContext,
252
+ ): Promise<void> {
253
+ const prov = getModMarketplaceProvider(providerId);
254
+ if (!prov) return;
255
+
256
+ const page = reset ? 0 : (searchPageByProvider[providerId] ?? -1) + 1;
257
+ const ctx = withMarketplaceLoaderIdsForProvider(
258
+ { ...baseSearchCtx, page },
259
+ providerId,
260
+ );
261
+
262
+ if (reset) {
263
+ isSearchingByProvider[providerId] = true;
264
+ } else {
265
+ isLoadingMoreByProvider[providerId] = true;
266
+ }
267
+
268
+ try {
269
+ const result = await runMarketplaceSearch(prov, ctx);
270
+ if (reset) {
271
+ searchResultsByProvider[providerId] = result.items;
272
+ } else {
273
+ searchResultsByProvider[providerId] = mergeMarketplaceItems(
274
+ searchResultsByProvider[providerId] ?? [],
275
+ result.items,
276
+ );
277
+ }
278
+ hasMoreByProvider[providerId] = result.hasMore;
279
+ searchPageByProvider[providerId] = page;
280
+ } catch (e) {
281
+ console.warn(`[modsEditor] ${providerId} search failed:`, e);
282
+ if (reset) {
283
+ searchResultsByProvider[providerId] = [];
284
+ hasMoreByProvider[providerId] = false;
285
+ searchPageByProvider[providerId] = -1;
286
+ }
287
+ } finally {
288
+ if (reset) {
289
+ isSearchingByProvider[providerId] = false;
290
+ } else {
291
+ isLoadingMoreByProvider[providerId] = false;
292
+ }
293
+ }
294
+ }
295
+
296
+ async function searchProviders(
297
+ providerIds: string[],
298
+ reset: boolean,
299
+ ): Promise<void> {
300
+ syncMarketplaceProviderSearchKeys();
301
+ marketplaceBrowseStarted.value = true;
302
+
303
+ const q = searchQuery.value;
304
+ const baseSearchCtx: MarketplaceSearchContext = {
305
+ projectType: projectType.value,
306
+ query: q,
307
+ minecraftVersion:
308
+ manifest.value?._meta?.manifest?.minecraftVersion || undefined,
309
+ loader: manifest.value?._meta?.manifest?.loader || undefined,
310
+ };
311
+
312
+ if (reset) {
313
+ isSearching.value = true;
314
+ searchError.value = null;
315
+ for (const id of providerIds) {
316
+ searchResultsByProvider[id] = [];
317
+ hasMoreByProvider[id] = false;
318
+ searchPageByProvider[id] = -1;
319
+ }
320
+ }
321
+
322
+ try {
323
+ await Promise.allSettled(
324
+ providerIds.map((id) => searchProvider(id, reset, baseSearchCtx)),
325
+ );
326
+ } catch (e) {
327
+ searchError.value = e instanceof Error ? e.message : String(e);
328
+ } finally {
329
+ if (reset) {
330
+ isSearching.value = false;
331
+ }
332
+ }
333
+ }
334
+
335
+ async function search(reset = true) {
336
+ const allIds = listModMarketplaceProviders().map((p) => p.id);
337
+ await searchProviders(allIds, reset);
338
+ }
339
+
340
+ async function loadMoreMarketplaceResults(): Promise<void> {
341
+ if (isSearching.value) return;
342
+ const tabId = marketplaceTab.value;
343
+ const providerIds = getProvidersForLoadMore(tabId);
344
+ if (!providerIds.length) return;
345
+ if (isLoadingMoreMarketplace(tabId)) return;
346
+ await searchProviders(providerIds, false);
347
+ }
348
+
349
+ async function install(
350
+ item: MarketplaceItem,
351
+ options?: { skipVersionPrompt?: boolean; selectedVersionId?: string },
352
+ ) {
353
+ if (!manifest.value) throw new Error('Manifest is not initialized');
354
+
355
+ if (!manifest.value.mods) {
356
+ manifest.value.mods = [];
357
+ }
358
+
359
+ const mcVersion = manifest.value._meta?.manifest?.minecraftVersion;
360
+ if (!mcVersion || mcVersion.trim() === '')
361
+ throw new Error('Select Minecraft version first');
362
+ const loader = manifest.value._meta?.manifest?.loader;
363
+ if (!loader || loader.trim() === '')
364
+ throw new Error('Select loader first');
365
+
366
+ const baseInstallCtx: InstallContext = {
367
+ projectType: projectType.value,
368
+ minecraftVersion:
369
+ manifest.value?._meta?.manifest?.minecraftVersion || '',
370
+ loader: manifest.value?._meta?.manifest?.loader || 'vanilla',
371
+ };
372
+ const installCtx = withMarketplaceLoaderIdsForProvider(
373
+ baseInstallCtx,
374
+ item.provider,
375
+ );
376
+
377
+ let selectedVersionId: string | undefined = options?.selectedVersionId;
378
+ const shouldSelectVersion =
379
+ !selectedVersionId &&
380
+ !options?.skipVersionPrompt &&
381
+ projectType.value === 'mod';
382
+ if (shouldSelectVersion) {
383
+ const prov = getModMarketplaceProvider(item.provider);
384
+ if (prov?.getInstallVersions) {
385
+ const versions = await prov.getInstallVersions(
386
+ item.projectId,
387
+ installCtx,
388
+ );
389
+ if (versions.length > 1) {
390
+ const selected = await showVersionSelectionDialog(item, versions);
391
+ if (!selected) {
392
+ return;
393
+ }
394
+ selectedVersionId = selected;
395
+ }
396
+ }
397
+ }
398
+
399
+ let entry: Mod | ResourcePack;
400
+ try {
401
+ const prov = getModMarketplaceProvider(item.provider);
402
+ if (!prov) {
403
+ throw new Error(`Unknown marketplace provider: ${item.provider}`);
404
+ }
405
+ entry = await prov.install(item, installCtx, selectedVersionId);
406
+ } catch (error) {
407
+ throw error;
408
+ }
409
+
410
+ if (projectType.value === 'resourcepack') {
411
+ const rp = entry as ResourcePack;
412
+ const next = [...(manifest.value.resourcePacks ?? [])].filter(
413
+ (x) => x.id !== rp.id, // Use id (projectId) for comparison
414
+ );
415
+ next.push(rp);
416
+ manifest.value.resourcePacks = next;
417
+ return;
418
+ }
419
+
420
+ const mod = entry as Mod;
421
+
422
+ // Resolve all required dependencies first
423
+ const dependencies = await resolveDependencies(mod, installCtx);
424
+
425
+ // Check for incompatibilities with all mods that will be installed (main mod + dependencies)
426
+ const modsToCheck = [mod, ...dependencies];
427
+ const currentInstalledMods = manifest.value.mods ?? [];
428
+
429
+ for (const modToCheck of modsToCheck) {
430
+ const { conflictingMod } = checkIncompatibilities(
431
+ modToCheck,
432
+ currentInstalledMods,
433
+ );
434
+ if (conflictingMod) {
435
+ const proceed = await showIncompatibilityDialog(
436
+ modToCheck,
437
+ conflictingMod,
438
+ );
439
+ if (!proceed) {
440
+ return; // User cancelled installation
441
+ }
442
+ }
443
+
444
+ // Also check for incompatibilities between mods being installed
445
+ for (const otherMod of modsToCheck) {
446
+ if (modToCheck.id === otherMod.id) continue; // Skip self
447
+
448
+ const { conflictingMod: conflict } = checkIncompatibilities(
449
+ modToCheck,
450
+ [otherMod],
451
+ );
452
+ if (conflict) {
453
+ const proceed = await showIncompatibilityDialog(
454
+ modToCheck,
455
+ conflict,
456
+ );
457
+ if (!proceed) {
458
+ return; // User cancelled installation
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ // Add all dependencies first
465
+ const allModsToInstall = [...installedMods.value];
466
+ for (const depMod of dependencies) {
467
+ // Remove if already exists (update) - use id (projectId) for comparison
468
+ const existingIndex = allModsToInstall.findIndex(
469
+ (m) => m.id === depMod.id,
470
+ );
471
+ if (existingIndex >= 0) {
472
+ allModsToInstall[existingIndex] = depMod;
473
+ } else {
474
+ allModsToInstall.push(depMod);
475
+ }
476
+ }
477
+
478
+ // Add/update the main mod - use id (projectId) for comparison
479
+ const mainModIndex = allModsToInstall.findIndex((m) => m.id === mod.id);
480
+
481
+ // Update dependencies of main mod with actual installed versions
482
+ if (mod.dependencies && mod.dependencies.length > 0) {
483
+ mod.dependencies = mod.dependencies.map((dep) => {
484
+ // First, try to find in newly installed dependencies
485
+ const installedDep = dependencies.find(
486
+ (m) => m.id === dep.modId || m.projectSlug === dep.slug,
487
+ );
488
+ if (installedDep) {
489
+ return {
490
+ ...dep,
491
+ version: installedDep.version, // Use actual installed dependency version
492
+ };
493
+ }
494
+
495
+ // If not found in new dependencies, check already installed mods
496
+ const existingDep = allModsToInstall.find(
497
+ (m) => m.id === dep.modId || m.projectSlug === dep.slug,
498
+ );
499
+ if (existingDep) {
500
+ return {
501
+ ...dep,
502
+ version: existingDep.version, // Use version of already installed dependency
503
+ };
504
+ }
505
+
506
+ // If dependency not found (shouldn't happen for required deps), keep original
507
+ return dep;
508
+ });
509
+ }
510
+
511
+ if (mainModIndex >= 0) {
512
+ allModsToInstall[mainModIndex] = mod;
513
+ } else {
514
+ allModsToInstall.push(mod);
515
+ }
516
+
517
+ console.log('[modsEditor.install] Добавляем мод:', {
518
+ modId: mod.id,
519
+ modName: mod.name,
520
+ allModsToInstallCount: allModsToInstall.length,
521
+ installedModsCountBefore: installedMods.value.length,
522
+ manifestValueExists: !!manifest.value,
523
+ manifestModsExists: !!manifest.value?.mods,
524
+ });
525
+
526
+ // Убеждаемся, что массив mods инициализирован
527
+ if (!manifest.value.mods) {
528
+ manifest.value.mods = [];
529
+ }
530
+
531
+ // Обновляем массив модов - Vue должен отследить изменение
532
+ manifest.value.mods = [...allModsToInstall];
533
+
534
+ console.log('[modsEditor.install] После добавления:', {
535
+ manifestModsCount: manifest.value.mods?.length ?? 0,
536
+ installedModsCountAfter: installedMods.value.length,
537
+ allModsComputedCount: allMods.value.length,
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Find all mods that depend on the given mod
543
+ * Uses slug for matching (as per new dependency format)
544
+ */
545
+ function findDependentMods(mod: Mod): Mod[] {
546
+ const installed = installedMods.value;
547
+ const dependent: Mod[] = [];
548
+
549
+ for (const installedMod of installed) {
550
+ if (
551
+ installedMod.id === mod.id ||
552
+ installedMod.projectSlug === mod.projectSlug
553
+ ) {
554
+ continue; // Skip the mod itself
555
+ }
556
+
557
+ // Check if this mod has a required dependency on the mod we're removing
558
+ // Match by slug (primary) or fallback to modId for backwards compatibility
559
+ const hasRequiredDep = installedMod.dependencies?.some(
560
+ (dep) =>
561
+ dep.type === 'required' &&
562
+ (dep.slug === mod.projectSlug ||
563
+ dep.modId === mod.id ||
564
+ dep.modId === mod.projectSlug),
565
+ );
566
+
567
+ if (hasRequiredDep) {
568
+ dependent.push(installedMod);
569
+ }
570
+ }
571
+
572
+ return dependent;
573
+ }
574
+
575
+ /**
576
+ * Find all mods that can be safely removed (no one depends on them)
577
+ * Recursively finds orphaned dependencies that were installed only for the removed mod
578
+ */
579
+ function findOrphanedDependencies(
580
+ removedMod: Mod,
581
+ remainingMods: Mod[],
582
+ ): Mod[] {
583
+ const orphaned: Mod[] = [];
584
+ const processed = new Set<string>();
585
+
586
+ function findOrphans(currentMod: Mod) {
587
+ const modKey = `${currentMod.source}:${currentMod.projectSlug || currentMod.id}`;
588
+ if (processed.has(modKey)) return;
589
+ processed.add(modKey);
590
+
591
+ // Check if anyone in remaining mods depends on this mod
592
+ // Use slug for matching (as per new dependency format)
593
+ const hasDependents = remainingMods.some((m) => {
594
+ if (m.id === currentMod.id) return false;
595
+ return m.dependencies?.some(
596
+ (dep) =>
597
+ dep.type === 'required' &&
598
+ (dep.slug === currentMod.projectSlug ||
599
+ dep.modId === currentMod.id),
600
+ );
601
+ });
602
+
603
+ if (!hasDependents) {
604
+ // This mod is orphaned - no one depends on it
605
+ orphaned.push(currentMod);
606
+
607
+ // Recursively check this mod's dependencies (they might also be orphaned)
608
+ const requiredDeps =
609
+ currentMod.dependencies?.filter((d) => d.type === 'required') ?? [];
610
+ for (const dep of requiredDeps) {
611
+ // Find dependency mod by slug (primary) or modId (fallback)
612
+ const depMod = remainingMods.find(
613
+ (m) => m.projectSlug === dep.slug || m.id === dep.modId,
614
+ );
615
+ if (depMod) {
616
+ findOrphans(depMod);
617
+ }
618
+ }
619
+ }
620
+ }
621
+
622
+ // Start from the removed mod's dependencies
623
+ const requiredDeps =
624
+ removedMod.dependencies?.filter((d) => d.type === 'required') ?? [];
625
+ for (const dep of requiredDeps) {
626
+ const depMod = remainingMods.find(
627
+ (m) => m.projectSlug === dep.modId || m.id === dep.modId,
628
+ );
629
+ if (depMod) {
630
+ findOrphans(depMod);
631
+ }
632
+ }
633
+
634
+ return orphaned;
635
+ }
636
+
637
+ /**
638
+ * Show dependency removal dialog and wait for user decision
639
+ */
640
+ function showDependencyRemovalDialog(
641
+ modToRemove: Mod,
642
+ dependentMods: Mod[],
643
+ ): Promise<'cancel' | 'remove-only' | 'remove-all'> {
644
+ return new Promise((resolve) => {
645
+ dependencyRemovalDialog.value = {
646
+ open: true,
647
+ modToRemove,
648
+ dependentMods,
649
+ resolveCallback: resolve,
650
+ };
651
+ });
652
+ }
653
+
654
+ function handleDependencyRemovalCancel() {
655
+ if (dependencyRemovalDialog.value.resolveCallback) {
656
+ dependencyRemovalDialog.value.resolveCallback('cancel');
657
+ }
658
+ dependencyRemovalDialog.value = {
659
+ open: false,
660
+ modToRemove: null,
661
+ dependentMods: [],
662
+ resolveCallback: null,
663
+ };
664
+ }
665
+
666
+ function handleDependencyRemovalOnly() {
667
+ if (dependencyRemovalDialog.value.resolveCallback) {
668
+ dependencyRemovalDialog.value.resolveCallback('remove-only');
669
+ }
670
+ dependencyRemovalDialog.value = {
671
+ open: false,
672
+ modToRemove: null,
673
+ dependentMods: [],
674
+ resolveCallback: null,
675
+ };
676
+ }
677
+
678
+ function handleDependencyRemovalAll() {
679
+ if (dependencyRemovalDialog.value.resolveCallback) {
680
+ dependencyRemovalDialog.value.resolveCallback('remove-all');
681
+ }
682
+ dependencyRemovalDialog.value = {
683
+ open: false,
684
+ modToRemove: null,
685
+ dependentMods: [],
686
+ resolveCallback: null,
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Check if a mod has all its required dependencies installed
692
+ * Uses slug and version for matching (as per new dependency format)
693
+ */
694
+ function checkModDependenciesStatus(mod: Mod): {
695
+ isValid: boolean;
696
+ missingDependencies: string[];
697
+ } {
698
+ const missing: string[] = [];
699
+ const installed = installedMods.value;
700
+ const currentMc = manifest.value?._meta?.manifest?.minecraftVersion || '';
701
+ const currentLoader =
702
+ manifest.value?._meta?.manifest?.loader || 'vanilla';
703
+
704
+ // Check compatibility with selected loader and Minecraft version
705
+ if (mod.constraints) {
706
+ const mcMatch = mod.constraints.minecraft === currentMc;
707
+ const loaderMatch = mod.constraints.loader === currentLoader;
708
+
709
+ if (!mcMatch || !loaderMatch) {
710
+ // Add compatibility issue to missing dependencies for UI display
711
+ const reasons: string[] = [];
712
+ if (!mcMatch) {
713
+ reasons.push(
714
+ `Minecraft ${mod.constraints.minecraft} (выбрано: ${currentMc})`,
715
+ );
716
+ }
717
+ if (!loaderMatch) {
718
+ reasons.push(
719
+ `Loader ${mod.constraints.loader} (выбран: ${currentLoader})`,
720
+ );
721
+ }
722
+ missing.push(`Несовместимость: ${reasons.join(', ')}`);
723
+ }
724
+ }
725
+
726
+ const requiredDeps =
727
+ mod.dependencies?.filter((d) => d.type === 'required') ?? [];
728
+
729
+ for (const dep of requiredDeps) {
730
+ // Check by slug (primary check) and optionally by version
731
+ const isInstalled = installed.some((installedMod) => {
732
+ const slugMatch = installedMod.projectSlug === dep.slug;
733
+ if (!slugMatch) return false;
734
+
735
+ // If version is specified in dependency, check version compatibility
736
+ // For now, we just check if slug matches (version range checking can be added later)
737
+ return true;
738
+ });
739
+
740
+ if (!isInstalled) {
741
+ missing.push(dep.slug || dep.modId);
742
+ }
743
+ }
744
+
745
+ return {
746
+ isValid: missing.length === 0,
747
+ missingDependencies: missing,
748
+ };
749
+ }
750
+
751
+ /**
752
+ * Get dependency status for all installed mods
753
+ */
754
+ const modsDependencyStatus = computed(() => {
755
+ const status = new Map<
756
+ string,
757
+ { isValid: boolean; missingDependencies: string[] }
758
+ >();
759
+ const installed = installedMods.value;
760
+ const currentMc = manifest.value?._meta?.manifest?.minecraftVersion || '';
761
+ const currentLoader =
762
+ manifest.value?._meta?.manifest?.loader || 'vanilla';
763
+
764
+ for (const mod of installed) {
765
+ const modKey = mod.id || mod.projectSlug;
766
+ const modStatus = checkModDependenciesStatus(mod);
767
+ status.set(modKey, modStatus);
768
+ }
769
+
770
+ return status;
771
+ });
772
+
773
+ /**
774
+ * Collect all dependency issues (missing dependencies and incompatibilities)
775
+ */
776
+ function collectAllDependencyIssues(): DependencyIssue[] {
777
+ const issues: DependencyIssue[] = [];
778
+ const installed = installedMods.value;
779
+ const currentMc = manifest.value?._meta?.manifest?.minecraftVersion || '';
780
+ const currentLoader =
781
+ manifest.value?._meta?.manifest?.loader || 'vanilla';
782
+
783
+ for (const mod of installed) {
784
+ // Check compatibility with selected loader and Minecraft version
785
+ if (mod.constraints) {
786
+ const mcMatch = mod.constraints.minecraft === currentMc;
787
+ const loaderMatch = mod.constraints.loader === currentLoader;
788
+
789
+ if (!mcMatch || !loaderMatch) {
790
+ const reasons: string[] = [];
791
+ if (!mcMatch) {
792
+ reasons.push(
793
+ `Minecraft ${mod.constraints.minecraft} (выбрано: ${currentMc})`,
794
+ );
795
+ }
796
+ if (!loaderMatch) {
797
+ reasons.push(
798
+ `${mod.constraints.loader} (выбран: ${currentLoader})`,
799
+ );
800
+ }
801
+
802
+ issues.push({
803
+ type: 'incompatible-version',
804
+ mod,
805
+ reason: `Несовместим с выбранными настройками: ${reasons.join(', ')}`,
806
+ });
807
+ // TODO: Implement auto-resolve: search for compatible version of this mod for current loader/version
808
+ continue; // Skip other checks for incompatible mods
809
+ }
810
+ }
811
+
812
+ // Check for missing required dependencies
813
+ const requiredDeps =
814
+ mod.dependencies?.filter((d) => d.type === 'required') ?? [];
815
+ for (const dep of requiredDeps) {
816
+ const isInstalled = installed.some(
817
+ (installedMod) =>
818
+ installedMod.projectSlug === dep.slug ||
819
+ installedMod.id === dep.modId,
820
+ );
821
+ if (!isInstalled) {
822
+ issues.push({
823
+ type: 'missing',
824
+ mod,
825
+ dependency: {
826
+ slug: dep.slug,
827
+ modId: dep.modId,
828
+ source: dep.source,
829
+ },
830
+ });
831
+ }
832
+ }
833
+
834
+ // Check for incompatibilities
835
+ const incompatibleDeps =
836
+ mod.dependencies?.filter((d) => d.type === 'incompatible') ?? [];
837
+ for (const dep of incompatibleDeps) {
838
+ const conflicting = installed.find(
839
+ (installedMod) =>
840
+ installedMod.projectSlug === dep.slug ||
841
+ installedMod.id === dep.modId,
842
+ );
843
+ if (conflicting) {
844
+ issues.push({
845
+ type: 'incompatible',
846
+ mod,
847
+ conflictingMod: conflicting,
848
+ });
849
+ }
850
+ }
851
+ }
852
+
853
+ return issues;
854
+ }
855
+
856
+ /**
857
+ * Open dependency issues dialog
858
+ */
859
+ function openDependencyIssuesDialog() {
860
+ const issues = collectAllDependencyIssues();
861
+ dependencyIssuesDialog.value = {
862
+ open: true,
863
+ issues,
864
+ isResolving: false,
865
+ isResolved: false,
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Close dependency issues dialog
871
+ */
872
+ function closeDependencyIssuesDialog() {
873
+ dependencyIssuesDialog.value = {
874
+ open: false,
875
+ issues: [],
876
+ isResolving: false,
877
+ isResolved: false,
878
+ };
879
+ }
880
+
881
+ /**
882
+ * Resolve a single issue (install missing dependency or remove incompatible mod)
883
+ */
884
+ async function resolveIssue(issue: DependencyIssue) {
885
+ if (!manifest.value) return;
886
+ const mc = manifest.value._meta?.manifest?.minecraftVersion;
887
+ const loader = manifest.value._meta?.manifest?.loader;
888
+ if (!mc || !loader) return;
889
+
890
+ dependencyIssuesDialog.value.isResolving = true;
891
+
892
+ try {
893
+ if (issue.type === 'missing') {
894
+ // Install missing dependency
895
+ const installCtx = {
896
+ projectType: projectType.value,
897
+ minecraftVersion:
898
+ manifest.value?._meta?.manifest?.minecraftVersion || '',
899
+ loader: manifest.value?._meta?.manifest?.loader || 'vanilla',
900
+ } as const;
901
+
902
+ let depItem: MarketplaceItem | null = null;
903
+ depItem = await marketplaceItemForProvider(
904
+ issue.dependency.source,
905
+ issue.dependency.modId,
906
+ installCtx.projectType,
907
+ );
908
+
909
+ if (depItem) {
910
+ await install(depItem, { skipVersionPrompt: true });
911
+ issue.dependency.name = depItem.name;
912
+ } else {
913
+ console.warn(
914
+ `[modsEditor] Could not resolve dependency ${issue.dependency.modId} from ${issue.dependency.source}`,
915
+ );
916
+ }
917
+ } else if (issue.type === 'incompatible') {
918
+ // Remove incompatible mod
919
+ await removeInstalled(issue.conflictingMod.id);
920
+ } else if (issue.type === 'incompatible-version') {
921
+ // Remove mod incompatible with selected loader/version
922
+ // TODO: Implement auto-resolve: search for compatible version of this mod for current loader/version
923
+ await removeInstalled(issue.mod.id);
924
+ }
925
+
926
+ // Refresh issues after resolution (wait a bit for reactive updates)
927
+ await new Promise((resolve) => setTimeout(resolve, 100));
928
+ const updatedIssues = collectAllDependencyIssues();
929
+ dependencyIssuesDialog.value.issues = updatedIssues;
930
+ dependencyIssuesDialog.value.isResolved = updatedIssues.length === 0;
931
+ } catch (error) {
932
+ console.error('[modsEditor] Error resolving issue:', error);
933
+ } finally {
934
+ dependencyIssuesDialog.value.isResolving = false;
935
+ }
936
+ }
937
+
938
+ /**
939
+ * Resolve all issues automatically
940
+ */
941
+ async function resolveAllIssues() {
942
+ if (!manifest.value) {
943
+ return;
944
+ }
945
+ const mc = manifest.value._meta?.manifest?.minecraftVersion;
946
+ const loader = manifest.value._meta?.manifest?.loader;
947
+ if (!mc || !loader) {
948
+ return;
949
+ }
950
+
951
+ dependencyIssuesDialog.value.isResolving = true;
952
+
953
+ try {
954
+ const issues = [...dependencyIssuesDialog.value.issues];
955
+ const installCtx = {
956
+ projectType: projectType.value,
957
+ minecraftVersion:
958
+ manifest.value?._meta?.manifest?.minecraftVersion || '',
959
+ loader: manifest.value?._meta?.manifest?.loader || 'vanilla',
960
+ } as const;
961
+
962
+ // Resolve issues in order: first install missing dependencies, then remove incompatibilities
963
+ const missingIssues = issues.filter((i) => i.type === 'missing');
964
+ const incompatibleIssues = issues.filter(
965
+ (i) => i.type === 'incompatible',
966
+ );
967
+ const incompatibleVersionIssues = issues.filter(
968
+ (i) => i.type === 'incompatible-version',
969
+ );
970
+
971
+ // Install missing dependencies first
972
+ for (const issue of missingIssues) {
973
+ if (issue.type === 'missing') {
974
+ let depItem: MarketplaceItem | null = null;
975
+ depItem = await marketplaceItemForProvider(
976
+ issue.dependency.source,
977
+ issue.dependency.modId,
978
+ installCtx.projectType,
979
+ );
980
+
981
+ if (depItem) {
982
+ await install(depItem, { skipVersionPrompt: true });
983
+ }
984
+ }
985
+ }
986
+
987
+ // Then remove incompatibilities
988
+ for (const issue of incompatibleIssues) {
989
+ if (issue.type === 'incompatible') {
990
+ await removeInstalled(issue.conflictingMod.id);
991
+ }
992
+ }
993
+
994
+ // Remove incompatible-version mods (mods incompatible with selected loader/version)
995
+ for (const issue of incompatibleVersionIssues) {
996
+ if (issue.type === 'incompatible-version') {
997
+ // TODO: Implement auto-resolve: search for compatible version of this mod for current loader/version
998
+ await removeInstalled(issue.mod.id);
999
+ }
1000
+ }
1001
+
1002
+ // Final check (wait a bit for reactive updates)
1003
+ await new Promise((resolve) => setTimeout(resolve, 100));
1004
+ const updatedIssues = collectAllDependencyIssues();
1005
+
1006
+ dependencyIssuesDialog.value.issues = updatedIssues;
1007
+ dependencyIssuesDialog.value.isResolved = updatedIssues.length === 0;
1008
+ } catch (error) {
1009
+ console.error('[modsEditor] Error resolving all issues:', error);
1010
+ } finally {
1011
+ dependencyIssuesDialog.value.isResolving = false;
1012
+ }
1013
+ }
1014
+
1015
+ async function removeInstalled(idOrSlug: string) {
1016
+ if (!manifest.value) return;
1017
+ if (projectType.value === 'resourcepack') {
1018
+ manifest.value.resourcePacks = (
1019
+ manifest.value.resourcePacks ?? []
1020
+ ).filter(
1021
+ (rp) => rp.id !== idOrSlug && rp.projectSlug !== idOrSlug, // Support both id and slug for lookup
1022
+ );
1023
+ return;
1024
+ }
1025
+
1026
+ const allMods = manifest.value.mods ?? [];
1027
+ // Support lookup by id (projectId) or slug
1028
+ const modToRemove = allMods.find(
1029
+ (m) => m.id === idOrSlug || m.projectSlug === idOrSlug,
1030
+ );
1031
+
1032
+ if (!modToRemove) {
1033
+ console.warn('[modsEditor] Mod not found for removal:', idOrSlug);
1034
+ return;
1035
+ }
1036
+
1037
+ // Check if anyone depends on this mod
1038
+ const dependentMods = findDependentMods(modToRemove);
1039
+ console.log(
1040
+ '[modsEditor] Removing mod:',
1041
+ modToRemove.name,
1042
+ 'Dependent mods found:',
1043
+ dependentMods.length,
1044
+ dependentMods.map((m) => m.name),
1045
+ );
1046
+
1047
+ if (dependentMods.length > 0) {
1048
+ // Show dialog to choose action
1049
+ console.log('[modsEditor] Showing dependency removal dialog');
1050
+ const action = await showDependencyRemovalDialog(
1051
+ modToRemove,
1052
+ dependentMods,
1053
+ );
1054
+ console.log('[modsEditor] User action:', action);
1055
+
1056
+ if (action === 'cancel') {
1057
+ return; // User cancelled
1058
+ }
1059
+
1060
+ if (action === 'remove-all') {
1061
+ // Remove the mod and all dependent mods - use id (projectId) for comparison
1062
+ const toRemoveIds = new Set([
1063
+ modToRemove.id,
1064
+ ...dependentMods.map((m) => m.id),
1065
+ ]);
1066
+
1067
+ let remainingMods = allMods.filter((m) => !toRemoveIds.has(m.id));
1068
+
1069
+ // Also remove orphaned dependencies recursively for all removed mods
1070
+ const allRemovedMods = [modToRemove, ...dependentMods];
1071
+ const allOrphanedIds = new Set<string>();
1072
+
1073
+ for (const removedMod of allRemovedMods) {
1074
+ const orphaned = findOrphanedDependencies(
1075
+ removedMod,
1076
+ remainingMods,
1077
+ );
1078
+ for (const orphan of orphaned) {
1079
+ allOrphanedIds.add(orphan.id); // Use id (projectId) for comparison
1080
+ }
1081
+ // Update remaining mods after each check to avoid counting already-orphaned mods
1082
+ remainingMods = remainingMods.filter(
1083
+ (m) => !allOrphanedIds.has(m.id),
1084
+ );
1085
+ }
1086
+
1087
+ // Files will be deleted when manifest is saved
1088
+ manifest.value.mods = remainingMods;
1089
+ } else {
1090
+ // Remove only this mod (user chose to keep dependent mods, even if they might break)
1091
+ // Files will be deleted when manifest is saved
1092
+ manifest.value.mods = allMods.filter(
1093
+ (m) => m.id !== idOrSlug && m.projectSlug !== idOrSlug, // Support both id and slug for lookup
1094
+ );
1095
+ }
1096
+ } else {
1097
+ // No one depends on this mod, safe to remove
1098
+ // Also remove orphaned dependencies
1099
+ const remainingMods = allMods.filter(
1100
+ (m) => m.id !== idOrSlug && m.projectSlug !== idOrSlug, // Support both id and slug for lookup
1101
+ );
1102
+ const orphaned = findOrphanedDependencies(modToRemove, remainingMods);
1103
+ const orphanedIds = new Set(orphaned.map((m) => m.id)); // Use id (projectId) for comparison
1104
+
1105
+ // Files will be deleted when manifest is saved
1106
+ manifest.value.mods = remainingMods.filter(
1107
+ (m) => !orphanedIds.has(m.id),
1108
+ );
1109
+ }
1110
+ }
1111
+
1112
+ async function changeInstalledModVersion(mod: Mod) {
1113
+ if (mod.source !== 'modrinth' && mod.source !== 'curseforge') {
1114
+ return;
1115
+ }
1116
+
1117
+ const baseInstallCtx: InstallContext = {
1118
+ projectType: 'mod',
1119
+ minecraftVersion:
1120
+ manifest.value?._meta?.manifest?.minecraftVersion || '',
1121
+ loader: manifest.value?._meta?.manifest?.loader || 'vanilla',
1122
+ };
1123
+ const installCtx = withMarketplaceLoaderIdsForProvider(
1124
+ baseInstallCtx,
1125
+ mod.source,
1126
+ );
1127
+
1128
+ const prov = getModMarketplaceProvider(mod.source);
1129
+ if (!prov?.getInstallVersions) {
1130
+ return;
1131
+ }
1132
+
1133
+ const versions = await prov.getInstallVersions(mod.id, installCtx);
1134
+ if (!versions.length) {
1135
+ return;
1136
+ }
1137
+
1138
+ let selectedVersionId = versions.find((v) => v.label === mod.version)?.id;
1139
+ if (versions.length > 1) {
1140
+ selectedVersionId =
1141
+ (await showVersionSelectionDialog(
1142
+ {
1143
+ provider: mod.source,
1144
+ projectType: 'mod',
1145
+ projectId: mod.id,
1146
+ slug: mod.projectSlug || mod.id,
1147
+ name: mod.name,
1148
+ iconUrl: mod.image,
1149
+ },
1150
+ versions,
1151
+ )) ?? undefined;
1152
+ if (!selectedVersionId) {
1153
+ return;
1154
+ }
1155
+ }
1156
+
1157
+ await install(
1158
+ {
1159
+ provider: mod.source,
1160
+ projectType: 'mod',
1161
+ projectId: mod.id,
1162
+ slug: mod.projectSlug || mod.id,
1163
+ name: mod.name,
1164
+ iconUrl: mod.image,
1165
+ },
1166
+ {
1167
+ skipVersionPrompt: true,
1168
+ selectedVersionId: selectedVersionId ?? versions[0].id,
1169
+ },
1170
+ );
1171
+ }
1172
+
1173
+ function showVersionSelectionDialog(
1174
+ item: MarketplaceItem,
1175
+ versions: MarketplaceInstallVersion[],
1176
+ ): Promise<string | null> {
1177
+ return new Promise((resolve) => {
1178
+ versionSelectionDialog.value = {
1179
+ open: true,
1180
+ item,
1181
+ versions,
1182
+ selectedVersionId:
1183
+ versions.find((v) => v.recommended)?.id ?? versions[0]?.id ?? null,
1184
+ resolveCallback: resolve,
1185
+ };
1186
+ });
1187
+ }
1188
+
1189
+ function closeVersionSelectionDialog() {
1190
+ if (versionSelectionDialog.value.resolveCallback) {
1191
+ versionSelectionDialog.value.resolveCallback(null);
1192
+ }
1193
+ versionSelectionDialog.value = {
1194
+ open: false,
1195
+ item: null,
1196
+ versions: [],
1197
+ selectedVersionId: null,
1198
+ resolveCallback: null,
1199
+ };
1200
+ }
1201
+
1202
+ function confirmVersionSelectionDialog() {
1203
+ if (versionSelectionDialog.value.resolveCallback) {
1204
+ versionSelectionDialog.value.resolveCallback(
1205
+ versionSelectionDialog.value.selectedVersionId,
1206
+ );
1207
+ }
1208
+ versionSelectionDialog.value = {
1209
+ open: false,
1210
+ item: null,
1211
+ versions: [],
1212
+ selectedVersionId: null,
1213
+ resolveCallback: null,
1214
+ };
1215
+ }
1216
+
1217
+ async function loadModDetails(item: MarketplaceItem) {
1218
+ isLoadingModDetails.value = true;
1219
+ modDetailsError.value = null;
1220
+ try {
1221
+ const prov = getModMarketplaceProvider(item.provider);
1222
+ if (!prov) {
1223
+ throw new Error(`Unknown provider: ${item.provider}`);
1224
+ }
1225
+ const details = await prov.getProjectDetails(item.projectId);
1226
+ selectedModDetails.value = details;
1227
+ } catch (e) {
1228
+ modDetailsError.value = e instanceof Error ? e.message : String(e);
1229
+ selectedModDetails.value = null;
1230
+ } finally {
1231
+ isLoadingModDetails.value = false;
1232
+ }
1233
+ }
1234
+
1235
+ async function loadInstalledModDetails(mod: Mod) {
1236
+ isLoadingModDetails.value = true;
1237
+ modDetailsError.value = null;
1238
+ try {
1239
+ let details: ProjectDetails | null = null;
1240
+ const prov = getModMarketplaceProvider(mod.source);
1241
+ if (prov) {
1242
+ details = await prov.getProjectDetails(mod.id);
1243
+ }
1244
+ if (details) {
1245
+ selectedModDetails.value = details;
1246
+ } else {
1247
+ // For direct/local mods, create a basic ProjectDetails object
1248
+ selectedModDetails.value = {
1249
+ provider: mod.source as any,
1250
+ projectId: mod.id,
1251
+ slug: mod.projectSlug || mod.id,
1252
+ name: mod.name,
1253
+ summary: 'Локальный мод',
1254
+ iconUrl: mod.image || '',
1255
+ downloads: 0,
1256
+ };
1257
+ }
1258
+ } catch (e) {
1259
+ modDetailsError.value = e instanceof Error ? e.message : String(e);
1260
+ selectedModDetails.value = null;
1261
+ } finally {
1262
+ isLoadingModDetails.value = false;
1263
+ }
1264
+ }
1265
+
1266
+ function clearModDetails() {
1267
+ selectedModDetails.value = null;
1268
+ modDetailsError.value = null;
1269
+ }
1270
+
1271
+ /**
1272
+ * Check for incompatibilities between a mod and installed mods
1273
+ * Uses slug for matching (as per new dependency format)
1274
+ * @param mod - The mod to check
1275
+ * @param installedModsList - Optional list of installed mods to check against (defaults to all installed mods)
1276
+ */
1277
+ function checkIncompatibilities(
1278
+ mod: Mod,
1279
+ installedModsList?: Mod[],
1280
+ ): { conflictingMod: Mod | null } {
1281
+ const installed = installedModsList ?? installedMods.value;
1282
+
1283
+ // Check if mod has incompatible dependencies
1284
+ const incompatibleDeps =
1285
+ mod.dependencies?.filter((d) => d.type === 'incompatible') ?? [];
1286
+
1287
+ for (const dep of incompatibleDeps) {
1288
+ // Find installed mod that matches this incompatible dependency
1289
+ // Match by slug (primary) or fallback to modId for backwards compatibility
1290
+ const conflicting = installed.find((installedMod) => {
1291
+ return (
1292
+ installedMod.projectSlug === dep.slug ||
1293
+ installedMod.id === dep.modId ||
1294
+ // Also check if installed mod's dependencies mark this mod as incompatible
1295
+ installedMod.dependencies?.some(
1296
+ (installedDep) =>
1297
+ installedDep.type === 'incompatible' &&
1298
+ (installedDep.slug === mod.projectSlug ||
1299
+ installedDep.modId === mod.id),
1300
+ )
1301
+ );
1302
+ });
1303
+
1304
+ if (conflicting) {
1305
+ return { conflictingMod: conflicting };
1306
+ }
1307
+ }
1308
+
1309
+ // Also check if any installed mod marks this mod as incompatible
1310
+ for (const installedMod of installed) {
1311
+ const hasIncompatible = installedMod.dependencies?.some(
1312
+ (dep) =>
1313
+ dep.type === 'incompatible' &&
1314
+ (dep.slug === mod.projectSlug || dep.modId === mod.id),
1315
+ );
1316
+
1317
+ if (hasIncompatible) {
1318
+ return { conflictingMod: installedMod };
1319
+ }
1320
+ }
1321
+
1322
+ return { conflictingMod: null };
1323
+ }
1324
+
1325
+ /**
1326
+ * Resolve all required dependencies for a mod recursively
1327
+ * Uses modId (projectId) for API resolution, slug for checking if already installed
1328
+ */
1329
+ async function resolveDependencies(
1330
+ mod: Mod,
1331
+ installCtx: InstallContext,
1332
+ resolved: Set<string> = new Set(),
1333
+ toInstall: Mod[] = [],
1334
+ ): Promise<Mod[]> {
1335
+ const modKey = `${mod.source}:${mod.id}`;
1336
+ if (resolved.has(modKey)) {
1337
+ return toInstall; // Already resolved
1338
+ }
1339
+ resolved.add(modKey);
1340
+
1341
+ const requiredDeps =
1342
+ mod.dependencies?.filter((d) => d.type === 'required') ?? [];
1343
+
1344
+ for (const dep of requiredDeps) {
1345
+ // Check if dependency is already installed by slug (primary check)
1346
+ const isAlreadyInstalled = installedMods.value.some(
1347
+ (installedMod) =>
1348
+ installedMod.projectSlug === dep.slug ||
1349
+ installedMod.id === dep.modId, // Fallback for backwards compatibility
1350
+ );
1351
+
1352
+ if (isAlreadyInstalled) {
1353
+ continue; // Skip already installed dependencies
1354
+ }
1355
+
1356
+ const depItem = await marketplaceItemForProvider(
1357
+ dep.source,
1358
+ dep.modId,
1359
+ installCtx.projectType,
1360
+ );
1361
+
1362
+ if (!depItem) {
1363
+ console.warn(
1364
+ `[modsEditor] Could not resolve dependency ${dep.modId} (slug: ${dep.slug}) from ${dep.source}`,
1365
+ );
1366
+ continue;
1367
+ }
1368
+
1369
+ const depProv = getModMarketplaceProvider(depItem.provider);
1370
+ if (!depProv) {
1371
+ console.warn(
1372
+ `[modsEditor] No marketplace provider for dependency source ${depItem.provider}`,
1373
+ );
1374
+ continue;
1375
+ }
1376
+
1377
+ const depInstallCtx = withMarketplaceLoaderIdsForProvider(
1378
+ installCtx,
1379
+ depItem.provider,
1380
+ );
1381
+ const depEntry = await depProv.install(depItem, depInstallCtx);
1382
+ const depMod = depEntry as Mod;
1383
+
1384
+ // Recursively resolve dependencies of this dependency
1385
+ await resolveDependencies(depMod, installCtx, resolved, toInstall);
1386
+
1387
+ toInstall.push(depMod);
1388
+ }
1389
+
1390
+ return toInstall;
1391
+ }
1392
+
1393
+ /**
1394
+ * Show incompatibility dialog and wait for user decision
1395
+ */
1396
+ function showIncompatibilityDialog(
1397
+ incompatibleMod: Mod,
1398
+ conflictingMod: Mod,
1399
+ ): Promise<boolean> {
1400
+ return new Promise((resolve) => {
1401
+ incompatibilityDialog.value = {
1402
+ open: true,
1403
+ incompatibleMod,
1404
+ conflictingMod,
1405
+ resolveCallback: resolve,
1406
+ };
1407
+ });
1408
+ }
1409
+
1410
+ function handleIncompatibilityCancel() {
1411
+ if (incompatibilityDialog.value.resolveCallback) {
1412
+ incompatibilityDialog.value.resolveCallback(false);
1413
+ }
1414
+ incompatibilityDialog.value = {
1415
+ open: false,
1416
+ incompatibleMod: null,
1417
+ conflictingMod: null,
1418
+ resolveCallback: null,
1419
+ };
1420
+ }
1421
+
1422
+ function handleIncompatibilityInstallAnyway() {
1423
+ if (incompatibilityDialog.value.resolveCallback) {
1424
+ incompatibilityDialog.value.resolveCallback(true);
1425
+ }
1426
+ incompatibilityDialog.value = {
1427
+ open: false,
1428
+ incompatibleMod: null,
1429
+ conflictingMod: null,
1430
+ resolveCallback: null,
1431
+ };
1432
+ }
1433
+
1434
+ const activeSearchResults = computed(() =>
1435
+ getMarketplaceSearchResults(marketplaceTab.value),
1436
+ );
1437
+
1438
+ function isMarketplaceTabSearching(tabId: string): boolean {
1439
+ syncMarketplaceProviderSearchKeys();
1440
+ if (tabId === 'all') {
1441
+ return listModMarketplaceProviders().some(
1442
+ (p) => isSearchingByProvider[p.id],
1443
+ );
1444
+ }
1445
+ return Boolean(isSearchingByProvider[tabId]);
1446
+ }
1447
+
1448
+ function getMarketplaceSearchResults(tabId: string): MarketplaceItem[] {
1449
+ syncMarketplaceProviderSearchKeys();
1450
+ if (tabId === 'all') {
1451
+ const merged: MarketplaceItem[] = [];
1452
+ for (const p of listModMarketplaceProviders()) {
1453
+ merged.push(...(searchResultsByProvider[p.id] ?? []));
1454
+ }
1455
+ return merged;
1456
+ }
1457
+ return searchResultsByProvider[tabId] ?? [];
1458
+ }
1459
+
1460
+ // Debounced search function (wait 500ms after user stops typing)
1461
+ let debouncedSearch = debounce(() => {
1462
+ void search(true);
1463
+ }, 500);
1464
+
1465
+ // Store watcher stop function
1466
+ let stopWatcher: (() => void) | null = null;
1467
+
1468
+ // Auto-search when query changes (debounced) - will be recreated in init
1469
+ function setupSearchWatcher() {
1470
+ // Stop existing watcher if any
1471
+ if (stopWatcher) {
1472
+ stopWatcher();
1473
+ }
1474
+
1475
+ // Create new watcher
1476
+ stopWatcher = watch(searchQuery, () => {
1477
+ debouncedSearch();
1478
+ });
1479
+ }
1480
+
1481
+ // Initial setup
1482
+ setupSearchWatcher();
1483
+
1484
+ watch(
1485
+ () => [
1486
+ manifest.value?._meta?.manifest?.minecraftVersion,
1487
+ manifest.value?._meta?.manifest?.loader,
1488
+ ] as const,
1489
+ () => {
1490
+ if (!marketplaceBrowseStarted.value) return;
1491
+ void search(true);
1492
+ },
1493
+ );
1494
+
1495
+ /**
1496
+ * Scan for local mods and resource packs
1497
+ */
1498
+ function scanLocalModsAndResourcePacks() {
1499
+ if (!manifest.value?.id) return;
1500
+
1501
+ // Get original manifest from cache (before any edits)
1502
+ const localBuilds = useLocalBuildsModel();
1503
+ const original = localBuilds.getManifestById(manifest.value.id);
1504
+ originalManifest.value = original;
1505
+
1506
+ // Scan local mods
1507
+ localMods.value = scanLocalMods(manifest.value.id, original);
1508
+ }
1509
+
1510
+ /**
1511
+ * Toggle disabled state for a mod
1512
+ */
1513
+ function toggleModDisabledState(mod: Mod, disabled: boolean) {
1514
+ if (!manifest.value?.id) return false;
1515
+
1516
+ // If it's a local mod, toggle the file
1517
+ if (mod.source === 'direct' && mod.id.startsWith('local-')) {
1518
+ const success = toggleModDisabled(manifest.value.id, mod, disabled);
1519
+ if (success) {
1520
+ // Update the mod's disabled state
1521
+ mod.disabled = disabled;
1522
+ // Update file path if needed
1523
+ if (mod.files.length > 0) {
1524
+ const file = mod.files[0];
1525
+ if (disabled) {
1526
+ file.path = file.path.replace(/\.jar$/, '.jar.disabled');
1527
+ } else {
1528
+ file.path = file.path.replace(/\.disabled$/, '');
1529
+ }
1530
+ }
1531
+ }
1532
+ return success;
1533
+ } else {
1534
+ // For manifest mods, update the manifest AND rename the file
1535
+ if (!manifest.value.mods) return false;
1536
+
1537
+ const modIndex = manifest.value.mods.findIndex((m) => m.id === mod.id);
1538
+ if (modIndex >= 0) {
1539
+ // First, rename the file on disk
1540
+ const success = toggleModDisabled(manifest.value.id, mod, disabled);
1541
+ if (!success) return false;
1542
+
1543
+ // Then update the manifest
1544
+ manifest.value.mods[modIndex].disabled = disabled;
1545
+ // Also update file paths
1546
+ for (const file of manifest.value.mods[modIndex].files) {
1547
+ if (disabled) {
1548
+ if (!file.path.endsWith('.disabled')) {
1549
+ file.path = `${file.path}.disabled`;
1550
+ }
1551
+ if (!file.filename.endsWith('.disabled')) {
1552
+ file.filename = `${file.filename}.disabled`;
1553
+ }
1554
+ } else {
1555
+ file.path = file.path.replace(/\.disabled$/, '');
1556
+ file.filename = file.filename.replace(/\.disabled$/, '');
1557
+ }
1558
+ }
1559
+ return true;
1560
+ }
1561
+ }
1562
+ return false;
1563
+ }
1564
+
1565
+ /**
1566
+ * Delete a local mod
1567
+ */
1568
+ function deleteLocalModFile(mod: Mod) {
1569
+ if (!manifest.value?.id) return false;
1570
+
1571
+ if (mod.source === 'direct' && mod.id.startsWith('local-')) {
1572
+ const success = deleteLocalMod(manifest.value.id, mod);
1573
+ if (success) {
1574
+ // Remove from local mods list
1575
+ localMods.value = localMods.value.filter((m) => m.id !== mod.id);
1576
+ }
1577
+ return success;
1578
+ }
1579
+ return false;
1580
+ }
1581
+
1582
+ /**
1583
+ * Инициализация редактора (манифест уже передан при создании store)
1584
+ */
1585
+ const init = async (_manifest: BuildVersionManifest | null) => {
1586
+ console.log('[modsEditor.init] Инициализация:', {
1587
+ hasValue: !!manifest.value,
1588
+ minecraftVersion: manifest.value?._meta?.manifest?.minecraftVersion,
1589
+ loader: manifest.value?._meta?.manifest?.loader,
1590
+ manifest,
1591
+ });
1592
+
1593
+ // Reset search state when reinitializing
1594
+ searchQuery.value = '';
1595
+ searchError.value = null;
1596
+ isSearching.value = false;
1597
+ selectedModDetails.value = null;
1598
+ modDetailsError.value = null;
1599
+ syncMarketplaceProviderSearchKeys();
1600
+ for (const p of listModMarketplaceProviders()) {
1601
+ searchResultsByProvider[p.id] = [];
1602
+ isSearchingByProvider[p.id] = false;
1603
+ }
1604
+
1605
+ const validTabs = new Set<string>([
1606
+ 'all',
1607
+ ...listModMarketplaceProviders().map((p) => p.id),
1608
+ ]);
1609
+ if (!validTabs.has(marketplaceTab.value)) {
1610
+ marketplaceTab.value = 'all';
1611
+ }
1612
+
1613
+ // Recreate watcher to ensure it works after reinitialization
1614
+ setupSearchWatcher();
1615
+
1616
+ // Browse marketplace with empty query
1617
+ void search(true);
1618
+
1619
+ // Scan local mods after initialization
1620
+ scanLocalModsAndResourcePacks();
1621
+ };
1622
+
1623
+ return {
1624
+ // Mods editor API
1625
+ projectType,
1626
+ marketplaceTab,
1627
+ searchQuery,
1628
+ isSearching,
1629
+ searchError,
1630
+ activeSearchResults,
1631
+ isMarketplaceTabSearching,
1632
+ getMarketplaceSearchResults,
1633
+ installedMods,
1634
+ installedResourcePacks,
1635
+ allMods,
1636
+ allResourcePacks,
1637
+ localMods,
1638
+ localResourcePacks,
1639
+ isInstalledBySlug,
1640
+ selectedModDetails,
1641
+ isLoadingModDetails,
1642
+ modDetailsError,
1643
+ search,
1644
+ loadMoreMarketplaceResults,
1645
+ canLoadMoreMarketplace,
1646
+ isLoadingMoreMarketplace,
1647
+ marketplaceBrowseStarted,
1648
+ install,
1649
+ removeInstalled,
1650
+ loadModDetails,
1651
+ loadInstalledModDetails,
1652
+ clearModDetails,
1653
+ incompatibilityDialog,
1654
+ handleIncompatibilityCancel,
1655
+ handleIncompatibilityInstallAnyway,
1656
+ dependencyRemovalDialog,
1657
+ handleDependencyRemovalCancel,
1658
+ handleDependencyRemovalOnly,
1659
+ handleDependencyRemovalAll,
1660
+ modsDependencyStatus,
1661
+ dependencyIssuesDialog,
1662
+ openDependencyIssuesDialog,
1663
+ closeDependencyIssuesDialog,
1664
+ resolveIssue,
1665
+ resolveAllIssues,
1666
+ collectAllDependencyIssues,
1667
+ scanLocalModsAndResourcePacks,
1668
+ toggleModDisabledState,
1669
+ deleteLocalModFile,
1670
+ versionSelectionDialog,
1671
+ closeVersionSelectionDialog,
1672
+ confirmVersionSelectionDialog,
1673
+ changeInstalledModVersion,
1674
+ init,
1675
+ };
1676
+ },
1677
+ false,
1678
+ );