Compare commits

...
57 Commits
Author SHA1 Message Date
aecsocket 9a717962fb raw/net estimated revenue fields 2026-08-15 07:10:25 +00:00
aecsocket 30df2ee4dd fix aditude month query 2026-08-14 12:52:23 +00:00
aecsocket a0c2537505 adjust payout report to show actual days 2026-08-14 12:08:38 +00:00
aecsocket ba1b7ce3c1 Merge branch 'main' into boris/dev-1126 2026-08-14 11:13:22 +00:00
aecsocket 5605ee578a wip: payout run reports 2026-08-14 11:09:35 +00:00
Prospector 17f13097f2 adjust design -> functionality language in blog post 2026-08-13 11:28:53 -07:00
Prospector a9de420d57 update date + changelog 2026-08-13 11:09:05 -07:00
Prospector ab7359a4c5 minor fixes to disclosures, basic markdown support (#7133) 2026-08-13 10:58:47 -07:00
Truman GaoandCalum H. 97b5138672 fix: modpack export (#7119)
* fix: stale function call set_webview_visible

* fix: modpack export progress bar and add notification when done

* remove: force_no_zip64

* feat: switch to zip crate

* add typo exception

* fix: remove unit test (dont do)

---------

Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-08-13 16:37:39 +00:00
Calum H. 579109a478 refactor: app event bus (#6985)
* refactor: app event bus

* fix: use messagepack + cleanup

* fix: use postcard

* fix: lint

* fix: import gate

* fix: prettierignore + lint

* fix: lint

* fix: rev
2026-08-13 16:33:06 +00:00
Prospector 5f7493491f changelog 2026-08-13 09:01:59 -07:00
755825b09a project disclosures frontend (#6955)
* begin project disclosures

* new project settings header

* begin disclosure settings, edit content rules

* togglecards

* update phrasing of the rules, prepr

* more disclosure settings structuring

* add functionality toggle

* implement functionality with staging api

* improve project settings head titles

* feat(labrinth): project disclosures model

* feat(labrinth): project disclosures database model

* feat(labrinth): project disclosures get endpoint

* feat(labrinth): censor user ids if set by moderator

* feat(labrinth): wrap disclosures in struct

* feat(labrinth): edit project disclosures endpoint

* style(labrinth): cargo fmt

* style(labrinth): fix typo

* feat(labrinth): add fields for ai content disclosure

* fix(labrinth): field typo

* chore(labrinth): update query cache

* feat(labrinth): index disclosures in search

* refactor(labrinth): use enum instead of bools

* feat(labrinth): trigger incremental index on disclosure change

* feat(labrinth): change archived status to disclosure

* feat(labrinth): use disclosure for archival status

* fix(labrinth): error type for deserialization

* fix(labrinth): migration timestamp

* fix(labrinth): add disclosures to elasticsearch schema

* disclosures in search

* update photosensitivity warning copy

* update archiving, move general project settings to v3 (start redesign there), use project id lookups for stable cache keys, advanced in server search

* blue archive banner

* display AI use types

* improve labels

* add modrinth to link

* add placeholder missing disclosure report type

* add AI metadata checking to block image uploads with notice

* update copy

* update section 4 of rules

* Update rule 6 layout

* fix(labrinth): don't remove archival disclosure when changing status via v3 api

* feat(labrinth): derive str for ai usages and telemtry consent

* feat(labrinth): include ai usages and telemetry consent in disclosure types

* Update moderation checklist (#7056)

* fix: missing delphi severity (#7054)

* Utils nav, new disclosures stage, ai button in rules

* add prefix to collect

* Finish up new disclosures stage

* update r4 messages

* new r4 msg, update showcase clarity message

* fix: version upload failing to detect mrpack loader (#7063)

* fix: mrpack exporting with zip64 (#7064)

* fix: action bar max width (#7048)

* fix: action bar max width

* fix: width

* changelog

* Rule placeholders with subsections & anchors, update messages.

* update new messages to account for new rule and placeholder layout

* Add nag for content disclosures

---------

Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech>
Co-authored-by: chyzman <chyzalt@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>

* prepr

* refactor(labrinth): move disclosure parsing to document creation

* fix(labrinth): prevent removing moderator added archival disclosures

* fix(labrinth): dedupe ai usages

* fix(labrinth): auth logic on archived disclosure removal

* feat(labrinth): add interactions field

* style(labrinth): cargo fmt

* feat(labrinth): granular disclosure locking

* add 5.8, move 5.9, update messages and placeholders accordingly.

* move disclosures stage earlier

* include consent model info in telemetry disclosure msgs

* prepr + update date

* typo

* qa pass

* search suboptions

* add empty state

* checkboxes in column

* persistent advanced filters

* blog draft

* prepr

* support soft deletion

* reload instead of optimistically updating

* update blog

* TOS update + minor verbiage adjustment

* 45 day grace

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
Co-authored-by: coolbot <76798835+coolbot100s@users.noreply.github.com>
Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech>
Co-authored-by: chyzman <chyzalt@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-08-13 09:01:18 -07:00
Calum H. 06b7c44dcc refactor: content tab styling (#7091)
* refactor: managed content card

* fix: update filter pill styling to be chips-like

* fix: modpack exporting

* fixed: vanilla server projs + header

* refactor: filters

* refactor: detect external file icons + other metadata inside

* refactor: support json5 metadata

* fix: qa

* fix: qa

* fix: fmt + prepr

* fix: fmt

* fix: qa

* fix: headers

* feat: locking

* fix: clippy

* fix: qa

* fix: frozen loc

* fix: shared instances diff calc break

* fix: env filter + remove categories
2026-08-13 14:55:35 +00:00
Sychic e087e571f3 fix(labrinth): disclosure modification attribution (#7121) 2026-08-13 14:51:22 +00:00
Calum H. fbdde8f3d8 feat: update required empty state for modrinth hosting (#7125) 2026-08-13 14:46:10 +00:00
Sychic 290bd2b934 chore: more idea sources (#7122) 2026-08-13 02:41:31 +02:00
Sychic 9ecbe8eaa6 feat(labrinth): granular disclosure locking (#7108)
* feat(labrinth): granular disclosure locking

* chore(labrinth): update query cache
2026-08-12 17:26:15 -07:00
Sychic 549b333eb2 feat(labrinth): project disclosures (#6945)
* feat(labrinth): project disclosures model

* feat(labrinth): project disclosures database model

* feat(labrinth): project disclosures get endpoint

* feat(labrinth): censor user ids if set by moderator

* feat(labrinth): wrap disclosures in struct

* feat(labrinth): edit project disclosures endpoint

* style(labrinth): cargo fmt

* style(labrinth): fix typo

* feat(labrinth): add fields for ai content disclosure

* fix(labrinth): field typo

* chore(labrinth): update query cache

* feat(labrinth): index disclosures in search

* refactor(labrinth): use enum instead of bools

* feat(labrinth): trigger incremental index on disclosure change

* feat(labrinth): change archived status to disclosure

* feat(labrinth): use disclosure for archival status

* fix(labrinth): error type for deserialization

* fix(labrinth): migration timestamp

* fix(labrinth): add disclosures to elasticsearch schema

* fix(labrinth): don't remove archival disclosure when changing status via v3 api

* feat(labrinth): derive str for ai usages and telemtry consent

* feat(labrinth): include ai usages and telemetry consent in disclosure types

* refactor(labrinth): move disclosure parsing to document creation

* fix(labrinth): prevent removing moderator added archival disclosures

* fix(labrinth): dedupe ai usages

* fix(labrinth): auth logic on archived disclosure removal

* feat(labrinth): add interactions field

* style(labrinth): cargo fmt

* feat(labrinth): soft delete disclosures

* feat(labrinth): return soft-deleted disclosures

* fix(labrinth): update disclosure on removal

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-08-12 17:26:14 -07:00
Prospector 4b82cca3bf fix sidebar not showing on user pages (#7097)
* fix sidebar not showing on user pages

* prepr
2026-08-12 17:25:26 -07:00
Prospector 734fd9cdb3 fix app build on windows & mac (#7118) 2026-08-12 17:25:15 -07:00
Prospector 2fd4495104 rejected projects go to ghost on delete (#7107)
* draft: rejected projects go to ghost on delete

* don't free slug

* fix sqlx

* free slugs but different

* add auto reject for processing items given to ghost, as well as send thread messages with a little info on who it came from
2026-08-12 23:54:57 +00:00
François-Xavier Talbot 552bc3f739 fix(labrinth): webhook scopes (#7114)
fix(labrinth): scope paypal & tremendous webhooks in payout scope
2026-08-12 18:34:33 +02:00
Truman Gao 4758cabbf9 feat: add hide and show download items (#7106)
* feat: add hide and show download items

* pnpm prepr
2026-08-12 16:18:02 +00:00
aecsocket 6ec979a076 fix: hide ad webview when showing fullscreen content (#7110)
* fix: hide ad webview when showing fullscreen content

* fixes
2026-08-12 16:02:02 +00:00
aecsocket c34bd26f87 fix: search indexing and schema changes (#7090)
* fix: incremental indexing uses latest db data

* dependent search fields
2026-08-12 11:54:35 -04:00
aecsocket 2a43792fd9 fix: app pinging servers over IPv6 (#7100)
fix: app pinging servers opver IPv6
2026-08-11 12:58:59 -04:00
Calum H. 2d567b93ce feat: imgur proxying for blocked countries (#7104) 2026-08-11 16:40:03 +00:00
Prospector 26e05ee9e5 update API docs to add environment fields (#7096)
* update API docs to add environment fields

* fix unknown on create/patch routes
2026-08-10 14:30:00 -07:00
jade ca8c5a717f fix: unique preview toggle ID in MarkdownEditor (#7093)
fix: preview toggle in MD editor targets other MD editors
2026-08-10 18:47:33 +00:00
Prospector 759f67a551 changelog 2026-08-10 11:17:45 -07:00
Prospector 66628fd69d add reddit and youtube as socials (#7092) 2026-08-10 11:17:13 -07:00
Prospector 1ffcd67562 changelog 2026-08-10 10:36:01 -07:00
Calum H. fda5c62bc6 fix: ignore extensions for mrpack warning modal (#6948) 2026-08-10 17:25:52 +00:00
aecsocket f8f05ce91b fix: expose raw icon URL in project response (#7086)
* fix: expose raw icon URL in project response

* remove unnecessary test
2026-08-10 15:48:23 +00:00
Calum H.andProspector 8b2438aebb fix: Gifs in the project description are not working properly (#7076)
* fix: Gifs in the project description are not working properly
Fixes #3786

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-08-10 15:43:13 +00:00
aecsocket 07bac8ffde fix: v3 environment fields in all v2 endpoints (#7082) 2026-08-10 15:42:56 +00:00
Calum H. d0856d46f2 fix: copy on unknown file warning modal (#7078)
Closes: #6865
2026-08-10 15:26:23 +00:00
Calum H. e8aee15664 chore: cleanup standards (#6970)
* chore: cleanup standards & skills

* remove: figma mcp doc, not needed anymore
2026-08-10 15:15:44 +00:00
Calum H. 98cb6b6b44 fix: buttons qa (#7067)
* fix: broken minecraft required modal button

* fix: app close icon issue

* fix: versions tooltip missing

* fix: instance not found bug on btn press
2026-08-10 15:15:27 +00:00
coolbot f27387462e Moderation: Rule placeholders with subsections & anchors, update messages. (#7068)
Rule placeholders with subsections & anchors, update messages.
2026-08-10 14:31:16 +00:00
coolbot ef90f08813 Moderation: Metadata Message (#7066)
* game versions msg

* sneak in some tec rev quick reply updates

* add loaders msg to metadata stage

* fix loaders msg
2026-08-10 14:31:03 +00:00
Calum H. 9fd45ef2ce fix: always check for updates/clear cache 10 min cooldown (#7077) 2026-08-10 14:00:42 +00:00
Michael H. 92971a3e0d build: don't auto update staging 2026-08-10 14:57:05 +02:00
Modrinth Bot 357224f22a New translations from Crowdin (main) (#7071) 2026-08-10 11:43:08 +00:00
Seungyeop Lee e2612d1a33 fix sidebar closing when collapsing friends list sections (#7060) (#7062) 2026-08-10 11:32:18 +00:00
coolbot 318840e403 Fix: Version nag navigation (#7070)
* fix: version nag navigation

* prepr
2026-08-10 10:54:29 +00:00
Prospector 1a56233c96 changelog 2026-08-08 12:10:41 -07:00
Truman Gao ff8c4f16a9 fix: action bar max width (#7048)
* fix: action bar max width

* fix: width
2026-08-08 19:06:42 +00:00
Truman Gao 757ec9ab53 fix: mrpack exporting with zip64 (#7064) 2026-08-08 17:34:54 +00:00
Truman Gao f38d351d32 fix: version upload failing to detect mrpack loader (#7063) 2026-08-08 17:18:58 +00:00
ThatGravyBoat b78dd9bf1b fix: missing delphi severity (#7054) 2026-08-07 18:16:13 -07:00
chyz 5072c1d298 Moderation Changes Phase 2.-1 (#6833)
* better default state handling

* make next stage button work for going to next project

* fix skill issue

* actually fix initial values this time,
make single line inputs with varying lengths (text input and dropdown) use their widest possible length instead of whatever the hell they used to be

* loaders in progress

* loaders and versions test (probably dont use them as the messages suck and idrk what they should do quick fix/message wise)

* WIP the wip

* good enough for now slug taken improvements

* trim all those extra newlines

* viewedness

* batch prefetch queue + technically slightly faster checklist init

* show all valid alternative project types regardless of current project type

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* gimme them versions

* better moderation permission stage shown check?

* betterer moderation permission stage shown check?

* bettererer moderation permission stage shown check? + also began deleting legacy checklist types

* fix certain stages being able to be considered viewed before being viewed

* more removals + i forgot to remove an import last commit

* changed nothing™️

* remove severities + bandaid post-approval message priorities + dependencies

* fix message preview tooltips

* next stage = next project when done

* insufficient description custom fix

* fix that thing coolbot told me was broken I forgot what it was oops

* oh there was a second bug there

* make alternate versions and incorrect project type required cuz they are

* re-navigate button also damn that's a lot of other stuff that pnpm fix... fixed?

* you cant just quick fix a slug into a taken one

* maybe lets not just try and set a slug to something entirely invalid?

* what if we just didn't drop ur queue :smart:

* more queue upgrades

* tweaks

* I love unbreaking things

* un-nuked renavigate button

* prepr

* prepr worked this time i think

* undo more mistakes

* i love intellij refactoring

* ok i think we're good
2026-08-07 23:40:11 +00:00
aecsocket 8e8640bfb8 add net-60 in-review payout rows 2026-08-04 15:02:24 +01:00
aecsocket 1c3fa44049 fix compile 2026-08-04 14:50:48 +01:00
aecsocket 7cd3e835ab Merge branch 'main' into boris/dev-1126 2026-08-04 14:21:03 +01:00
aecsocket 5c4c30e514 wip 2026-08-04 14:17:50 +01:00
aecsocket 6549d047dc wip: payouts runs migration 2026-07-20 12:50:19 +01:00
672 changed files with 24936 additions and 11392 deletions
+23
View File
@@ -0,0 +1,23 @@
---
name: api-module
description: Add an API endpoint module to packages/api-client from an OpenAPI schema. Use for new backend endpoints, API client modules, or tasks that provide an OpenAPI schema.
---
# Add an API Module
Read the applicable `AGENTS.md` files before you edit code.
Read [the API module standard](../../../standards/frontend/ADDING_API_MODULES.md) in full.
1. Identify the OpenAPI schema from the request. If more than one schema is possible, ask the user to select one.
2. Read the schema. Identify each endpoint, HTTP method, request type, response type, and path parameter.
3. Get the service and version from the URL prefix. For example, map `/v3/projects` to `labrinth/v3/`.
4. Define the API types in `types.ts`. Make each type match the schema exactly.
5. Do not change, rename, or remove API fields.
6. Make a module class that extends `AbstractModule`. Implement each endpoint with `this.client.request()` or `this.client.upload()`.
7. Use the request-option pattern from the standard. Do not call `$fetch`, `fetch`, or another HTTP client directly.
8. Add the module to `MODULE_REGISTRY` so the client can instantiate it.
9. Export new service types from the applicable barrel `index.ts`.
10. Check the module paths, registry key, public type exports, and endpoint types.
Run only the checks that the user or the applicable `AGENTS.md` permits.
@@ -0,0 +1,4 @@
interface:
display_name: "Add API Module"
short_description: "Add typed API client modules from OpenAPI"
default_prompt: "Use $api-module to add an API client module from this OpenAPI schema."
@@ -0,0 +1,38 @@
---
name: cross-platform-pages
description: Convert a page to the shared Modrinth page system for the website and desktop app. Use for shared layouts, wrapped layouts, or platform dependency-injection contracts.
---
# Convert a Cross-Platform Page
Read the applicable `AGENTS.md` files before you edit code.
Read these standards in full:
- [Cross-platform pages](../../../standards/frontend/CROSS_PLATFORM_PAGES.md)
- [Dependency injection](../../../standards/frontend/DEPENDENCY_INJECTION.md)
1. Identify the target page from the request.
2. Read the page and its route shell. Identify data sources, mutations, navigation, and platform APIs.
3. Use a wrapped layout when both platforms use the same API source and page logic.
4. Use a shared layout when platform data or operations have different implementations.
For a shared layout:
1. Define a provider contract for all platform operations.
2. Put common UI and state logic in the shared layout.
3. Put reusable search, filter, and selection logic in local composables.
4. Implement the contract in `apps/frontend/` and `apps/app-frontend/`.
5. Use optional contract fields only for capabilities that are not available on both platforms.
For a wrapped layout:
1. Move the page to `packages/ui/src/layouts/wrapped/` and preserve its route structure.
2. Replace platform-only imports with common utilities or provider calls.
3. Make each frontend route shell render the wrapped component.
4. Match primary query options in both route shells when the layout uses `ReadyTransition` and `useReadyState`.
5. Prefetch these queries with `ensureQueryData`, as the standard specifies.
Check that both route shells resolve their imports. Check that all required provider fields have implementations.
Run only the checks that the user or the applicable `AGENTS.md` permits.
@@ -0,0 +1,4 @@
interface:
display_name: "Convert Cross-Platform Page"
short_description: "Share pages across the web and desktop app"
default_prompt: "Use $cross-platform-pages to convert this page for the website and desktop app."
+21
View File
@@ -0,0 +1,21 @@
---
name: figma-mcp
description: Convert a Figma design into a Modrinth Vue page or component. Use when a request provides a Figma URL or asks to implement a Figma layout.
---
# Implement a Figma Design
Read the applicable `AGENTS.md` files before you edit code.
Read `packages/ui/AGENTS.md` in full.
1. Load the available Figma design-to-code instructions and follow the MCP tool guidance.
2. Call `get_design_context` first with `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`.
3. Treat the result as reference code and adapt it to the Modrinth codebase.
4. Map Figma color variables to the applicable `surface-*` and `text-*` tokens. Do not use aliased Figma names directly.
5. Reuse applicable components from `packages/ui/src/components/` before creating new ones. Also refer to `standards/frontend/COMPONENT_STRUCTURE.md`
6. Read `packages/assets/styles/variables.scss` when Figma does not supply a required token.
7. Use exact spacing values from the design.
8. Implement the result as a Vue SFC with Tailwind classes and the existing component library.
Run only the checks that the user or the applicable `AGENTS.md` permits.
@@ -0,0 +1,4 @@
interface:
display_name: "Implement Figma Design"
short_description: "Build Modrinth Vue UI from Figma designs"
default_prompt: "Use $figma-mcp to implement this Figma design as a Modrinth Vue component."
+32
View File
@@ -0,0 +1,32 @@
---
name: i18n-pass
description: Convert hard-coded English text in changed Vue components to the @modrinth/ui localization system. Use for an i18n pass, untranslated-string review, pull request, or component migration.
---
# Do an Internationalization Pass
Read the applicable `AGENTS.md` files before you edit code.
Read [the internationalization standard](../../../standards/frontend/INTERNATIONALIZATION.md) in full.
1. Identify the scope from the request.
2. For a pull request, use `gh pr diff <number>` to identify changed files.
3. For a file path, inspect that file.
4. When the request gives no scope, inspect the current uncommitted diff.
5. Limit the pass to changed `.vue` files.
6. Find user-visible text in templates and scripts.
Check inner text, `alt`, `placeholder`, `aria-label`, buttons, tooltips, notifications, dropdown labels, and error messages.
Do not change dynamic expressions, HTML tag names, CSS classes, internal identifiers, or log messages.
1. Define stable message IDs with `defineMessage` or `defineMessages`.
2. Replace simple text with `formatMessage()` calls.
3. Use `<IntlFormatted>` for text that contains links or markup.
4. Use ICU selections and plurals when grammar depends on a value.
5. Add a space before `}}` when an ICU placeholder ends at the Vue delimiter.
6. Do not change component logic, layout, or reactivity.
7. Do not edit localization JSON files. The user maintains those files.
8. Check the changed templates again for hard-coded English text.
Run only the checks that the user or the applicable `AGENTS.md` permits.
@@ -0,0 +1,4 @@
interface:
display_name: "Run Internationalization Pass"
short_description: "Localize user-visible text in Vue files"
default_prompt: "Use $i18n-pass to localize the user-visible text in these changed Vue files."
+40
View File
@@ -0,0 +1,40 @@
---
name: review-changelog
description: Review the latest packages/blog/changelog.ts entry against the Modrinth changelog standard. Use before a pull request or when asked to review or lint a changelog entry.
---
# Review a Changelog Entry
Read [the changelog standard](../../../standards/maintaining/CHANGELOG.md) in full before the review.
1. Open `packages/blog/changelog.ts`.
2. Find the first entry in the `VERSIONS` array.
3. If the request names `web`, `hosting`, or `app`, review the latest entry for that product.
4. Otherwise, review the latest entry and all adjacent entries with the same date.
Check the entry structure:
- `date` contains a valid ISO 8601 timestamp.
- `product` is `web`, `hosting`, or `app`.
- An `app` entry has a `version` value.
- A `web` or `hosting` entry does not have a `version` value.
- Standard headings are `## Added`, `## Changed`, `## Fixed`, and `## Security`.
- A featured release can use a linked heading.
- Flag the legacy `## Improvements` heading.
Check each bullet:
- The voice and tense agree with the section.
- The first verb agrees with the section.
- The bullet describes user-visible behavior, not implementation.
- The bullet identifies the applicable page, tab, modal, or feature.
- The bullet contains one sentence, uses sentence case, and ends with a period.
- Product and UI names use the public labels.
- The bullet does not contain filler, vague intensifiers, apologies, or internal references.
- The bullet is not a duplicate detail of a larger listed change.
Group findings by entry. For each finding, show the original bullet and a proposed replacement.
If the entry has no findings, state this result. Do not edit the changelog unless the user asks you to apply fixes.
When the user asks for fixes, preserve tab indentation and template-literal formatting.
@@ -0,0 +1,4 @@
interface:
display_name: "Review Changelog"
short_description: "Review changelog entries for style problems"
default_prompt: "Use $review-changelog to review the latest changelog entry."
+39
View File
@@ -0,0 +1,39 @@
---
name: tanstack-query
description: Convert Vue server-state code to TanStack Query. Use for useQuery, useMutation, cache invalidation, optimistic updates, or replacement of useAsyncData and manual ref patterns.
---
# Convert Data Code to TanStack Query
Read the applicable `AGENTS.md` files before you edit code.
Read [the TanStack Query standard](../../../standards/frontend/FETCHING_DATA.md) in full.
1. Identify the target file from the request.
2. Find `useAsyncData`, `useFetch`, manual API refs, and fetch calls in `onMounted`.
3. Identify mutations that use manual loading, error, or result refs.
For queries:
1. Replace manual fetch logic with `useQuery`.
2. Get `api-client` with `injectModrinthClient()`.
3. Use a hierarchical query key with the resource, qualifier, and parameters.
4. Use a computed query key for reactive parameters.
5. Use a computed `enabled` option when the query depends on other data.
6. Use a shared query-option factory when multiple components use the query.
For mutations:
1. Replace manual mutation state with `useMutation`.
2. Invalidate or update related query data after success.
3. Use an optimistic update only when the UI needs an immediate response.
4. Cancel the applicable query and save its prior data before an optimistic update.
5. Restore the prior data after an error. Invalidate the query after settlement.
Remove manual loading and error refs that TanStack Query replaces. Remove obsolete `onMounted` fetch calls.
Keep Nuxt SSR behavior. Match route-shell prefetch options when `ReadyTransition` and `useReadyState` depend on the query.
Check query keys, invalidation prefixes, reactive values, and rollback data.
Run only the checks that the user or the applicable `AGENTS.md` permits.
@@ -0,0 +1,4 @@
interface:
display_name: "Migrate to TanStack Query"
short_description: "Migrate Vue server state to TanStack Query"
default_prompt: "Use $tanstack-query to migrate this Vue component to TanStack Query."
-18
View File
@@ -1,18 +0,0 @@
---
name: api-module
description: Add a new API endpoint module to packages/api-client from an OpenAPI schema. Use when adding new backend endpoints, creating API client modules, or when an openapi.yml is provided.
argument-hint: <path-to-openapi.yml>
---
Refer to the standard: @standards/frontend/ADDING_API_MODULES.md
## Steps
1. **Read the OpenAPI schema** at `$ARGUMENTS` — identify the endpoints, request/response shapes, and path parameters.
2. **Read the standard above** for naming conventions, type rules, and the module registration pattern.
3. **Determine the service and version** — the URL path prefix tells you which service directory and version namespace to use (e.g. `/v3/projects``labrinth/v3/`).
4. **Define types in `types.ts`** — types must match the API response 1:1. Use the OpenAPI schema as the source of truth. Do not reshape or rename fields.
5. **Create the module class** — extend `BaseModule`, implement each endpoint as a method. Use the correct HTTP verb and request options pattern from the standard.
6. **Register in `MODULE_REGISTRY`** — add the module entry so it's auto-instantiated on the client.
7. **Export types** from the service's barrel `index.ts`.
8. **Verify** — check that the module compiles and the types are accessible from `@modrinth/api-client`.
@@ -1,26 +0,0 @@
---
name: cross-platform-pages
description: Convert a page to the cross-platform page system so it works in both the website and the desktop app. Use when moving a page into packages/ui/src/layouts/, creating shared or wrapped layouts, or setting up DI contracts for platform abstraction.
argument-hint: <path-to-page>
---
Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standards/frontend/DEPENDENCY_INJECTION.md
## Steps
1. **Read the target page** at `$ARGUMENTS` and understand its data sources, mutations, and navigation.
2. **Read the standards above** to understand the shared vs wrapped distinction and the DI pattern.
3. **Decide the category:**
- **Wrapped** (`layouts/wrapped/`) — if the page uses the same API source on both platforms (e.g. web requests, not Tauri plugins). Just move the page component into `packages/ui` and import it from both frontends.
- **Shared** (`layouts/shared/`) — if the page has different data-fetching logic per platform (e.g. website uses `api-client`, app uses Tauri `invoke`). Requires a DI contract.
4. **For shared layouts:**
- Define a DI contract interface in `providers/` capturing all platform-specific operations.
- Create the layout component that injects the context and handles all UI logic.
- Extract reusable stateful logic (search, filtering, selection) into `composables/`.
- Implement the contract separately in each frontend (`apps/frontend/`, `apps/app-frontend/`).
5. **For wrapped pages:**
- Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure.
- Replace any platform-specific imports with shared utilities.
- Import and render the wrapped page from both frontends as a simple component.
- If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`.
6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied.
-22
View File
@@ -1,22 +0,0 @@
---
name: figma-mcp
description: Use the Figma MCP server to translate a Figma design into a Vue page or component layout. Use when the user provides a Figma URL, asks to implement a design, or wants to draft a page layout from Figma.
argument-hint: <figma-url>
---
Refer to the standard: @standards/frontend/FIGMA_MCP_USAGE.md
Also read @packages/ui/CLAUDE.md for color token mapping and component conventions.
## Steps
1. **Parse the Figma URL** from `$ARGUMENTS` — extract the `fileKey` and `nodeId`. Convert `-` to `:` in the node ID.
2. **Read the standards above** for the available tools, adaptation rules, and color usage.
3. **Call `get_design_context`** with the extracted `nodeId` and `fileKey`, using `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`. This is always the first tool to call.
5. **Adapt the output to the Modrinth codebase:**
- Map Figma color variables to `surface-*` / `text-*` tokens — never use Figma's aliased names directly.
- Check `packages/ui/src/components/` for existing components that match elements in the design (buttons, cards, modals, inputs, etc.).
- Check `packages/assets/styles/variables.scss` for tokens not exposed in Figma.
- Match spacing values exactly from the design.
6. **Use `get_screenshot`** if you need a closer visual reference of specific nodes.
7. **Use `get_variable_defs`** to verify which design tokens are applied to ambiguous elements.
8. **Build the component** as a Vue SFC using Tailwind classes and the project's existing component library.
-24
View File
@@ -1,24 +0,0 @@
---
name: i18n-pass
description: Perform an i18n localization pass on changed files or a pull request, converting hard-coded English strings to the @modrinth/ui i18n system. Use when internationalizing a set of changes, reviewing a PR for untranslated strings, or converting a specific component.
argument-hint: [file-path-or-pr-number]
---
Refer to the standard: @standards/frontend/INTERNATIONALIZATION.md
## Steps
1. **Identify the scope of changes:**
- If `$ARGUMENTS` is a PR number, run `gh pr diff $ARGUMENTS` to get the changed files.
- If `$ARGUMENTS` is a file path, use that directly.
- If no argument, check `git diff` for uncommitted changes.
2. **Read the standard above** for the message definition pattern, ICU format rules, and `IntlFormatted` usage.
3. **Filter to Vue SFCs** — only `.vue` files need i18n passes. Skip non-component files.
4. **For each file, scan for hard-coded strings:**
- `<template>`: inner text, `alt`, `placeholder`, `aria-label`, button labels, tooltip text.
- `<script>`: string literals passed to user-visible UI (notification messages, dropdown labels, error messages).
- Skip: dynamic expressions, HTML tag names, CSS classes, internal identifiers, log messages.
5. **Define messages** with `defineMessages` — use descriptive, stable `id`s based on the component's domain (e.g. `project.settings.title`).
6. **Replace strings in templates** with `formatMessage()` calls, or `<IntlFormatted>` for strings containing links or markup.
7. **Handle ICU edge cases** — add a space before `}}` if an ICU placeholder ends at a Vue template delimiter boundary.
8. **Verify** no hard-coded English strings remain in the changed templates. Do not alter logic, layout, or reactivity.
-36
View File
@@ -1,36 +0,0 @@
---
name: review-changelog
description: Review the latest changelog entry in packages/blog/changelog.ts against the project's changelog style guide and flag bullets that need rewriting. Use when checking a freshly added changelog entry before opening a PR, or when the user asks to review/lint the latest changelog.
argument-hint: [product?]
---
Refer to the standard: @standards/maintaining/CHANGELOG.md
## Steps
1. **Locate the latest entry:**
- Open `packages/blog/changelog.ts`.
- The latest entries are at the top of the `VERSIONS` array.
- If `$ARGUMENTS` specifies a product (`web`, `hosting`, `app`), review the most recent entry for that product. Otherwise, review the most recent entry overall, plus any sibling entries sharing the same `date` (coordinated releases ship together).
2. **Read the standard above** in full before reviewing. The bullet rules, section/verb agreement, and "Don't" list are the source of truth.
3. **Check the entry shell:**
- `date` is a valid ISO 8601 timestamp.
- `product` is one of `web`, `hosting`, `app`.
- `version` is present for `app` entries and omitted for `web`/`hosting`.
- Section headings use `## Added`, `## Changed`, `## Fixed`, `## Security` (or a featured-release linked heading). Flag legacy `## Improvements`.
4. **Review each bullet** against the standard. For each bullet, check:
- Voice/tense matches the section heading.
- Opening verb agrees with its section.
- Describes observable behavior, not implementation.
- Specific enough to identify the surface (names the tab/page/modal).
- One sentence, ends with a period, sentence case.
- Uses branded names (Modrinth App, Modrinth Hosting) correctly.
- No filler ("issue with", "issue where", "various", "some"), no vague intensifiers, no apologies, no PR/commit references (unless crediting a third-party contributor with a linked GitHub profile).
- Not a duplicate sub-fix of a bigger change already listed.
5. **Report findings** as a short list grouped by entry. For each problem bullet, show the original line and a suggested rewrite. If the entry is clean, say so explicitly. Do not edit the file unless the user asks - this skill is a review pass, not a rewrite pass.
6. **If the user then asks to apply fixes**, edit `packages/blog/changelog.ts` directly using the suggested rewrites. Preserve tab indentation and template literal formatting.
-27
View File
@@ -1,27 +0,0 @@
---
name: tanstack-query
description: Convert a page or component from useAsyncData/manual ref patterns to TanStack Query for server state management. Use when migrating data fetching to useQuery/useMutation, adding cache invalidation, or replacing useAsyncData with TanStack Query.
argument-hint: <path-to-file>
---
Refer to the standard: @standards/frontend/FETCHING_DATA.md
## Steps
1. **Read the target file** at `$ARGUMENTS` and identify all data-fetching patterns: `useAsyncData`, `useFetch`, manual `ref()` + `await`, or `onMounted` fetch calls.
2. **Read the standard above** for the query/mutation patterns, query key conventions, and optimistic update approach.
3. **Convert queries:**
- Replace `useAsyncData` / `useFetch` / manual fetches with `useQuery`.
- Use the `api-client` via `injectModrinthClient()` for the `queryFn`.
- Design query keys with the `['resource', 'version', ...params]` convention.
- Use `computed` query keys for reactive parameters.
- Use the `enabled` option for conditional queries that depend on other data.
4. **Convert mutations:**
- Replace manual `try/catch` + `ref` patterns with `useMutation`.
- Add `onSuccess` handlers that invalidate or update related query caches.
- Consider optimistic updates for UI-critical mutations (follow the pattern in the standard).
5. **Clean up:**
- Remove manual loading/error `ref()`s that are now handled by TanStack Query's return values (`isPending`, `isError`, `error`).
- Remove manual `onMounted` fetch calls.
- Ensure SSR compatibility — queries in Nuxt pages are automatically awaited during SSR.
6. **Verify** the page still renders correctly and that cache invalidation triggers re-fetches where expected.
+1 -1
View File
@@ -147,7 +147,7 @@ jobs:
deploy:
needs: [skip-if-clean, docker-build]
if: ${{ needs.skip-if-clean.outputs.internal == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/prod') }}
if: ${{ needs.skip-if-clean.outputs.internal == 'true' && github.ref == 'refs/heads/prod' }}
uses: SparkUniverse/workflows/.github/workflows/argo-update.yaml@main
secrets:
ARGOCD_DEPLOY_KEY: ${{ secrets.ARGOCD_DEPLOY_KEY }}
+9 -2
View File
@@ -59,14 +59,21 @@ apps/frontend/src/generated
.turbo
target
generated
!apps/app-frontend/src/generated/
!apps/app-frontend/src/generated/app-events/
!apps/app-frontend/src/generated/app-events/*.ts
!apps/app-frontend/src/generated/app-events/README.md
!apps/app-frontend/src/generated/app-events/postcard/
!apps/app-frontend/src/generated/app-events/postcard/index.d.ts
!apps/app-frontend/src/generated/app-events/postcard/index.js
!apps/app-frontend/src/generated/app-events/postcard/package.json
.env
# app testing dir
app-playground-data/*
.astro
.claude/*
!.claude/skills/
.claude/
.letta
# labrinth demo fixtures
+11
View File
@@ -17,6 +17,17 @@
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-util/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/muralpay/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/labrinth-derive/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-content-management/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-content-management/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/neverbounce/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/serde-binhum/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/sqlx-tracing/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/sqlx-tracing/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/xredis/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
-1
View File
@@ -1 +0,0 @@
CLAUDE.md
+93
View File
@@ -0,0 +1,93 @@
# Modrinth Monorepo
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read its AGENTS.md.
## Architecture
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
- **Indentation:** Use TAB everywhere, never spaces
### Apps (`apps/`)
| App | Description |
| ----------------- | ------------------------------ |
| `frontend` | Main Modrinth website (Nuxt 3) |
| `app-frontend` | Desktop/app frontend (Vue 3) |
| `app` | Desktop/app shell (Tauri) |
| `app-playground` | Testing playground for app |
| `labrinth` | Backend API service |
| `daedalus_client` | Daedalus client implementation |
| `docs` | Documentation site (Astro) |
### Packages (`packages/`)
| Package | Description |
| ------------------ | ----------------------------------------------------- |
| `ui` | Shared Vue component library (`@modrinth/ui`) |
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
| `app-lib` | Shared app library |
| `blog` | Blog system and changelog data |
| `utils` | Shared utility functions (mostly deprecated) |
| `moderation` | Moderation utilities |
| `daedalus` | Daedalus protocol |
| `tooling-config` | ESLint, Prettier, TypeScript configs |
| `ariadne` | Analytics library |
| `modrinth-log` | Logging utilities |
| `modrinth-maxmind` | MaxMind GeoIP |
| `modrinth-util` | General utilities |
| `muralpay` | Payment processing |
| `path-util` | Path utilities |
| `sqlx-tracing` | SQLx query tracing |
## Pre-PR Commands
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
- **Website:** `pnpm prepr:frontend:web`
- **App frontend:** `pnpm prepr:frontend:app`
- **Frontend libs:** `pnpm prepr:frontend:lib`
- **All frontend (app+web):** `pnpm prepr`
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
The website and app `prepr` commands
## Dev Commands
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
- **Storybook (packages/ui):** `pnpm storybook`
## Project-Specific Instructions
Each project may have its own file with detailed instructions:
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/AGENTS.md`](apps/frontend/AGENTS.md) - Frontend Website
## Code Guidelines
### Comments
- DO NOT use "heading" comments like: `=== Helper methods ===`.
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
## Bash Guidelines
### Output handling
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
- NEVER use `| head -n X` or `| tail -n X` to truncate output
- IMPORTANT: Run commands directly without pipes when possible
- IMPORTANT: If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
- ALWAYS read the full output — never pipe through filters
### General
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
- For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc.
- Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client`
- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them.
## Standards
Standards available at the @standards/ folder.
-110
View File
@@ -1,110 +0,0 @@
# Modrinth Monorepo
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read it's CLAUDE.md.
## Architecture
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
- **Indentation:** Use TAB everywhere, never spaces
### Apps (`apps/`)
| App | Description |
| ----------------- | ------------------------------ |
| `frontend` | Main Modrinth website (Nuxt 3) |
| `app-frontend` | Desktop/app frontend (Vue 3) |
| `app` | Desktop/app shell (Tauri) |
| `app-playground` | Testing playground for app |
| `labrinth` | Backend API service |
| `daedalus_client` | Daedalus client implementation |
| `docs` | Documentation site (Astro) |
### Packages (`packages/`)
| Package | Description |
| ------------------ | ----------------------------------------------------- |
| `ui` | Shared Vue component library (`@modrinth/ui`) |
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
| `app-lib` | Shared app library |
| `blog` | Blog system and changelog data |
| `utils` | Shared utility functions (mostly deprecated) |
| `moderation` | Moderation utilities |
| `daedalus` | Daedalus protocol |
| `tooling-config` | ESLint, Prettier, TypeScript configs |
| `ariadne` | Analytics library |
| `modrinth-log` | Logging utilities |
| `modrinth-maxmind` | MaxMind GeoIP |
| `modrinth-util` | General utilities |
| `muralpay` | Payment processing |
| `path-util` | Path utilities |
| `sqlx-tracing` | SQLx query tracing |
## Pre-PR Commands
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
- **Website:** `pnpm prepr:frontend:web`
- **App frontend:** `pnpm prepr:frontend:app`
- **Frontend libs:** `pnpm prepr:frontend:lib`
- **All frontend (app+web):** `pnpm prepr`
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
The website and app `prepr` commands
## Dev Commands
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
- **Storybook (packages/ui):** `pnpm storybook`
## Project-Specific Instructions
Each project may have its own file with detailed instructions:
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
## Code Guidelines
### Comments
- DO NOT use "heading" comments like: `=== Helper methods ===`.
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
## Bash Guidelines
### Output handling
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
- NEVER use `| head -n X` or `| tail -n X` to truncate output
- IMPORTANT: Run commands directly without pipes when possible
- IMPORTANT: If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
- ALWAYS read the full output — never pipe through filters
### General
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
- For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc.
- Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client`
- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them.
## Edit Tool - Whitespace Handling (CLAUDE ONLY)
The Read tool uses `→` to mark where line numbers end and file content begins.
**Rule:** Copy the EXACT whitespace that appears after the `→` marker.
- Whatever appears between `→` and the code text is what's actually in the file
- That whitespace must be used EXACTLY in Edit tool's old_string
- Don't count arrows, don't interpret - just copy what's after the `→`
**Example:**
14→ private byte tag;
For Edit, use: ` private byte tag;` (copy everything after →, including the two tabs)
**If Edit fails:** Stop and explain the problem. Do not attempt sed/awk/bash workarounds.
**IMPORTANT**: Trust the Read tool output. Copy what's after `→` into Edit immediately. DO NOT verify with sed/od/grep first - that's wasting time and the instructions already tell you to stop if Edit fails, not to pre-verify.
## Standards
Standards available at the @standards/ folder.
Generated
+190 -2
View File
@@ -2116,7 +2116,7 @@ checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"serde_derive_internals 0.29.1",
"syn 2.0.106",
]
@@ -2340,6 +2340,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.16.2"
@@ -3991,6 +4000,28 @@ dependencies = [
"x11",
]
[[package]]
name = "genco"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ab846431e5d637791b3279e7242fe2b21e11c3d8b4cf6a99f645c5f16ba7c0"
dependencies = [
"genco-macros",
"relative-path",
"smallvec",
]
[[package]]
name = "genco-macros"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42a1fe5a699c7f1d36ea6e04ed680a5c787cabff4b610ae3b8954ea3bcefec1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "generic-array"
version = "0.14.9"
@@ -5347,6 +5378,16 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "json5"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c"
dependencies = [
"serde",
"ucd-trie",
]
[[package]]
name = "jsonptr"
version = "0.6.3"
@@ -7499,6 +7540,45 @@ dependencies = [
"serde",
]
[[package]]
name = "postcard-bindgen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e709af61573ae2563a00e760ff7db47f5c70b9d96c6dcb28993526fb8479f68"
dependencies = [
"postcard-bindgen-core",
"postcard-bindgen-derive",
]
[[package]]
name = "postcard-bindgen-core"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f19cebcb145fdccb24b63ad410785aa7c2ff9364551646ca7a927422d2d0386"
dependencies = [
"convert_case 0.11.0",
"genco",
"tree-ds",
]
[[package]]
name = "postcard-bindgen-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0372f55dd7c15d73f7b0f7ca507ac3db5653f346b2b44a36537e2ad39f55441b"
dependencies = [
"convert_case 0.11.0",
"genco",
"postcard-bindgen-core",
"proc-macro2",
"quote",
"regex",
"regex-macro",
"serde",
"serde_derive_internals 0.30.0",
"syn 3.0.3",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
@@ -8381,12 +8461,27 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
[[package]]
name = "regex-macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d306632607af6ec61c0b117971d57a96381b6317cf18ae419b5558048fe016e"
dependencies = [
"regex",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rend"
version = "0.4.2"
@@ -9052,7 +9147,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"serde_derive_internals 0.29.1",
"syn 2.0.106",
]
@@ -9295,6 +9390,16 @@ dependencies = [
"uuid 1.23.3",
]
[[package]]
name = "sequential_gen"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d630b1418311e028ceb45e1e567845501a9d9c770a258b80d2edc025ffad17c"
dependencies = [
"lazy_static",
"uuid 1.23.3",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -9400,6 +9505,17 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "serde_derive_internals"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_ini"
version = "0.2.0"
@@ -9840,6 +9956,9 @@ name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]]
name = "spinning_top"
@@ -10267,6 +10386,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -10880,6 +11010,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "testcontainers"
version = "0.25.2"
@@ -10959,6 +11098,7 @@ dependencies = [
"image",
"indicatif",
"itertools 0.14.0",
"json5",
"modrinth-content-management",
"notify",
"notify-debouncer-mini",
@@ -10968,6 +11108,8 @@ dependencies = [
"path-util",
"phf 0.13.1",
"png 0.18.0",
"postcard",
"postcard-bindgen",
"quartz_nbt",
"quick-xml 0.38.3",
"rand 0.8.5",
@@ -10975,6 +11117,7 @@ dependencies = [
"reqwest 0.12.24",
"rgb",
"serde",
"serde-binhum",
"serde_ini",
"serde_json",
"serde_with",
@@ -10988,9 +11131,11 @@ dependencies = [
"thiserror 2.0.17",
"tokio",
"tokio-util",
"toml 0.9.8",
"tracing",
"tracing-error",
"tracing-subscriber",
"ts-rs",
"url",
"urlencoding",
"uuid 1.23.3",
@@ -11667,12 +11812,49 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "tree-ds"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710c388a2bb52d1a7186ae4aa2e92c6fae0606426bf4bdfb4106fa1e8e5c602e"
dependencies = [
"lazy_static",
"sequential_gen",
"serde",
"spin 0.10.0",
"thiserror 2.0.17",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
dependencies = [
"chrono",
"thiserror 2.0.17",
"ts-rs-macros",
"uuid 1.23.3",
]
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"termcolor",
]
[[package]]
name = "tungstenite"
version = "0.27.0"
@@ -11710,6 +11892,12 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uds_windows"
version = "1.1.0"
+4
View File
@@ -113,6 +113,7 @@ indicatif = "0.18.0"
itertools = "0.14.0"
jemalloc_pprof = "0.8.1"
json-patch = { version = "4.1.0", default-features = false }
json5 = "1.3.1"
lettre = { version = "0.11.19", default-features = false, features = [
"aws-lc-rs",
"builder",
@@ -148,6 +149,7 @@ path-util = { path = "packages/path-util" }
phf = { version = "0.13.1", features = ["macros"] }
png = "0.18.0"
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
postcard-bindgen = "0.8.0"
proc-macro2 = { version = "1.0" }
prometheus = "0.14.0"
quartz_nbt = "0.2.9"
@@ -224,12 +226,14 @@ tikv-jemallocator = "0.6.0"
tokio = "1.47.1"
tokio-stream = "0.1.17"
tokio-util = "0.7.16"
toml = "0.9.8"
totp-rs = "5.7.0"
tracing = "0.1.41"
tracing-actix-web = { version = "0.7.19", default-features = false }
tracing-ecs = "0.5.0"
tracing-error = "0.2.1"
tracing-subscriber = "0.3.20"
ts-rs = "12.0.1"
typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
+4
View File
@@ -24,3 +24,7 @@ gam = "gam"
consts = "consts"
# short for "Copy"
Cpy = "Cpy"
[default.extend-identifiers]
# Constant from the `zip` crate
ZIP64_BYTES_THR = "ZIP64_BYTES_THR"
+4
View File
@@ -2,3 +2,7 @@
*.gltf
src/locales/
src/assets/**/*.svg
# Generated app-event bindings
src/generated/app-events/*.ts
src/generated/app-events/postcard/**
+6 -1
View File
@@ -1,2 +1,7 @@
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
export default config
export default config.append([
{
ignores: ['src/generated/app-events/*.ts', 'src/generated/app-events/postcard/**'],
},
])
+78 -29
View File
@@ -71,6 +71,7 @@ import AppActionBar from '@/components/ui/AppActionBar.vue'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
@@ -89,19 +90,19 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { useAppEvent } from '@/composables/use-app-event'
import { config } from '@/config'
import {
ads_consent_listener,
hide_ads_window,
init_ads_window,
perform_ads_consent_action,
release_ads_window_hold,
should_show_ads_consent_popup,
show_ads_window,
take_ads_window_hold,
} from '@/helpers/ads.js'
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance'
import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
@@ -140,6 +141,7 @@ import {
} from '@/providers/download-progress.ts'
import { createServerInstall, provideServerInstall } from '@/providers/server-install'
import { setupProviders } from '@/providers/setup'
import { setupAppEventsProvider } from '@/providers/setup/app-events'
import { setupAuthProvider } from '@/providers/setup/auth'
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
import { useError } from '@/store/error.js'
@@ -155,6 +157,7 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
const breadcrumbManager = createBreadcrumbManager()
provideBreadcrumbManager(breadcrumbManager)
const canNavigateBack = ref(false)
@@ -166,6 +169,25 @@ function updateHistoryNavigationState() {
canNavigateForward.value = historyState?.forward != null
}
let fullscreenAdsWindowHold = false
async function handleFullscreenChange() {
const fullscreen = document.fullscreenElement !== null
if (fullscreen === fullscreenAdsWindowHold) return
fullscreenAdsWindowHold = fullscreen
try {
if (fullscreen) {
await take_ads_window_hold()
} else {
await release_ads_window_hold()
}
} catch (error) {
fullscreenAdsWindowHold = !fullscreen
handleError(error)
}
}
updateHistoryNavigationState()
const APP_LEFT_NAV_WIDTH = '4rem'
@@ -175,14 +197,26 @@ const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
let credentialsRefreshId = 0
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
watch(
() => themeStore.toggleSidebar,
(toggleSidebar) => {
sidebarToggled.value = !toggleSidebar
},
)
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
() =>
route.path.startsWith('/browse') ||
route.path.startsWith('/project') ||
route.path.startsWith('/user'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const hostingUpdateRequired = computed(
() =>
hostingRouteActive.value &&
!!appUpdateState.availableUpdate.value &&
appUpdateState.updatesEnabled.value,
)
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
)
@@ -193,7 +227,9 @@ const hostingIntercomIdentityKey = computed(() => {
return `${userId}:${serverId ?? 'hosting'}`
})
const hostingIntercom = useHostingIntercom({
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
enabled: computed(
() => hostingRouteActive.value && !hostingUpdateRequired.value && !!credentials.value?.session,
),
appId: 'ykeritl9',
fetchToken: fetchIntercomToken,
identityKey: hostingIntercomIdentityKey,
@@ -208,11 +244,22 @@ const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
const { handleError, addNotification } = notificationManager
useAppEvent(
'warning',
(event) =>
addNotification({
title: 'Warning',
text: event.message,
type: 'warning',
}),
appEvents,
)
const popupNotificationManager = new AppPopupNotificationManager()
providePopupNotificationManager(popupNotificationManager)
const { addPopupNotification } = popupNotificationManager
let adsConsentPopupId = null
let unlistenAdsConsent
useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents)
const appVersion = getVersion()
const tauriApiClient = new TauriModrinthClient({
@@ -281,8 +328,8 @@ providePageContext({
})
provideModalBehavior({
noblur: computed(() => !themeStore.advancedRendering),
onShow: () => hide_ads_window(),
onHide: () => show_ads_window(),
onShow: () => take_ads_window_hold(),
onHide: () => release_ads_window_hold(),
})
const {
@@ -297,7 +344,7 @@ const {
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
} = setupProviders(notificationManager, popupNotificationManager)
} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager)
const news = ref([])
const displayedServerInviteNotifications = new Set()
@@ -349,7 +396,6 @@ const authUnreachable = computed(() => {
onMounted(async () => {
await useCheckDisableMouseover()
try {
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
handleAdsConsentRequired(await should_show_ads_consent_popup())
} catch (error) {
handleError(error)
@@ -357,6 +403,7 @@ onMounted(async () => {
document.querySelector('body').addEventListener('click', handleClick)
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
document.addEventListener('fullscreenchange', handleFullscreenChange)
checkUpdates()
})
@@ -364,10 +411,13 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
unsubscribeSidebarToggle()
document.removeEventListener('fullscreenchange', handleFullscreenChange)
clearDelayedUpdatePopup()
await unlistenAdsConsent?.()
if (fullscreenAdsWindowHold) {
fullscreenAdsWindowHold = false
await release_ads_window_hold().catch(handleError)
}
await unlistenUpdateDownload?.()
})
@@ -576,14 +626,6 @@ async function setupApp() {
document.getElementsByTagName('html')[0].classList.add('windows')
}
await warning_listener((e) =>
addNotification({
title: 'Warning',
text: e.message,
type: 'warning',
}),
)
fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`)
.then((response) => response.json())
.then((res) => {
@@ -632,7 +674,7 @@ async function setupApp() {
}
const stateFailed = ref(false)
initialize_state()
initialize_state(appEventChannel)
.then(() => {
setupApp().catch((err) => {
stateFailed.value = true
@@ -762,7 +804,7 @@ const errorModal = ref()
const minecraftAuthErrorModal = ref()
const minecraftRequiredModal = ref()
const contentInstall = createContentInstall({ router, handleError })
const contentInstall = createContentInstall({ router, handleError, appEvents })
provideContentInstall(contentInstall)
const {
instances: contentInstallInstances,
@@ -795,7 +837,12 @@ const {
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
const serverInstall = createServerInstall({
router,
handleError,
popupNotificationManager,
appEvents,
})
provideServerInstall(serverInstall)
const {
setInstallToPlayModal: setServerInstallToPlayModal,
@@ -971,8 +1018,8 @@ onMounted(() => {
const accounts = ref(null)
provide('accountsCard', accounts)
command_listener(handleCommand)
notification_listener(handleLiveNotification)
useAppEvent('command', handleCommand, appEvents)
useAppEvent('notification', handleLiveNotification, appEvents)
async function markLiveNotificationRead(notification) {
try {
@@ -1398,6 +1445,7 @@ async function downloadUpdate(versionToDownload) {
handleError(e)
})
unlistenUpdateDownload = await subscribeToDownloadProgress(
appEvents,
appUpdateDownload,
versionToDownload.version,
)
@@ -1756,7 +1804,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
<RouterView v-slot="{ Component }">
<HostingUpdateRequired v-if="hostingUpdateRequired" />
<RouterView v-else v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
<component :is="Component"></component>
@@ -104,8 +104,9 @@ import {
useVIntl,
} from '@modrinth/ui'
import type { Ref } from 'vue'
import { computed, onUnmounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import {
get_default_user,
@@ -114,7 +115,6 @@ import {
set_default_user,
users,
} from '@/helpers/auth'
import { process_listener } from '@/helpers/events'
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Skin } from '@/helpers/skins'
import { get_available_skins } from '@/helpers/skins'
@@ -251,16 +251,12 @@ async function logout(id: string) {
trackEvent('AccountLogOut')
}
const unlisten = await process_listener(async (e) => {
useAppEvent('process', async (e) => {
if (e.event === 'launched') {
await refreshValues()
}
})
onUnmounted(() => {
unlisten()
})
const messages = defineMessages({
notSignedIn: {
id: 'minecraft-account.not-signed-in',
@@ -1,15 +1,16 @@
<template>
<div class="flex gap-2 items-center">
<IconButton
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
v-tooltip="formatMessage(messages.viewActiveDownloads)"
type="quiet"
color="brand"
:label="formatMessage(messages.viewActiveDownloads)"
@click="openDownloadToast()"
>
<DownloadIcon />
</IconButton>
<div v-if="downloadState.total > 0 || hasActiveLoadingBars" class="relative">
<IconButton
v-tooltip="downloadToggleLabel"
:color="downloadState.hidden > 0 ? 'brand' : undefined"
type="quiet"
:label="downloadToggleLabel"
@click="toggleDownloadNotifications"
>
<DownloadIcon />
</IconButton>
</div>
<div v-if="offline" class="flex items-center gap-1">
<UnplugIcon class="text-secondary" />
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
@@ -147,8 +148,8 @@ import { useRouter } from 'vue-router'
import AppUpdateButton from '@/components/ui/app-update-button/index.vue'
import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { loading_listener, process_listener } from '@/helpers/events'
import { get_many as getInstances } from '@/helpers/instance'
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
import type { LoadingBar } from '@/helpers/state'
@@ -218,8 +219,35 @@ const messages = defineMessages({
id: 'app.action-bar.view-active-downloads',
defaultMessage: 'View active downloads',
},
hideDownloads: {
id: 'app.action-bar.hide-downloads',
defaultMessage: 'Hide active downloads',
},
showDownloads: {
id: 'app.action-bar.show-downloads',
defaultMessage: 'Show active downloads',
},
})
const downloadState = computed(() => popupNotificationManager.getDownloadState())
const downloadToggleLabel = computed(() =>
formatMessage(
downloadState.value.hidden > 0
? messages.showDownloads
: downloadState.value.total > 0
? messages.hideDownloads
: messages.viewActiveDownloads,
),
)
function toggleDownloadNotifications(): void {
if (downloadState.value.total > 0) {
popupNotificationManager.toggleDownloadNotifications()
} else if (hasActiveLoadingBars.value) {
openDownloadToast()
}
}
const currentProcesses = ref<RunningProcess[]>([])
const selectedProcess = ref<RunningProcess | undefined>()
@@ -266,7 +294,7 @@ onMounted(() => {
window.addEventListener('online', handleOnline)
})
const unlistenProcess = await process_listener(async () => {
useAppEvent('process', async () => {
await refresh()
})
@@ -296,6 +324,7 @@ function goToTerminal(instanceId?: string) {
const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const terminalNotificationIds = new Map<string, string | number>()
const dismissed = ref(false)
function getLoadingBarKey(loadingBar: LoadingBar): string {
@@ -341,6 +370,44 @@ function removeNotification(): void {
notificationId.value = null
}
function syncTerminalNotifications(): void {
const terminalNotifications = installJobNotifications.terminalNotifications.value
const currentJobIds = new Set(terminalNotifications.map((notification) => notification.id))
for (const terminal of terminalNotifications) {
const popupId = terminalNotificationIds.get(terminal.id)
let notification = popupId
? popupNotificationManager.getNotifications().find((candidate) => candidate.id === popupId)
: undefined
if (!notification) {
notification = popupNotificationManager.addPopupNotification({
title: terminal.title,
text: terminal.text,
type: terminal.type,
buttons: terminal.buttons,
onDismiss: terminal.onDismiss,
autoCloseMs: null,
})
terminalNotificationIds.set(terminal.id, notification.id)
continue
}
notification.title = terminal.title
notification.text = terminal.text
notification.type = terminal.type
notification.buttons = terminal.buttons
notification.onDismiss = terminal.onDismiss
}
for (const [jobId, popupId] of terminalNotificationIds) {
if (!currentJobIds.has(jobId)) {
popupNotificationManager.removeNotification(popupId)
terminalNotificationIds.delete(jobId)
}
}
}
function buildDownloadItems(): PopupNotificationProgressItem[] {
return [
...installJobNotifications.progressItems.value,
@@ -358,12 +425,13 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
]
}
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
const hasActiveLoadingBars = computed(
() => currentLoadingBars.value.length > 0 || installJobNotifications.active.value,
)
function updateNotification(resummon = false): void {
syncTerminalNotifications()
if (resummon) {
dismissed.value = false
}
@@ -392,7 +460,6 @@ function updateNotification(resummon = false): void {
: formatMessage(messages.downloads)
notif.text = undefined
notif.progressItems = progressItems
notif.buttons = installJobNotifications.buttons.value
notif.progress = undefined
notif.waiting = undefined
} else {
@@ -403,7 +470,6 @@ function updateNotification(resummon = false): void {
type: 'download',
autoCloseMs: null,
progressItems,
buttons: installJobNotifications.buttons.value,
})
notificationId.value = notif.id
}
@@ -488,7 +554,7 @@ const installJobNotifications = await useInstallJobNotifications({
await refreshLoadingBars()
const unlistenLoading = await loading_listener(async () => {
useAppEvent('loading', async () => {
await refreshLoadingBars()
})
@@ -502,11 +568,11 @@ function selectProcess(process: RunningProcess) {
onBeforeUnmount(() => {
removeNotification()
terminalNotificationIds.forEach((id) => popupNotificationManager.removeNotification(id))
terminalNotificationIds.clear()
dismissed.value = false
window.removeEventListener('offline', handleOffline)
window.removeEventListener('online', handleOnline)
unlistenProcess()
unlistenLoading()
installJobNotifications.dispose()
})
</script>
@@ -1,11 +1,12 @@
<script setup>
import { XIcon } from '@modrinth/assets'
import { FolderOpenIcon, XIcon } from '@modrinth/assets'
import {
Button,
commonMessages,
defineMessages,
FileTreeSelect,
injectNotificationManager,
injectPopupNotificationManager,
NewModal,
StyledInput,
useVIntl,
@@ -15,8 +16,10 @@ import { ref, shallowRef } from 'vue'
import { PackageIcon } from '@/assets/icons'
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
import { highlightInFolder } from '@/helpers/utils'
const { handleError } = injectNotificationManager()
const popupNotificationManager = injectPopupNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
@@ -39,6 +42,14 @@ const messages = defineMessages({
defaultMessage: 'Enter modpack description...',
},
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
exportComplete: {
id: 'app.export-modal.export-complete',
defaultMessage: 'Export complete',
},
exportCompleteDescription: {
id: 'app.export-modal.export-complete-description',
defaultMessage: '{name} was exported successfully.',
},
})
const props = defineProps({
@@ -93,16 +104,35 @@ const exportPack = async () => {
})
if (outputPath) {
export_instance_mrpack(
props.instance.id,
outputPath,
includedFilePaths.value,
excludedFilePaths.value,
versionInput.value,
exportDescription.value,
nameInput.value,
).catch((err) => handleError(err))
exportModal.value.hide()
try {
await export_instance_mrpack(
props.instance.id,
outputPath,
includedFilePaths.value,
excludedFilePaths.value,
versionInput.value,
exportDescription.value,
nameInput.value,
)
const fileName = outputPath.split(/[\\/]/).pop() ?? outputPath
popupNotificationManager.addPopupNotification({
title: formatMessage(messages.exportComplete),
text: formatMessage(messages.exportCompleteDescription, { name: fileName }),
type: 'success',
buttons: [
{
label: formatMessage(commonMessages.openInFolderButton),
icon: FolderOpenIcon,
action: () => highlightInFolder(outputPath).catch(handleError),
},
],
})
} catch (error) {
handleError(error)
}
}
}
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { DownloadIcon, ExcitedRinthbot, RefreshCwIcon, ServerStackIcon } from '@modrinth/assets'
import { Button, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { computed } from 'vue'
import {
appUpdateState,
downloadAvailableAppUpdate,
installAvailableAppUpdate,
} from '@/providers/app-update'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
const { formatMessage } = useVIntl()
const messages = defineMessages({
title: {
id: 'app.hosting.update-required.title',
defaultMessage: 'Modrinth App update required',
},
description: {
id: 'app.hosting.update-required.description',
defaultMessage: 'You need to update to use Modrinth Hosting through the Modrinth App',
},
downloadToUpdate: {
id: 'app.hosting.update-required.download',
defaultMessage: 'Download to update',
},
downloadingUpdate: {
id: 'app.action-bar.downloading-update',
defaultMessage: 'Downloading update',
},
reloadToUpdate: {
id: 'app.action-bar.reload-to-update',
defaultMessage: 'Reload to update',
},
excitedRinthbotAlt: {
id: 'app.hosting.update-required.rinthbot-alt',
defaultMessage: 'Excited Modrinth Bot',
},
})
useRootBreadcrumb({
slot: 'root',
id: 'servers',
label: () => formatMessage(commonMessages.serversLabel),
to: '/hosting/manage/',
visual: { type: 'icon', component: ServerStackIcon },
})
const { downloading, downloadPercent, downloadProgress, finishedDownloading } = appUpdateState
const isUpdateDownloading = computed(
() =>
downloading.value ||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
)
async function handleUpdateClick() {
if (isUpdateDownloading.value) {
return
}
if (finishedDownloading.value) {
await installAvailableAppUpdate()
} else {
await downloadAvailableAppUpdate()
}
}
</script>
<template>
<div class="box-border flex min-h-full items-center justify-center p-4">
<div class="relative mx-auto w-full max-w-xl pt-28">
<img
:src="ExcitedRinthbot"
:alt="formatMessage(messages.excitedRinthbotAlt)"
class="absolute right-8 top-0 h-28 w-auto md:right-20"
/>
<div class="relative flex flex-col gap-5 rounded-lg bg-bg-raised p-7 shadow-lg">
<div
class="absolute left-0 top-0 h-px w-full bg-gradient-to-r from-transparent via-green-500 to-transparent opacity-40"
style="
background: linear-gradient(
to right,
transparent 2rem,
var(--color-green) calc(100% - 13rem),
var(--color-green) calc(100% - 5rem),
transparent calc(100% - 2rem)
);
"
></div>
<div class="flex flex-col gap-5">
<h1 class="m-0 text-3xl font-extrabold">
{{ formatMessage(messages.title) }}
</h1>
<p class="m-0 text-lg">
{{ formatMessage(messages.description) }}
</p>
<Button
type="colored"
color="brand"
:disabled="isUpdateDownloading"
:aria-busy="isUpdateDownloading"
@click="handleUpdateClick"
>
<RefreshCwIcon v-if="finishedDownloading" />
<DownloadIcon v-else />
<span v-if="isUpdateDownloading">
{{ formatMessage(messages.downloadingUpdate) }}
<span class="inline-block w-[3ch] text-right tabular-nums">
{{ downloadPercent }}%
</span>
</span>
<span v-else-if="finishedDownloading">
{{ formatMessage(messages.reloadToUpdate) }}
</span>
<span v-else>{{ formatMessage(messages.downloadToUpdate) }}</span>
</Button>
</div>
</div>
</div>
</div>
</template>
@@ -10,11 +10,11 @@ import {
import { Avatar, IconButton, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
@@ -136,7 +136,7 @@ defineExpose({
const currentEvent = ref(null)
const unlisten = await process_listener((e) => {
useAppEvent('process', (e) => {
if (e.instance_id === props.instance.id) {
currentEvent.value = e.event
if (e.event === 'finished') {
@@ -148,7 +148,6 @@ const unlisten = await process_listener((e) => {
onMounted(() => {
checkProcess()
})
onUnmounted(() => unlisten())
</script>
<template>
@@ -7,7 +7,7 @@ import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import NavButton from '@/components/ui/NavButton.vue'
import { instance_listener } from '@/helpers/events.js'
import { useAppEvent } from '@/composables/use-app-event'
import { list } from '@/helpers/instance'
import { instanceKeys } from '@/pages/instance/query-options'
@@ -148,7 +148,7 @@ const getInstances = async () => {
await getInstances()
updateMaxAuto()
const unlistenInstance = await instance_listener(async (event) => {
useAppEvent('instance', async (event) => {
if (event.event !== 'synced') {
await getInstances()
}
@@ -162,7 +162,6 @@ onUnmounted(() => {
window.removeEventListener('resize', updateMaxAuto)
document.body.classList.remove('quick-instance-dragging')
clearOverdragFlash()
unlistenInstance()
})
const messages = defineMessages({
@@ -82,7 +82,7 @@ import { injectLoadingState } from '@modrinth/ui'
import { ref, watch } from 'vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { loading_listener } from '@/helpers/events.js'
import { useAppEvent } from '@/composables/use-app-event'
const doneLoading = ref(false)
const loadingProgress = ref(0)
@@ -132,13 +132,10 @@ function fakeLoadingIncrease() {
}
}
loading_listener(async (e) => {
useAppEvent('loading', (e) => {
if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'
}
})
</script>
@@ -3,12 +3,14 @@ import { NotepadTextIcon, XIcon } from '@modrinth/assets'
import { Button, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { type } from '@tauri-apps/plugin-os'
import { $fetch } from 'ofetch'
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
import { release_ads_window_hold, take_ads_window_hold } from '@/helpers/ads.js'
import { list } from '@/helpers/instance'
import { get as getCreds } from '@/helpers/mr_auth.ts'
let adsWindowHold = false
type Survey = {
id: string
tally_id: string
@@ -92,30 +94,44 @@ async function openSurvey() {
onOpen: () => console.info('Opened user survey'),
onClose: () => {
console.info('Closed user survey')
show_ads_window()
if (adsWindowHold) {
adsWindowHold = false
release_ads_window_hold()
}
},
onSubmit: () => console.info('Active user survey submitted'),
}
try {
hide_ads_window()
await take_ads_window_hold()
adsWindowHold = true
if (tallyWindow.Tally?.openPopup) {
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
dismissSurvey()
tallyWindow.Tally.openPopup(formId, popupOptions)
} else {
console.warn('Tally script not yet loaded')
show_ads_window()
adsWindowHold = false
await release_ads_window_hold()
}
} catch (e) {
console.error('Error opening Tally popup:', e)
show_ads_window()
if (adsWindowHold) {
adsWindowHold = false
await release_ads_window_hold()
}
}
console.info(`Found user survey to show with tally_id: ${formId}`)
tallyWindow.Tally?.openPopup(formId, popupOptions)
}
onUnmounted(() => {
if (adsWindowHold) {
adsWindowHold = false
release_ads_window_hold()
}
})
function dismissSurvey() {
if (!availableSurvey.value) return
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, String(new Date()))
@@ -23,9 +23,8 @@
</IconButton>
<IconButton
type="quiet"
color="red"
label="Close window"
class="relative expanded-button close-button hover:!bg-red focus-visible:!bg-red"
class="relative expanded-button close-button"
@click="handleClose"
>
<XIcon />
@@ -10,7 +10,6 @@ import {
import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui'
import { computed, ref } from 'vue'
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleSevereError } from '@/store/error.js'
@@ -29,19 +28,13 @@ function show(errorVal: { message?: string }) {
matchedError.value = findMinecraftAuthError(rawError.value)
debugCollapsed.value = true
hide_ads_window()
modal.value?.show()
}
function hide() {
onModalHide()
modal.value?.hide()
}
function onModalHide() {
show_ads_window()
}
defineExpose({
show,
hide,
@@ -74,7 +67,7 @@ async function copyToClipboard(text: string) {
</script>
<template>
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'" @hide="onModalHide">
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'">
<div class="flex flex-col gap-6">
<Admonition
type="warning"
@@ -20,18 +20,12 @@
</div>
<div class="flex flex-col gap-6 px-6 pb-6">
<div class="flex justify-end gap-2">
<ButtonLink class="w-full" href="https://support.modrinth.com" @click="modal?.hide()">
<div class="grid grid-cols-2 gap-2">
<ButtonLink href="https://support.modrinth.com" @click="modal?.hide()">
<MessagesSquareIcon />
{{ formatMessage(messages.getSupport) }}
</ButtonLink>
<Button
type="colored"
color="brand"
class="w-full"
:disabled="loadingSignIn"
@click="signIn"
>
<Button type="colored" color="brand" :disabled="loadingSignIn" @click="signIn">
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
<svg
v-else
@@ -3,7 +3,6 @@
ref="modal"
:header="formatMessage(messages.installToPlay)"
:closable="true"
:on-hide="show_ads_window"
max-width="544px"
width="544px"
>
@@ -141,10 +140,11 @@
</div>
</NewModal>
<ModpackContentModal
ref="modpackContentModal"
:modpack-name="project?.name ?? ''"
:modpack-icon-url="project?.icon_url ?? undefined"
<ManagedContentModal
ref="managedContentModal"
:header="formatMessage(messages.modpackContent)"
:source-name="project?.name ?? ''"
:source-icon-url="project?.icon_url ?? undefined"
/>
</template>
@@ -159,7 +159,7 @@ import {
type ContentItem,
defineMessages,
formatLoader,
ModpackContentModal,
ManagedContentModal,
NewModal,
Table,
type TableColumn,
@@ -169,7 +169,6 @@ import {
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, ref } from 'vue'
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
import { injectServerInstall } from '@/providers/server-install'
@@ -269,10 +268,10 @@ function handleReport() {
}
}
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal>>()
const managedContentModal = ref<InstanceType<typeof ManagedContentModal>>()
async function openViewContents() {
modpackContentModal.value?.showLoading()
managedContentModal.value?.showLoading()
try {
// Ensure version data is available the useQuery may not have resolved yet
const versionId = modpackVersionId.value
@@ -330,10 +329,10 @@ async function openViewContents() {
}
},
)
modpackContentModal.value?.show(contentItems)
managedContentModal.value?.show(contentItems)
} catch (err) {
console.error('Failed to load modpack contents:', err)
modpackContentModal.value?.show([])
managedContentModal.value?.show([])
}
}
@@ -352,7 +351,6 @@ async function show(
if (modpackVersionIdVal) await fetchData(modpackVersionIdVal)
hide_ads_window()
modal.value?.show(e)
await nextTick()
forceCheckTableScroll()
@@ -363,6 +361,10 @@ function hide() {
}
const messages = defineMessages({
modpackContent: {
id: 'app.modal.install-to-play.managed-content.modpack-header',
defaultMessage: 'Modpack content',
},
installToPlay: {
id: 'app.modal.install-to-play.header',
defaultMessage: 'Install to play',
@@ -403,8 +405,7 @@ const messages = defineMessages({
},
reviewedFiles: {
id: 'app.modal.install-to-play.reviewed-files',
defaultMessage:
'A file is only reviewed if its published to Modrinth, regardless of its file format (including .mrpack).',
defaultMessage: "Files that aren't published to Modrinth aren't reviewed.",
},
installAnyway: {
id: 'app.modal.install-to-play.install-anyway',
@@ -34,6 +34,7 @@ import { get_project_many, get_version, get_version_many } from '@/helpers/cache
import { wait_for_install_job } from '@/helpers/install'
import { update_managed_modrinth_version } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { injectAppEvents } from '@/providers/app-events'
import { injectServerInstall } from '@/providers/server-install'
type Dependency = Labrinth.Versions.v3.Dependency
@@ -74,6 +75,7 @@ type ProjectInfo = {
}
const { formatMessage } = useVIntl()
const appEvents = injectAppEvents()
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
type UpdateCompleteCallback = () => void | Promise<void>
@@ -253,7 +255,7 @@ async function handleUpdate() {
try {
if (modpackVersionId.value && instance.value) {
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
await wait_for_install_job(job.job_id)
await wait_for_install_job(appEvents, job.job_id)
await onUpdateComplete.value()
}
} catch (error) {
@@ -39,6 +39,7 @@ import {
} from '@/helpers/install'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import type { GameInstance } from '@/helpers/types'
import { injectAppEvents } from '@/providers/app-events'
type UpdateCompleteCallback = () => void | Promise<void>
@@ -54,6 +55,7 @@ const instance = ref<GameInstance | null>(null)
const preview = ref<SharedInstanceUpdatePreview | null>(null)
const onComplete = ref<UpdateCompleteCallback>(() => {})
const { formatMessage } = useVIntl()
const appEvents = injectAppEvents()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const diffs = computed<ContentDiffItem[]>(
() =>
@@ -78,7 +80,7 @@ async function update() {
try {
if (instance.value) {
const job = await install_update_shared_instance(instance.value.id)
await wait_for_install_job(job.job_id)
await wait_for_install_job(appEvents, job.job_id)
await onComplete.value()
successful = true
}
@@ -229,11 +229,11 @@
</div>
</template>
</NewModal>
<ModpackContentModal
<ManagedContentModal
ref="contentModal"
:header="formatMessage(messages.sharedInstanceContent)"
:modpack-name="preview?.name ?? ''"
:modpack-icon-url="preview?.iconUrl ?? undefined"
:source-name="preview?.name ?? ''"
:source-icon-url="preview?.iconUrl ?? undefined"
/>
</template>
@@ -251,12 +251,13 @@ import {
type ComboboxOption,
commonMessages,
defineMessages,
formatReportType,
injectAuth,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
ManagedContentModal,
MarkdownEditor,
ModpackContentModal,
NewModal,
Table,
type TableColumn,
@@ -268,7 +269,6 @@ import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, ref } from 'vue'
import { config } from '@/config'
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
import { toError } from '@/helpers/errors'
import type { SharedInstanceInstallPreview } from '@/helpers/install'
import { create_report } from '@/helpers/reports'
@@ -289,7 +289,7 @@ type SharedInstanceCreator = {
}
const modal = ref<InstanceType<typeof NewModal>>()
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
const contentModal = ref<InstanceType<typeof ManagedContentModal>>()
const externalFileTable = ref<HTMLElement | null>(null)
const preview = ref<SharedInstanceInstallPreview | null>(null)
const creator = ref<SharedInstanceCreator | null>(null)
@@ -329,9 +329,9 @@ const externalFileRows = computed<ExternalFileRow[]>(() =>
.sort((left, right) => left.name.localeCompare(right.name)),
)
const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
{ value: 'malicious', label: formatMessage(messages.maliciousReason) },
{ value: 'inappropriate', label: formatMessage(messages.inappropriateReason) },
{ value: 'spam', label: formatMessage(messages.spamReason) },
{ value: 'malicious', label: formatReportType(formatMessage, 'malicious') },
{ value: 'inappropriate', label: formatReportType(formatMessage, 'inappropriate') },
{ value: 'spam', label: formatReportType(formatMessage, 'spam') },
])
const canSubmitReport = computed(
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
@@ -448,7 +448,6 @@ function handleCancel() {
function handleHide() {
resetReportState()
creator.value = null
show_ads_window()
}
function resetReportState() {
reportMode.value = false
@@ -488,7 +487,6 @@ function showReport(
}
function showPreview(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
preview.value = previewValue
hide_ads_window()
modal.value?.show(event)
void nextTick(() => forceCheckTableScroll())
}
@@ -543,18 +541,6 @@ const messages = defineMessages({
id: 'app.modal.install-to-play.report-reason',
defaultMessage: 'Which rule does this instance violate?',
},
maliciousReason: {
id: 'app.modal.install-to-play.report-reason.malicious',
defaultMessage: 'Malicious',
},
inappropriateReason: {
id: 'app.modal.install-to-play.report-reason.inappropriate',
defaultMessage: 'Inappropriate',
},
spamReason: {
id: 'app.modal.install-to-play.report-reason.spam',
defaultMessage: 'Spam',
},
additionalContext: {
id: 'app.modal.install-to-play.additional-context',
defaultMessage: 'Additional context',
@@ -598,8 +584,7 @@ const messages = defineMessages({
},
reviewedFiles: {
id: 'app.modal.install-to-play.reviewed-files',
defaultMessage:
'A file is only reviewed if its published to Modrinth, regardless of its file format (including .mrpack).',
defaultMessage: "Files that aren't published to Modrinth aren't reviewed.",
},
installAnyway: {
id: 'app.modal.install-to-play.install-anyway',
@@ -21,12 +21,12 @@ import {
import { capitalizeString } from '@modrinth/utils'
import { convertFileSrc } from '@tauri-apps/api/core'
import type { Dayjs } from 'dayjs'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { get_project } from '@/helpers/cache'
import { process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
@@ -108,7 +108,7 @@ const stop = async (event: MouseEvent) => {
loading.value = false
}
const unlistenProcesses = await process_listener(async () => {
useAppEvent('process', async () => {
await checkProcess()
})
@@ -121,10 +121,6 @@ const checkProcess = async () => {
onMounted(() => {
checkProcess()
})
onUnmounted(() => {
unlistenProcesses()
})
</script>
<template>
<SmartClickable>
@@ -5,12 +5,12 @@ import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
import { platform } from '@tauri-apps/plugin-os'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { instance_listener, process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_all } from '@/helpers/process'
import { get_game_versions } from '@/helpers/tags'
@@ -215,11 +215,11 @@ async function stopInstance(path: string) {
const currentInstance = ref<string>()
const currentWorld = ref<string>()
const unlistenProcesses = await process_listener(async () => {
useAppEvent('process', async () => {
await checkProcesses()
})
const unlistenInstances = await instance_listener(async () => {
useAppEvent('instance', async () => {
await populateJumpBackIn().catch(() => {
console.error('Failed to populate jump back in')
})
@@ -251,11 +251,6 @@ onMounted(() => {
checkProcesses()
linuxPopulateCount.value = 0
})
onUnmounted(() => {
unlistenProcesses()
unlistenInstances()
})
</script>
<template>
@@ -10,7 +10,6 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref } from 'vue'
import type { Router } from 'vue-router'
import { install_job_listener } from '@/helpers/events'
import {
install_job_dismiss,
install_job_list,
@@ -23,6 +22,7 @@ import {
type InstallProgress,
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
import { injectAppEvents } from '@/providers/app-events'
import { useTheming } from '@/store/state'
const messages = defineMessages({
@@ -234,6 +234,7 @@ export async function useInstallJobNotifications(opts: {
handleError: (err: unknown) => void
onChange: () => void
}) {
const appEvents = injectAppEvents()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const jobs = ref<InstallJobSnapshot[]>([])
@@ -404,8 +405,6 @@ export async function useInstallJobNotifications(opts: {
}
function getProgress(job: InstallJobSnapshot): number {
if (job.status === 'succeeded') return 1
if (job.status === 'failed' || job.status === 'interrupted') return 0
const progress = getEffectiveProgress(job)
if (!progress || progress.total <= 0) return 0
return Math.max(0, Math.min(1, progress.current / progress.total))
@@ -526,8 +525,12 @@ export async function useInstallJobNotifications(opts: {
)
}
const activeJobs = computed(() =>
jobs.value.filter((job) => job.status === 'queued' || job.status === 'running'),
)
const progressItems = computed<PopupNotificationProgressItem[]>(() =>
jobs.value.map((job) => {
activeJobs.value.map((job) => {
const progress = getEffectiveProgress(job)
return {
@@ -536,20 +539,26 @@ export async function useInstallJobNotifications(opts: {
text: getText(job),
iconUrl: iconUrls.value[job.job_id] ?? null,
progress: getProgress(job),
waiting: !job.progress && ['queued', 'running'].includes(job.status),
showProgress: !isTerminalJob(job),
wrapText: isTerminalJob(job),
progressType: isTerminalJob(job) ? undefined : getProgressType(job),
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
waiting: !job.progress && job.status === 'running',
showProgress: job.status === 'running',
progressType: getProgressType(job),
progressCurrent: progress?.current,
progressTotal: progress?.total,
buttons: getButtons(job),
dismissible: isTerminalJob(job),
onDismiss: getDismissHandler(job),
}
}),
)
const buttons = computed<PopupNotificationButton[] | undefined>(() => undefined)
const terminalNotifications = computed(() =>
jobs.value.filter(isTerminalJob).map((job) => ({
id: job.job_id,
title: getTitle(job),
text: getText(job),
type: job.status === 'failed' ? ('error' as const) : ('warning' as const),
buttons: getButtons(job),
onDismiss: getDismissHandler(job),
})),
)
async function refreshMetadata(notify = true) {
const request = ++metadataRequest
@@ -627,14 +636,14 @@ export async function useInstallJobNotifications(opts: {
void refreshMetadata()
}
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
const unlisten = appEvents.on('install_job', applyJobUpdate)
await refresh(false)
return {
active: computed(() => jobs.value.length > 0),
active: computed(() => activeJobs.value.length > 0),
title: computed(() => formatMessage(messages.installs)),
progressItems,
buttons,
terminalNotifications,
refresh,
dispose: () => {
for (const timeout of copiedResetTimeouts.values()) {
@@ -12,7 +12,7 @@ import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { useAppEvent } from '@/composables/use-app-event'
import { kill, list as listInstances } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
@@ -78,7 +78,6 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('checkServerRunningStates', { hitCount: hits.length })
@@ -280,7 +279,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
process_listener((event: { event: string; instance_id: string }) => {
useAppEvent('process', (event) => {
debugLog('process event', event)
if (event.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
@@ -292,14 +291,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
})
.then((unlisten) => {
unlistenProcesses = unlisten
})
.catch(options.handleError)
onUnmounted(() => {
serverPingsActive = false
unlistenProcesses?.()
})
return {
@@ -0,0 +1,18 @@
import { onScopeDispose } from 'vue'
import {
type AppEventHandler,
type AppEvents,
type AppEventType,
injectAppEvents,
} from '@/providers/app-events'
export function useAppEvent<Type extends AppEventType>(
type: Type,
handler: AppEventHandler<Type>,
events: AppEvents = injectAppEvents(),
) {
const unsubscribe = events.on(type, handler)
onScopeDispose(unsubscribe)
return unsubscribe
}
@@ -1,8 +1,8 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
import { useAppEvent } from '@/composables/use-app-event'
import { toError } from '@/helpers/errors'
import { friend_listener } from '@/helpers/events.js'
import {
acceptCachedFriend,
add_friend,
@@ -127,13 +127,9 @@ export function useFriends(options: {
)
}
let unlisten: (() => void) | undefined
void friend_listener(() => {
useAppEvent('friend', () => {
void queryClient.invalidateQueries({ queryKey: queryKey.value })
}).then((listener) => {
unlisten = listener
})
onUnmounted(() => unlisten?.())
return {
query,
@@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CommandPayload } from "./CommandPayload";
import type { FriendPayload } from "./FriendPayload";
import type { InstallJobSnapshot } from "./InstallJobSnapshot";
import type { InstanceBulkUpdateProgressPayload } from "./InstanceBulkUpdateProgressPayload";
import type { InstancePayload } from "./InstancePayload";
import type { LoadingPayload } from "./LoadingPayload";
import type { LogPayload } from "./LogPayload";
import type { ProcessPayload } from "./ProcessPayload";
import type { WarningPayload } from "./WarningPayload";
export type AppEvent = { "type": "loading", "payload": LoadingPayload } | { "type": "process", "payload": ProcessPayload } | { "type": "instance", "payload": InstancePayload } | { "type": "instance_bulk_update_progress", "payload": InstanceBulkUpdateProgressPayload } | { "type": "install_job", "payload": InstallJobSnapshot } | { "type": "command", "payload": CommandPayload } | { "type": "warning", "payload": WarningPayload } | { "type": "friend", "payload": FriendPayload } | { "type": "notification", "payload": unknown } | { "type": "log", "payload": LogPayload } | { "type": "ads_consent_required", "payload": boolean };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FriendStatusPayload } from "./FriendStatusPayload";
export type FriendPayload = { "event": "friend_request", from: string, } | { "event": "user_offline", id: string, } | { "event": "status_update", user_status: FriendStatusPayload, } | { "event": "status_sync" };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FriendStatusPayload = { user_id: string, profile_name: string | null, last_update: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImportLauncherType = "MultiMC" | "PrismLauncher" | "ATLauncher" | "GDLauncher" | "Curseforge" | "Unknown";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallApiErrorDetails = { error: string, status?: number, method?: string, url?: string, route?: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallErrorContext = { operation: string, source_path?: string, target_path?: string, file_path?: string, entry_path?: string, urls: Array<string>, expected_hash?: string, expected_size?: number, project_id?: string, version_id?: string, minecraft_version?: string, loader?: string, java_version?: number, os?: string, arch?: string, };
@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallApiErrorDetails } from "./InstallApiErrorDetails";
import type { InstallErrorContext } from "./InstallErrorContext";
import type { InstallPhaseId } from "./InstallPhaseId";
import type { SharedInstanceUnavailableReason } from "./SharedInstanceUnavailableReason";
export type InstallErrorView = { code: string, phase?: InstallPhaseId, message: string, reason?: SharedInstanceUnavailableReason, api?: InstallApiErrorDetails, context?: InstallErrorContext, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJavaStep = "resolving" | "fetching_metadata" | "downloading" | "extracting" | "validating";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobDisplay = { title: string, icon: string | null, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobKind = "create_instance" | "create_modpack_instance" | "create_shared_instance" | "import_instance" | "duplicate_instance" | "install_existing_instance" | "install_pack_to_existing_instance" | "update_shared_instance";
@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallErrorView } from "./InstallErrorView";
import type { InstallJobDisplay } from "./InstallJobDisplay";
import type { InstallJobKind } from "./InstallJobKind";
import type { InstallJobStatus } from "./InstallJobStatus";
import type { InstallPhaseDetails } from "./InstallPhaseDetails";
import type { InstallPhaseId } from "./InstallPhaseId";
import type { InstallProgress } from "./InstallProgress";
import type { InstallTarget } from "./InstallTarget";
export type InstallJobSnapshot = { job_id: string, instance_id: string | null, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | null, details: InstallPhaseDetails, display: InstallJobDisplay | null, error: InstallErrorView | null, rollback_error: InstallErrorView | null, created: string, modified: string, finished: string | null, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobStatus = "queued" | "running" | "succeeded" | "failed" | "interrupted" | "canceled";
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImportLauncherType } from "./ImportLauncherType";
import type { InstallJavaStep } from "./InstallJavaStep";
import type { ModLoader } from "./ModLoader";
export type InstallPhaseDetails = { "type": "empty" } | { "type": "instance", name: string, } | { "type": "minecraft", game_version: string, loader: ModLoader, } | { "type": "java", major_version: number, step: InstallJavaStep, } | { "type": "modpack", project_id: string | null, version_id: string | null, title: string | null, } | { "type": "import", launcher_type: ImportLauncherType, instance_folder: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallPhaseId = "preparing_instance" | "resolving_pack" | "downloading_pack_file" | "reading_pack_manifest" | "downloading_content" | "extracting_overrides" | "resolving_minecraft" | "resolving_loader" | "preparing_java" | "downloading_minecraft" | "running_loader_processors" | "finalizing" | "rolling_back";
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallProgressSecondary } from "./InstallProgressSecondary";
export type InstallProgress = { current: number, total: number, secondary?: InstallProgressSecondary, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallProgressSecondary = { current: number, total: number, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallTarget = { "type": "new_instance", instance_id: string | null, } | { "type": "existing_instance", instance_id: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstanceBulkUpdateProgressStage } from "./InstanceBulkUpdateProgressStage";
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: number, total: number, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstanceBulkUpdateProgressStage = "resolving_versions" | "downloading" | "finishing";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstancePayload = { instance_id: string, } & ({ "event": "created" } | { "event": "synced" } | { "event": "servers_updated" } | { "event": "world_updated", world: string, } | { "event": "server_joined", host: string, port: number, timestamp: string, } | { "event": "edited" } | { "event": "content_install_finished", project_ids: Array<string>, } | { "event": "content_install_failed", project_ids: Array<string>, message: string, } | { "event": "removed" });
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LoadingBarType = { "type": "legacy_data_migration" } | { "type": "directory_move", old: string, new: string, } | { "type": "java_download", version: number, } | { "type": "pack_file_download", instance_id: string, pack_name: string, icon: string | null, pack_version: string, } | { "type": "pack_download", instance_id: string, pack_name: string, icon: string | null, pack_id: string | null, pack_version: string | null, } | { "type": "minecraft_download", instance_id: string, instance_name: string, } | { "type": "instance_update", instance_id: string, instance_name: string, } | { "type": "zip_extract", instance_id: string, instance_name: string, } | { "type": "config_change", new_path: string, } | { "type": "copy_instance", import_location: string, instance_name: string, } | { "type": "launcher_update", version: string, current_version: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LoadingBarType } from "./LoadingBarType";
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: number | null, message: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Log4jEvent = { timestamp_millis: number | null, logger_name: string | null, level: string | null, thread_name: string | null, message: string | null, throwable: string | null, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Log4jEvent } from "./Log4jEvent";
export type LogPayload = { instance_id: string, } & ({ "type": "log4j" } & Log4jEvent | { "type": "legacy", message: string, });
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModLoader = "vanilla" | "forge" | "fabric" | "quilt" | "neoforge";
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProcessPayloadType } from "./ProcessPayloadType";
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProcessPayloadType = "launched" | "finished";
@@ -0,0 +1,8 @@
# App event bindings
> [!WARNING]
> Do not edit the generated TypeScript or Postcard binding files in this directory manually.
The event bus types are determined by the Rust [`AppEvent`](../../../../../packages/app-lib/src/event/mod.rs) contract.
They are regenerated automatically when you run `pnpm app:dev` from the workspace root. Commit any generated changes with the Rust contract change.
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SharedInstanceUnavailableReason = "deleted" | "access_revoked" | "quarantined";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type WarningPayload = { message: string, };
@@ -0,0 +1,78 @@
declare type u8 = number
declare type u16 = number
declare type u32 = number
declare type u64 = bigint
declare type u128 = bigint
declare type usize = bigint
declare type i8 = number
declare type i16 = number
declare type i32 = number
declare type i64 = bigint
declare type i128 = bigint
declare type isize = bigint
declare type NonZeroU8 = number
declare type NonZeroU16 = number
declare type NonZeroU32 = number
declare type NonZeroU64 = bigint
declare type NonZeroU128 = bigint
declare type NonZeroUsize = bigint
declare type NonZeroI8 = number
declare type NonZeroI16 = number
declare type NonZeroI32 = number
declare type NonZeroI64 = bigint
declare type NonZeroI128 = bigint
declare type NonZeroIsize = bigint
declare type f32 = number
declare type f64 = number
declare type ArrayLengthMutationKeys = "splice" | "push" | "pop" | "shift" | "unshift"
declare type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
& {
readonly length: L
[ I : number ] : T
[Symbol.iterator]: () => IterableIterator<T>
}
export type AppEvent = { tag: "loading", value: LoadingPayload } | { tag: "process", value: ProcessPayload } | { tag: "instance", value: InstancePayload } | { tag: "instance_bulk_update_progress", value: InstanceBulkUpdateProgressPayload } | { tag: "install_job", value: InstallJobSnapshot } | { tag: "command", value: CommandPayload } | { tag: "warning", value: WarningPayload } | { tag: "friend", value: FriendPayload } | { tag: "notification", value: string } | { tag: "log", value: LogPayload } | { tag: "ads_consent_required", value: boolean }
export type LoadingBarType = { tag: "legacy_data_migration" } | { tag: "directory_move", value: { old: string, new: string } } | { tag: "java_download", value: { version: u32 } } | { tag: "pack_file_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_version: string } } | { tag: "pack_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_id: string | undefined, pack_version: string | undefined } } | { tag: "minecraft_download", value: { instance_id: string, instance_name: string } } | { tag: "instance_update", value: { instance_id: string, instance_name: string } } | { tag: "zip_extract", value: { instance_id: string, instance_name: string } } | { tag: "config_change", value: { new_path: string } } | { tag: "copy_instance", value: { import_location: string, instance_name: string } } | { tag: "launcher_update", value: { version: string, current_version: string } }
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: f64 | undefined, message: string }
export type WarningPayload = { message: string }
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: u64, total: u64 }
export type InstanceBulkUpdateProgressStage = { tag: "resolving_versions" } | { tag: "downloading" } | { tag: "finishing" }
export type CommandPayload = { tag: "InstallMod", value: { id: string } } | { tag: "InstallVersion", value: { id: string } } | { tag: "InstallModpack", value: { id: string } } | { tag: "InstallServer", value: { id: string } } | { tag: "LaunchInstance", value: { id: string, server: string | undefined, singleplayer_world: string | undefined } } | { tag: "InstallSharedInstanceInvite", value: { invite_id: string } } | { tag: "RunMRPack", value: { path: string } }
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string }
export type ProcessPayloadType = { tag: "launched" } | { tag: "finished" }
export type InstancePayload = { instance_id: string, event: InstancePayloadType }
export type InstancePayloadType = { tag: "created" } | { tag: "synced" } | { tag: "servers_updated" } | { tag: "world_updated", value: { world: string } } | { tag: "server_joined", value: { host: string, port: u16, timestamp: string } } | { tag: "edited" } | { tag: "content_install_finished", value: { project_ids: string[] } } | { tag: "content_install_failed", value: { project_ids: string[], message: string } } | { tag: "removed" }
export type FriendPayload = { tag: "friend_request", value: { from: string } } | { tag: "user_offline", value: { id: string } } | { tag: "status_update", value: { user_status: FriendStatusPayload } } | { tag: "status_sync" }
export type FriendStatusPayload = { user_id: string, profile_name: string | undefined, last_update: string }
export type SharedInstanceUnavailableReason = { tag: "deleted" } | { tag: "access_revoked" } | { tag: "quarantined" }
export type LogEvent = { tag: "log4j", value: Log4jEvent } | { tag: "legacy", value: { message: string } }
export type LogPayload = { instance_id: string, event: LogEvent }
export type Log4jEvent = { timestamp_millis: i64 | undefined, logger_name: string | undefined, level: string | undefined, thread_name: string | undefined, message: string | undefined, throwable: string | undefined }
export type ModLoader = { tag: "vanilla" } | { tag: "forge" } | { tag: "fabric" } | { tag: "quilt" } | { tag: "neoforge" }
export type InstallJobSnapshot = { job_id: string, instance_id: string | undefined, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | undefined, details: InstallPhaseDetails, display: InstallJobDisplay | undefined, error: InstallErrorView | undefined, rollback_error: InstallErrorView | undefined, created: string, modified: string, finished: string | undefined }
export type InstallJobKind = { tag: "create_instance" } | { tag: "create_modpack_instance" } | { tag: "create_shared_instance" } | { tag: "import_instance" } | { tag: "duplicate_instance" } | { tag: "install_existing_instance" } | { tag: "install_pack_to_existing_instance" } | { tag: "update_shared_instance" }
export type InstallJobStatus = { tag: "queued" } | { tag: "running" } | { tag: "succeeded" } | { tag: "failed" } | { tag: "interrupted" } | { tag: "canceled" }
export type InstallTarget = { tag: "new_instance", value: { instance_id: string | undefined } } | { tag: "existing_instance", value: { instance_id: string } }
export type InstallPhaseId = { tag: "preparing_instance" } | { tag: "resolving_pack" } | { tag: "downloading_pack_file" } | { tag: "reading_pack_manifest" } | { tag: "downloading_content" } | { tag: "extracting_overrides" } | { tag: "resolving_minecraft" } | { tag: "resolving_loader" } | { tag: "preparing_java" } | { tag: "downloading_minecraft" } | { tag: "running_loader_processors" } | { tag: "finalizing" } | { tag: "rolling_back" }
export type InstallProgress = { current: u64, total: u64, secondary: InstallProgressSecondary | undefined }
export type InstallProgressSecondary = { current: u64, total: u64 }
export type InstallPhaseDetails = { tag: "empty" } | { tag: "instance", value: { name: string } } | { tag: "minecraft", value: { game_version: string, loader: ModLoader } } | { tag: "java", value: { major_version: u32, step: InstallJavaStep } } | { tag: "modpack", value: { project_id: string | undefined, version_id: string | undefined, title: string | undefined } } | { tag: "import", value: { launcher_type: ImportLauncherType, instance_folder: string } }
export type InstallJavaStep = { tag: "resolving" } | { tag: "fetching_metadata" } | { tag: "downloading" } | { tag: "extracting" } | { tag: "validating" }
export type InstallJobDisplay = { title: string, icon: string | undefined }
export type InstallErrorView = { code: string, phase: InstallPhaseId | undefined, message: string, reason: SharedInstanceUnavailableReason | undefined, api: InstallApiErrorDetails | undefined, context: InstallErrorContext | undefined }
export type InstallApiErrorDetails = { error: string, status: u16 | undefined, method: string | undefined, url: string | undefined, route: string | undefined }
export type InstallErrorContext = { operation: string, source_path: string | undefined, target_path: string | undefined, file_path: string | undefined, entry_path: string | undefined, urls: string[], expected_hash: string | undefined, expected_size: u64 | undefined, project_id: string | undefined, version_id: string | undefined, minecraft_version: string | undefined, loader: string | undefined, java_version: u32 | undefined, os: string | undefined, arch: string | undefined }
export type ImportLauncherType = { tag: "MultiMC" } | { tag: "PrismLauncher" } | { tag: "ATLauncher" } | { tag: "GDLauncher" } | { tag: "Curseforge" } | { tag: "Unknown" }
export type Type = "AppEvent" | "LoadingBarType" | "LoadingPayload" | "WarningPayload" | "InstanceBulkUpdateProgressPayload" | "InstanceBulkUpdateProgressStage" | "CommandPayload" | "ProcessPayload" | "ProcessPayloadType" | "InstancePayload" | "InstancePayloadType" | "FriendPayload" | "FriendStatusPayload" | "LogEvent" | "LogPayload" | "Log4jEvent" | "InstallJobSnapshot" | "InstallJobKind" | "InstallJobStatus" | "InstallTarget" | "InstallPhaseId" | "InstallProgress" | "InstallProgressSecondary" | "InstallPhaseDetails" | "InstallJavaStep" | "InstallJobDisplay" | "InstallErrorView" | "InstallApiErrorDetails" | "InstallErrorContext" | "ImportLauncherType" | "ModLoader" | "SharedInstanceUnavailableReason"
declare type ValueType<T extends Type> = T extends "AppEvent" ? AppEvent : T extends "LoadingBarType" ? LoadingBarType : T extends "LoadingPayload" ? LoadingPayload : T extends "WarningPayload" ? WarningPayload : T extends "InstanceBulkUpdateProgressPayload" ? InstanceBulkUpdateProgressPayload : T extends "InstanceBulkUpdateProgressStage" ? InstanceBulkUpdateProgressStage : T extends "CommandPayload" ? CommandPayload : T extends "ProcessPayload" ? ProcessPayload : T extends "ProcessPayloadType" ? ProcessPayloadType : T extends "InstancePayload" ? InstancePayload : T extends "InstancePayloadType" ? InstancePayloadType : T extends "FriendPayload" ? FriendPayload : T extends "FriendStatusPayload" ? FriendStatusPayload : T extends "LogEvent" ? LogEvent : T extends "LogPayload" ? LogPayload : T extends "Log4jEvent" ? Log4jEvent : T extends "InstallJobSnapshot" ? InstallJobSnapshot : T extends "InstallJobKind" ? InstallJobKind : T extends "InstallJobStatus" ? InstallJobStatus : T extends "InstallTarget" ? InstallTarget : T extends "InstallPhaseId" ? InstallPhaseId : T extends "InstallProgress" ? InstallProgress : T extends "InstallProgressSecondary" ? InstallProgressSecondary : T extends "InstallPhaseDetails" ? InstallPhaseDetails : T extends "InstallJavaStep" ? InstallJavaStep : T extends "InstallJobDisplay" ? InstallJobDisplay : T extends "InstallErrorView" ? InstallErrorView : T extends "InstallApiErrorDetails" ? InstallApiErrorDetails : T extends "InstallErrorContext" ? InstallErrorContext : T extends "ImportLauncherType" ? ImportLauncherType : T extends "ModLoader" ? ModLoader : T extends "SharedInstanceUnavailableReason" ? SharedInstanceUnavailableReason : void
export interface Result<T extends Type> {
value: ValueType<T>;
bytes: Uint8Array;
}
export function deserialize<T extends Type>(type: T, bytes: Uint8Array): Result<T>
@@ -0,0 +1,953 @@
const BITS_PER_BYTE = 8, BITS_PER_VARINT_BYTE = 7, U8_BYTES = 1, U16_BYTES = 2, U32_BYTES = 4, U64_BYTES = 8, U128_BYTES = 16
const de_zig_zag_signed = (n) => (n >> 1n) ^ (-(n & 0b1n))
const zig_zag = (n_bytes, n) => (n << 1n) ^ (n >> BigInt(n_bytes * BITS_PER_BYTE - 1))
const varint_max = (n_bytes) => Math.floor((n_bytes * BITS_PER_BYTE + (BITS_PER_BYTE - 1)) / BITS_PER_VARINT_BYTE)
const max_of_last_byte = (n_bytes) => (1 << (n_bytes * BITS_PER_BYTE) % 7) - 1
const to_number_if_safe = (n) => Number.MAX_SAFE_INTEGER < ((n < 0n) ? -n : n) ? n : Number(n)
const varint = (n_bytes, n) => { let value = BigInt(n), out = []; for (let i = 0; i < varint_max(n_bytes); i++) { out.push(Number(value & 0xFFn)); if (value < 128n) { return out } out[i] |= 0x80; value >>= 7n } }
class Deserializer {
constructor(bytes_in) { this.bytes = Array.from(bytes_in); }
pop_next = () => { const next = this.bytes.shift(); if (next === undefined) { throw "input buffer too small" } return next }
pop_n = (n) => { const bytes = Array(); for (let i = 0; i < n; i++) { bytes.push(this.bytes.shift()) } return bytes }
get_int8 = (signed) => signed ? new Int8Array([this.pop_next()])[0] : this.pop_next();
try_take = (n_bytes) => { let out = 0n, v_max = varint_max(n_bytes); for (let i = 0; i < v_max; i++) { const val = this.pop_next(), carry = BigInt(val & 0x7F); out |= carry << BigInt(7 * i); if ((val & 0x80) === 0) { if (i === v_max - 1 && val > max_of_last_byte(n_bytes)) { throw "Bad Variant" } else return out } } throw "Bad Variant"; }
deserialize_bool = () => { const byte = this.pop_next(); return byte === undefined ? undefined : byte > 0 ? true : false }
deserialize_number = (n_bytes, signed) => { if (n_bytes === U8_BYTES) { return this.get_int8(signed) } else if (n_bytes === U16_BYTES || n_bytes === U32_BYTES || n_bytes === U64_BYTES || n_bytes === U128_BYTES) { const val = this.try_take(n_bytes); return to_number_if_safe(signed ? de_zig_zag_signed(val) : val) } else { throw "byte count not supported" } }
deserialize_number_float = (n_bytes) => { const b_buffer = new ArrayBuffer(n_bytes), b_view = new DataView(b_buffer); this.pop_n(n_bytes).forEach((b, i) => b_view.setUint8(i, b)); if (n_bytes === U32_BYTES) { return b_view.getFloat32(0, true) } else if (n_bytes === U64_BYTES) { return b_view.getFloat64(0, true) } else { throw "byte count not supported" } }
deserialize_string = () => new TextDecoder().decode(new Uint8Array(this.pop_n(Number(this.try_take(U32_BYTES)))))
deserialize_array = (des, len) => Array.from({length: len === undefined ? Number(this.try_take(U32_BYTES)) : len}, (v, i) => des(this))
deserialize_string_key_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { prev[this.deserialize_string()] = des(this); return prev }, {}) }
deserialize_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { const d = des(this); prev.set(d[0], d[1]); return prev }, new Map()) }
release_bytes = () => { return new Uint8Array(this.bytes); }
}
function deserialize_APP_EVENT(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "loading",
value: deserialize_LOADING_PAYLOAD(d)
};
case 1:
return {
tag: "process",
value: deserialize_PROCESS_PAYLOAD(d)
};
case 2:
return {
tag: "instance",
value: deserialize_INSTANCE_PAYLOAD(d)
};
case 3:
return {
tag: "instance_bulk_update_progress",
value: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d)
};
case 4:
return {
tag: "install_job",
value: deserialize_INSTALL_JOB_SNAPSHOT(d)
};
case 5:
return {
tag: "command",
value: deserialize_COMMAND_PAYLOAD(d)
};
case 6:
return {
tag: "warning",
value: deserialize_WARNING_PAYLOAD(d)
};
case 7:
return {
tag: "friend",
value: deserialize_FRIEND_PAYLOAD(d)
};
case 8:
return {
tag: "notification",
value: d.deserialize_string()
};
case 9:
return {
tag: "log",
value: deserialize_LOG_PAYLOAD(d)
};
case 10:
return {
tag: "ads_consent_required",
value: d.deserialize_bool()
};
default:
throw "variant not implemented"
}
}
function deserialize_LOADING_BAR_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "legacy_data_migration"
};
case 1:
return {
tag: "directory_move",
value: {
old: d.deserialize_string(),
new: d.deserialize_string()
}
};
case 2:
return {
tag: "java_download",
value: {
version: d.deserialize_number(U32_BYTES, false)
}
};
case 3:
return {
tag: "pack_file_download",
value: {
instance_id: d.deserialize_string(),
pack_name: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_version: d.deserialize_string()
}
};
case 4:
return {
tag: "pack_download",
value: {
instance_id: d.deserialize_string(),
pack_name: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "minecraft_download",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 6:
return {
tag: "instance_update",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 7:
return {
tag: "zip_extract",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 8:
return {
tag: "config_change",
value: {
new_path: d.deserialize_string()
}
};
case 9:
return {
tag: "copy_instance",
value: {
import_location: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 10:
return {
tag: "launcher_update",
value: {
version: d.deserialize_string(),
current_version: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_LOADING_PAYLOAD(d) {
return {
event: deserialize_LOADING_BAR_TYPE(d),
loader_uuid: d.deserialize_string(),
fraction: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number_float(U64_BYTES),
message: d.deserialize_string()
};
}
function deserialize_WARNING_PAYLOAD(d) {
return {
message: d.deserialize_string()
};
}
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d) {
return {
instanceId: d.deserialize_string(),
stage: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d),
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false)
};
}
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "resolving_versions"
};
case 1:
return {
tag: "downloading"
};
case 2:
return {
tag: "finishing"
};
default:
throw "variant not implemented"
}
}
function deserialize_COMMAND_PAYLOAD(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "InstallMod",
value: {
id: d.deserialize_string()
}
};
case 1:
return {
tag: "InstallVersion",
value: {
id: d.deserialize_string()
}
};
case 2:
return {
tag: "InstallModpack",
value: {
id: d.deserialize_string()
}
};
case 3:
return {
tag: "InstallServer",
value: {
id: d.deserialize_string()
}
};
case 4:
return {
tag: "LaunchInstance",
value: {
id: d.deserialize_string(),
server: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
singleplayer_world: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "InstallSharedInstanceInvite",
value: {
invite_id: d.deserialize_string()
}
};
case 6:
return {
tag: "RunMRPack",
value: {
path: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_PROCESS_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
uuid: d.deserialize_string(),
event: deserialize_PROCESS_PAYLOAD_TYPE(d),
message: d.deserialize_string()
};
}
function deserialize_PROCESS_PAYLOAD_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "launched"
};
case 1:
return {
tag: "finished"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTANCE_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
event: deserialize_INSTANCE_PAYLOAD_TYPE(d)
};
}
function deserialize_INSTANCE_PAYLOAD_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "created"
};
case 1:
return {
tag: "synced"
};
case 2:
return {
tag: "servers_updated"
};
case 3:
return {
tag: "world_updated",
value: {
world: d.deserialize_string()
}
};
case 4:
return {
tag: "server_joined",
value: {
host: d.deserialize_string(),
port: d.deserialize_number(U16_BYTES, false),
timestamp: d.deserialize_string()
}
};
case 5:
return {
tag: "edited"
};
case 6:
return {
tag: "content_install_finished",
value: {
project_ids: d.deserialize_array(() => d.deserialize_string())
}
};
case 7:
return {
tag: "content_install_failed",
value: {
project_ids: d.deserialize_array(() => d.deserialize_string()),
message: d.deserialize_string()
}
};
case 8:
return {
tag: "removed"
};
default:
throw "variant not implemented"
}
}
function deserialize_FRIEND_PAYLOAD(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "friend_request",
value: {
from: d.deserialize_string()
}
};
case 1:
return {
tag: "user_offline",
value: {
id: d.deserialize_string()
}
};
case 2:
return {
tag: "status_update",
value: {
user_status: deserialize_FRIEND_STATUS_PAYLOAD(d)
}
};
case 3:
return {
tag: "status_sync"
};
default:
throw "variant not implemented"
}
}
function deserialize_FRIEND_STATUS_PAYLOAD(d) {
return {
user_id: d.deserialize_string(),
profile_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
last_update: d.deserialize_string()
};
}
function deserialize_LOG_EVENT(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "log4j",
value: deserialize_LOG4J_EVENT(d)
};
case 1:
return {
tag: "legacy",
value: {
message: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_LOG_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
event: deserialize_LOG_EVENT(d)
};
}
function deserialize_LOG4J_EVENT(d) {
return {
timestamp_millis: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, true),
logger_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
level: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
thread_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
message: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
throwable: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_JOB_SNAPSHOT(d) {
return {
job_id: d.deserialize_string(),
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
kind: deserialize_INSTALL_JOB_KIND(d),
status: deserialize_INSTALL_JOB_STATUS(d),
target: deserialize_INSTALL_TARGET(d),
phase: deserialize_INSTALL_PHASE_ID(d),
progress: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS(d),
details: deserialize_INSTALL_PHASE_DETAILS(d),
display: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_JOB_DISPLAY(d),
error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
rollback_error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
created: d.deserialize_string(),
modified: d.deserialize_string(),
finished: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_JOB_KIND(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "create_instance"
};
case 1:
return {
tag: "create_modpack_instance"
};
case 2:
return {
tag: "create_shared_instance"
};
case 3:
return {
tag: "import_instance"
};
case 4:
return {
tag: "duplicate_instance"
};
case 5:
return {
tag: "install_existing_instance"
};
case 6:
return {
tag: "install_pack_to_existing_instance"
};
case 7:
return {
tag: "update_shared_instance"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JOB_STATUS(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "queued"
};
case 1:
return {
tag: "running"
};
case 2:
return {
tag: "succeeded"
};
case 3:
return {
tag: "failed"
};
case 4:
return {
tag: "interrupted"
};
case 5:
return {
tag: "canceled"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_TARGET(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "new_instance",
value: {
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 1:
return {
tag: "existing_instance",
value: {
instance_id: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_PHASE_ID(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "preparing_instance"
};
case 1:
return {
tag: "resolving_pack"
};
case 2:
return {
tag: "downloading_pack_file"
};
case 3:
return {
tag: "reading_pack_manifest"
};
case 4:
return {
tag: "downloading_content"
};
case 5:
return {
tag: "extracting_overrides"
};
case 6:
return {
tag: "resolving_minecraft"
};
case 7:
return {
tag: "resolving_loader"
};
case 8:
return {
tag: "preparing_java"
};
case 9:
return {
tag: "downloading_minecraft"
};
case 10:
return {
tag: "running_loader_processors"
};
case 11:
return {
tag: "finalizing"
};
case 12:
return {
tag: "rolling_back"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_PROGRESS(d) {
return {
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false),
secondary: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS_SECONDARY(d)
};
}
function deserialize_INSTALL_PROGRESS_SECONDARY(d) {
return {
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false)
};
}
function deserialize_INSTALL_PHASE_DETAILS(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "empty"
};
case 1:
return {
tag: "instance",
value: {
name: d.deserialize_string()
}
};
case 2:
return {
tag: "minecraft",
value: {
game_version: d.deserialize_string(),
loader: deserialize_MOD_LOADER(d)
}
};
case 3:
return {
tag: "java",
value: {
major_version: d.deserialize_number(U32_BYTES, false),
step: deserialize_INSTALL_JAVA_STEP(d)
}
};
case 4:
return {
tag: "modpack",
value: {
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
title: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "import",
value: {
launcher_type: deserialize_IMPORT_LAUNCHER_TYPE(d),
instance_folder: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JAVA_STEP(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "resolving"
};
case 1:
return {
tag: "fetching_metadata"
};
case 2:
return {
tag: "downloading"
};
case 3:
return {
tag: "extracting"
};
case 4:
return {
tag: "validating"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JOB_DISPLAY(d) {
return {
title: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_ERROR_VIEW(d) {
return {
code: d.deserialize_string(),
phase: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PHASE_ID(d),
message: d.deserialize_string(),
reason: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d),
api: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_API_ERROR_DETAILS(d),
context: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_CONTEXT(d)
};
}
function deserialize_INSTALL_API_ERROR_DETAILS(d) {
return {
error: d.deserialize_string(),
status: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U16_BYTES, false),
method: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
url: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
route: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_ERROR_CONTEXT(d) {
return {
operation: d.deserialize_string(),
source_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
target_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
file_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
entry_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
urls: d.deserialize_array(() => d.deserialize_string()),
expected_hash: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
expected_size: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, false),
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
minecraft_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
loader: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
java_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U32_BYTES, false),
os: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
arch: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_IMPORT_LAUNCHER_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "MultiMC"
};
case 1:
return {
tag: "PrismLauncher"
};
case 2:
return {
tag: "ATLauncher"
};
case 3:
return {
tag: "GDLauncher"
};
case 4:
return {
tag: "Curseforge"
};
case 5:
return {
tag: "Unknown"
};
default:
throw "variant not implemented"
}
}
function deserialize_MOD_LOADER(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "vanilla"
};
case 1:
return {
tag: "forge"
};
case 2:
return {
tag: "fabric"
};
case 3:
return {
tag: "quilt"
};
case 4:
return {
tag: "neoforge"
};
default:
throw "variant not implemented"
}
}
function deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "deleted"
};
case 1:
return {
tag: "access_revoked"
};
case 2:
return {
tag: "quarantined"
};
default:
throw "variant not implemented"
}
}
/**
* Deserialize a value from an array of bytes.
* @param {string} type - The type of the value to deserialize.
* @param {Uint8Array} bytes - The byte array to deserialize from.
* @return {Object} The deserialized value and remaining bytes.
*/
function deserialize(type, bytes) {
if (!(typeof type === "string")) {
throw "type must be a string";
}
const d = new Deserializer(bytes);
var return_value = undefined;
switch (type) {
case "AppEvent":
return_value = deserialize_APP_EVENT(d);
break;
case "LoadingBarType":
return_value = deserialize_LOADING_BAR_TYPE(d);
break;
case "LoadingPayload":
return_value = deserialize_LOADING_PAYLOAD(d);
break;
case "WarningPayload":
return_value = deserialize_WARNING_PAYLOAD(d);
break;
case "InstanceBulkUpdateProgressPayload":
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d);
break;
case "InstanceBulkUpdateProgressStage":
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d);
break;
case "CommandPayload":
return_value = deserialize_COMMAND_PAYLOAD(d);
break;
case "ProcessPayload":
return_value = deserialize_PROCESS_PAYLOAD(d);
break;
case "ProcessPayloadType":
return_value = deserialize_PROCESS_PAYLOAD_TYPE(d);
break;
case "InstancePayload":
return_value = deserialize_INSTANCE_PAYLOAD(d);
break;
case "InstancePayloadType":
return_value = deserialize_INSTANCE_PAYLOAD_TYPE(d);
break;
case "FriendPayload":
return_value = deserialize_FRIEND_PAYLOAD(d);
break;
case "FriendStatusPayload":
return_value = deserialize_FRIEND_STATUS_PAYLOAD(d);
break;
case "LogEvent":
return_value = deserialize_LOG_EVENT(d);
break;
case "LogPayload":
return_value = deserialize_LOG_PAYLOAD(d);
break;
case "Log4jEvent":
return_value = deserialize_LOG4J_EVENT(d);
break;
case "InstallJobSnapshot":
return_value = deserialize_INSTALL_JOB_SNAPSHOT(d);
break;
case "InstallJobKind":
return_value = deserialize_INSTALL_JOB_KIND(d);
break;
case "InstallJobStatus":
return_value = deserialize_INSTALL_JOB_STATUS(d);
break;
case "InstallTarget":
return_value = deserialize_INSTALL_TARGET(d);
break;
case "InstallPhaseId":
return_value = deserialize_INSTALL_PHASE_ID(d);
break;
case "InstallProgress":
return_value = deserialize_INSTALL_PROGRESS(d);
break;
case "InstallProgressSecondary":
return_value = deserialize_INSTALL_PROGRESS_SECONDARY(d);
break;
case "InstallPhaseDetails":
return_value = deserialize_INSTALL_PHASE_DETAILS(d);
break;
case "InstallJavaStep":
return_value = deserialize_INSTALL_JAVA_STEP(d);
break;
case "InstallJobDisplay":
return_value = deserialize_INSTALL_JOB_DISPLAY(d);
break;
case "InstallErrorView":
return_value = deserialize_INSTALL_ERROR_VIEW(d);
break;
case "InstallApiErrorDetails":
return_value = deserialize_INSTALL_API_ERROR_DETAILS(d);
break;
case "InstallErrorContext":
return_value = deserialize_INSTALL_ERROR_CONTEXT(d);
break;
case "ImportLauncherType":
return_value = deserialize_IMPORT_LAUNCHER_TYPE(d);
break;
case "ModLoader":
return_value = deserialize_MOD_LOADER(d);
break;
case "SharedInstanceUnavailableReason":
return_value = deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d);
break;
default:
throw "type not implemented";
}
return { value: return_value, bytes: d.release_bytes() };
}
export {
deserialize
};
@@ -0,0 +1,8 @@
{
"name": "postcard",
"description": "Auto generated bindings for postcard format serializing and deserializing javascript to and from bytes.",
"version": "0.0.0",
"main": "index.js",
"types": "index.d.ts",
"type": "module"
}
+21 -7
View File
@@ -1,5 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
export async function init_ads_window(overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', {
@@ -8,8 +7,27 @@ export async function init_ads_window(overrideShown = false) {
})
}
export async function show_ads_window() {
return await invoke('plugin:ads|show_ads_window', { dpr: window.devicePixelRatio })
let adsWindowHoldUpdate = Promise.resolve()
async function update_ads_window_hold(acquire) {
adsWindowHoldUpdate = adsWindowHoldUpdate
.catch(() => {})
.then(() =>
invoke('plugin:ads|update_ads_window_hold', {
acquire,
dpr: window.devicePixelRatio,
}),
)
return await adsWindowHoldUpdate
}
export async function take_ads_window_hold() {
return await update_ads_window_hold(true)
}
export async function release_ads_window_hold() {
return await update_ads_window_hold(false)
}
export async function hide_ads_window(reset) {
@@ -28,10 +46,6 @@ export async function open_ads_consent_preferences() {
return await invoke('plugin:ads|open_ads_consent_preferences')
}
export async function ads_consent_listener(callback) {
return await listen('ads-consent-required', (event) => callback(event.payload))
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
-137
View File
@@ -1,137 +0,0 @@
/*
Event listeners for interacting with the Rust api
These are all async functions that return a promise that resolves to the payload object (whatever Rust is trying to deliver)
*/
/*
callback is a function that takes a single argument, which is the payload object (whatever Rust is trying to deliver)
You can call these to await any kind of emitted signal from Rust, and then do something with the payload object
An example place to put this is at the start of main.js before the state is initialized- that way
you can listen for any emitted signal from Rust and do something with it as the state is being initialized
Example:
import { loading_listener } from '@/helpers/events'
await loading_listener((event) => {
// event.event is the event name (useful if you want to use a single callback fn for multiple event types)
// event.payload is the payload object
console.log(event)
})
Putting that in a script will print any emitted signal from rust
*/
import { listen } from '@tauri-apps/api/event'
/// Payload for the 'loading' event
/*
LoadingPayload {
event: {
type: string, one of "StateInit", "PackDownload", etc
(Optional fields depending on event type)
pack_name: name of the pack
pack_id, optional, the id of the modpack
pack_version, optional, the version of the modpack
instance_name: name of the instance
instance_id: unique identification of the instance
}
loader_uuid: unique identification of the loading bar
fraction: number, (as a fraction of 1, how much we've loaded so far). If null, by convention, loading is finished
message: message to display to the user
}
*/
export async function loading_listener(callback) {
return await listen('loading', (event) => callback(event.payload))
}
/// Payload for the 'process' event
/*
ProcessPayload {
uuid: unique identification of the process in the state (currently identified by PID, but that will change)
pid: process ID
event: event type ("Launched", "Finished")
message: message to display to the user
}
*/
export async function process_listener(callback) {
return await listen('process', (event) => callback(event.payload))
}
/// Payload for the 'instance' event
/*
InstancePayload {
instance_id: unique identification of the instance
event: event type ("Created", "Added", "Edited", "Removed")
}
*/
export async function instance_listener(callback) {
return await listen('instance', (event) => callback(event.payload))
}
/// Payload for the 'instance_bulk_update_progress' event
/*
InstanceBulkUpdateProgress {
instanceId: string
stage: "resolving_versions" | "downloading" | "finishing"
current: number
total: number
}
*/
export async function instance_bulk_update_progress_listener(callback) {
return await listen('instance_bulk_update_progress', (event) => callback(event.payload))
}
export async function install_job_listener(callback) {
return await listen('install_job', (event) => callback(event.payload))
}
/// Payload for the 'command' event
/*
CommandPayload {
event: event type ("InstallMod", "InstallModpack", "InstallVersion"),
id: string id of the mod/modpack/version to install
}
*/
export async function command_listener(callback) {
return await listen('command', (event) => {
callback(event.payload)
})
}
/// Payload for the 'warning' event
/*
WarningPayload {
message: message to display to the user
}
*/
export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload))
}
export async function friend_listener(callback) {
return await listen('friend', (event) => callback(event.payload))
}
export async function notification_listener(callback) {
return await listen('notification', (event) => callback(event.payload))
}
/// Payload for the 'log' event
/*
LogPayload {
instance_id: string,
type: "log4j" | "legacy",
// log4j fields (when type === "log4j"):
timestamp_millis?: number,
logger_name?: string,
level?: string,
thread_name?: string,
message?: string,
throwable?: string,
// legacy fields (when type === "legacy"):
message?: string,
}
*/
export async function log_listener(callback) {
return await listen('log', (event) => callback(event.payload))
}
+24 -125
View File
@@ -1,8 +1,28 @@
import { invoke } from '@tauri-apps/api/core'
import { install_job_listener } from './events'
import type { InstallErrorView } from '@/generated/app-events/InstallErrorView'
import type { InstallJavaStep } from '@/generated/app-events/InstallJavaStep'
import type { InstallJobSnapshot } from '@/generated/app-events/InstallJobSnapshot'
import type { InstallJobStatus } from '@/generated/app-events/InstallJobStatus'
import type { InstallPhaseId } from '@/generated/app-events/InstallPhaseId'
import type { InstallProgress } from '@/generated/app-events/InstallProgress'
import type { InstallProgressSecondary } from '@/generated/app-events/InstallProgressSecondary'
import type { SharedInstanceUnavailableReason } from '@/generated/app-events/SharedInstanceUnavailableReason'
import type { AppEvents } from '@/providers/app-events'
import type { InstanceLink, InstanceLoader } from './types'
export type {
InstallErrorView,
InstallJavaStep,
InstallJobSnapshot,
InstallJobStatus,
InstallPhaseId,
InstallProgress,
InstallProgressSecondary,
SharedInstanceUnavailableReason,
}
export interface PackLocationVersionId {
type: 'fromVersionId'
project_id: string
@@ -104,8 +124,6 @@ export interface SharedInstanceUpdateDiff {
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
@@ -136,116 +154,6 @@ export function getErrorMessage(error: unknown): string {
return 'Unknown error'
}
export type InstallJobStatus =
| 'queued'
| 'running'
| 'succeeded'
| 'failed'
| 'interrupted'
| 'canceled'
export type InstallPhaseId =
| 'preparing_instance'
| 'resolving_pack'
| 'downloading_pack_file'
| 'reading_pack_manifest'
| 'downloading_content'
| 'extracting_overrides'
| 'resolving_minecraft'
| 'resolving_loader'
| 'preparing_java'
| 'downloading_minecraft'
| 'running_loader_processors'
| 'finalizing'
| 'rolling_back'
export interface InstallProgress {
current: number
total: number
secondary?: InstallProgressSecondary | null
}
export interface InstallProgressSecondary {
current: number
total: number
}
export type InstallJavaStep =
| 'resolving'
| 'fetching_metadata'
| 'downloading'
| 'extracting'
| 'validating'
export interface InstallErrorView {
code: string
phase?: InstallPhaseId | null
message: string
reason?: SharedInstanceUnavailableReason | null
api?: {
error: string
status?: number | null
method?: string | null
url?: string | null
route?: string | null
} | null
context?: {
operation: string
source_path?: string | null
target_path?: string | null
file_path?: string | null
entry_path?: string | null
urls?: string[]
expected_hash?: string | null
expected_size?: number | null
project_id?: string | null
version_id?: string | null
minecraft_version?: string | null
loader?: string | null
java_version?: number | null
os?: string | null
arch?: string | null
} | null
}
export interface InstallJobSnapshot {
job_id: string
instance_id?: string | null
kind:
| 'create_instance'
| 'create_modpack_instance'
| 'create_shared_instance'
| 'update_shared_instance'
| 'import_instance'
| 'duplicate_instance'
| 'install_existing_instance'
| 'install_pack_to_existing_instance'
status: InstallJobStatus
target:
| { type: 'new_instance'; instance_id?: string | null }
| { type: 'existing_instance'; instance_id: string }
phase: InstallPhaseId
progress?: InstallProgress | null
details:
| { type: 'empty' }
| { type: 'instance'; name: string }
| { type: 'minecraft'; game_version: string; loader: InstanceLoader }
| { type: 'java'; major_version: number; step: InstallJavaStep }
| {
type: 'modpack'
project_id?: string | null
version_id?: string | null
title?: string | null
}
| { type: 'import'; launcher_type: string; instance_folder: string }
display?: { title: string; icon?: string | null } | null
error?: InstallErrorView | null
rollback_error?: InstallErrorView | null
created: string
modified: string
finished?: string | null
}
export async function install_get_modpack_preview(location: CreatePackLocation) {
return await invoke<InstallModpackPreview>('plugin:install|install_get_modpack_preview', {
location,
@@ -399,7 +307,7 @@ function settleInstallJob(job: InstallJobSnapshot) {
throw new Error(`Install job ${job.job_id} ${job.status}`)
}
export async function wait_for_install_job(jobId: string) {
export async function wait_for_install_job(events: AppEvents, jobId: string) {
const current = await install_job_get(jobId)
if (isInstallJobFinished(current.status)) return settleInstallJob(current)
@@ -434,16 +342,7 @@ export async function wait_for_install_job(jobId: string) {
reject(err)
}
install_job_listener(resolveJob)
.then((listener) => {
if (finished) {
listener()
return
}
unlisten = listener
install_job_get(jobId).then(resolveJob).catch(rejectWait)
})
.catch(rejectWait)
unlisten = events.on('install_job', resolveJob)
install_job_get(jobId).then(resolveJob).catch(rejectWait)
})
}
@@ -1,17 +1,10 @@
import type {
ContentItem,
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '@modrinth/ui'
import type { ContentItem, ManagedContentProject, ManagedContentVersion } from '@modrinth/ui'
import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/instance'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
export type InstanceContentData = {
@@ -21,11 +14,8 @@ export type InstanceContentData = {
}
export type InstanceContentModpackData = {
project: ContentModpackCardProject
version: ContentModpackCardVersion
owner: ContentOwner | null
categories: ContentModpackCardCategory[]
hasUpdate: boolean
project: ManagedContentProject
version: ManagedContentVersion
updateVersionId: string | null
}
@@ -34,19 +24,15 @@ export async function loadInstanceContentData(
cacheBehaviour?: CacheBehaviour,
onError?: (error: Error) => unknown,
): Promise<InstanceContentData> {
const [contentItems, modpackInfo, allCategories] = await Promise.all([
const [contentItems, modpackInfo] = await Promise.all([
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_categories().catch((error) => handleLoadError(error, onError)),
])
return {
path,
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
modpack: normalizeLinkedModpackInfo(
modpackInfo as LinkedModpackInfo | null | undefined,
allCategories as ContentModpackCardCategory[] | null | undefined,
),
modpack: normalizeLinkedModpackInfo(modpackInfo as LinkedModpackInfo | null | undefined),
}
}
@@ -58,7 +44,6 @@ function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
function normalizeLinkedModpackInfo(
modpackInfo: LinkedModpackInfo | null | undefined,
allCategories: ContentModpackCardCategory[] | null | undefined,
): InstanceContentModpackData | null {
if (!modpackInfo) return null
@@ -72,30 +57,6 @@ function normalizeLinkedModpackInfo(
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
},
owner: modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null,
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
hasUpdate: modpackInfo.has_update,
updateVersionId: modpackInfo.update_version_id,
}
}
function resolveLinkedModpackCategories(
modpackInfo: LinkedModpackInfo,
allCategories: ContentModpackCardCategory[] | null | undefined,
) {
if (!allCategories || !modpackInfo.project.categories) return []
const seen = new Set<string>()
return allCategories.filter((category) => {
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
seen.add(category.name)
return true
}
return false
})
}
+46 -6
View File
@@ -5,7 +5,7 @@
*/
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
import type { InstallJobSnapshot, SharedInstanceUpdateDiff } from './install'
import type {
@@ -74,7 +74,15 @@ export async function get_content_items(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_content_items', { instanceId, cacheBehaviour })
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_content_items', {
instanceId,
cacheBehaviour,
})
return adaptContentItems(items)
}
export async function refresh_content_updates(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_refresh_content_updates', { instanceId })
}
// Linked modpack info returned from backend
@@ -107,10 +115,11 @@ export async function get_linked_modpack_content(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_linked_modpack_content', {
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_linked_modpack_content', {
instanceId,
cacheBehaviour,
})
return adaptContentItems(items)
}
// Convert a list of dependencies into ContentItems with rich metadata
@@ -118,9 +127,28 @@ export async function get_dependencies_as_content_items(
dependencies: Labrinth.Versions.v3.Dependency[],
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_dependencies_as_content_items', {
dependencies,
cacheBehaviour,
const items = await invoke<ContentItem[]>(
'plugin:instance|instance_get_dependencies_as_content_items',
{
dependencies,
cacheBehaviour,
},
)
return adaptContentItems(items)
}
function adaptContentItems(items: ContentItem[]): ContentItem[] {
return items.map((item) => {
const embeddedMetadata = item.embedded_metadata
if (!embeddedMetadata?.icon_path) return item
return {
...item,
embedded_metadata: {
...embeddedMetadata,
icon_url: convertFileSrc(embeddedMetadata.icon_path),
},
}
})
}
@@ -260,6 +288,18 @@ export async function toggle_disable_project(
})
}
export async function set_project_locked(
instanceId: string,
projectPath: string,
locked: boolean,
): Promise<void> {
return await invoke('plugin:instance|instance_set_project_locked', {
instanceId,
projectPath,
locked,
})
}
// Remove a project
export async function remove_project(instanceId: string, projectPath: string): Promise<void> {
return await invoke('plugin:instance|instance_remove_project', { instanceId, projectPath })
+6 -16
View File
@@ -3,7 +3,9 @@
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
import { type Channel, invoke } from '@tauri-apps/api/core'
import type { AppEvent } from '@/generated/app-events/AppEvent'
export interface LoadingBarType {
type?: string
@@ -24,24 +26,12 @@ export interface LoadingBar {
bar_type?: LoadingBarType
}
export type OpeningCommandEvent =
| 'RunMRPack'
| 'InstallServer'
| 'InstallVersion'
| 'InstallMod'
| 'InstallModpack'
| string
export interface OpeningCommand {
event: OpeningCommandEvent
id?: string
path?: string
}
export type OpeningCommand = Extract<AppEvent, { type: 'command' }>['payload']
// Initialize the theseus API state
// This should be called during the initializion/opening of the launcher
export async function initialize_state() {
return await invoke<void>('initialize_state')
export async function initialize_state(events: Channel<ArrayBuffer>) {
return await invoke<void>('initialize_state', { events })
}
// Gets active progress bars
+1
View File
@@ -128,6 +128,7 @@ export type ContentSourceKind =
type ContentFile = {
enabled: boolean
locked: boolean
source_kind?: ContentSourceKind | null
metadata?: {
project_id: string
+2 -15
View File
@@ -3,6 +3,7 @@ import { autoToHTML } from '@sfirew/minecraft-motd-parser'
import { invoke } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import type { InstancePayload } from '@/generated/app-events/InstancePayload'
import { get_full_path } from '@/helpers/instance'
import { openPath } from '@/helpers/utils'
@@ -532,18 +533,4 @@ export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVer
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
}
export type InstanceEvent = { instance_id: string } & (
| {
event: 'servers_updated'
}
| {
event: 'world_updated'
world: string
}
| {
event: 'server_joined'
host: string
port: number
timestamp: string
}
)
export type InstanceEvent = InstancePayload
@@ -1073,16 +1073,7 @@
"search.filter.locked.instance.sync": {
"message": "مزامنة مع النسخة"
},
"search.filter.locked.server": {
"message": "يقدمها الخادم"
},
"search.filter.locked.server-environment.title": {
"message": "يمكن إضافة التعديلات **المحليه** فقط إلى نموذج الخادم"
},
"search.filter.locked.server-game-version.title": {
"message": "يتم توفير نسخة اللعبة من قبل الخادم"
},
"search.filter.locked.server-loader.title": {
"message": "يتم توفير المحمّل من قبل الخادم"
}
}
+12 -9
View File
@@ -14,6 +14,9 @@
"app.action-bar.install.copied-details": {
"message": "Zkopírováno"
},
"app.action-bar.install.copy-details": {
"message": "Kopírovat detaily"
},
"app.action-bar.install.dismiss": {
"message": "Zavřít"
},
@@ -26,6 +29,9 @@
"app.action-bar.install.summary.canceled": {
"message": "Zrušeno"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Čištění nebylo dokončeno"
},
"app.action-bar.install.unknown-instance": {
"message": "Neznámá instance"
},
@@ -65,6 +71,9 @@
"app.action-bar.view-logs": {
"message": "Zobrazit logy"
},
"app.ads-consent.title": {
"message": "Vaše soukromí a způsob, jakým reklamy podporují Modrinth"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pokročilé vykreslování"
},
@@ -212,6 +221,9 @@
"app.install.phase.running_loader_processors": {
"message": "Spuštění loader procesů"
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Odstraněno"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Všechna data tvé instance budou trvale smazána, včetně světů, konfigurací a veškerého nainstalovaného obsahu."
},
@@ -953,16 +965,7 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizováno s instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Pouze módy ze strany klientu mohou být přidány na server"
},
"search.filter.locked.server-game-version.title": {
"message": "Verze hry je poskytována serverem"
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
}
}
+30 -21
View File
@@ -179,6 +179,9 @@
"app.auth-servers.unreachable.header": {
"message": "Kan ikke nå autentificeringsservere"
},
"app.behavior-settings.content.title": {
"message": "Hjem og indhold"
},
"app.browse.add-servers-to-instance": {
"message": "Tilføjet server til instance"
},
@@ -200,12 +203,18 @@
"app.browse.back-to-instance": {
"message": "Tilbage til instance"
},
"app.browse.discover-project-type": {
"message": "Udforsk {projectType}"
},
"app.browse.discover-servers": {
"message": "Opdag servere"
},
"app.browse.hide-added-servers": {
"message": "Gem servere som allerede er tilføjet"
},
"app.browse.hide-installed-modpacks": {
"message": "Skjul allerede installeret"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
@@ -245,15 +254,30 @@
"app.install.phase.downloading_minecraft": {
"message": "Downloader Minecraft"
},
"app.install.phase.finalizing": {
"message": "Færdiggører"
},
"app.install.phase.preparing_instance": {
"message": "I kø til at intallere"
},
"app.install.phase.preparing_java": {
"message": "Forbereder Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Downloader Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Udpakker Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Henter Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Forbereder Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Validerer Java {version}"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Tilføjet"
},
@@ -272,18 +296,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Gennemså ændringer"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Gennemgå opdatering"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Gennemgår..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "En opdatering er tilgængelig"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Al' data for din instance vil blive permanent slettet, dette inkludere dine verdener, konfigurationer, og alt installeret indhold."
},
@@ -359,6 +374,12 @@
"app.instance.share.remove-user-modal.effects-label": {
"message": "Hvad vil der ske?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Fjern adgang"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Fjern adgang"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}'s avatar"
},
@@ -440,9 +461,6 @@
"app.modal.install-to-play.report-image-too-large": {
"message": "Filen overskrider 1MiB størrelse begrænsning"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.shared-instance": {
"message": "Delt instance"
},
@@ -1151,16 +1169,7 @@
"search.filter.locked.instance.sync": {
"message": "Synkroniser med instance"
},
"search.filter.locked.server": {
"message": "Givet af serveren"
},
"search.filter.locked.server-environment.title": {
"message": "Kun klient-sided mods kan blive tilføjet til denne server instance"
},
"search.filter.locked.server-game-version.title": {
"message": "Spille version er givet af serveren"
},
"search.filter.locked.server-loader.title": {
"message": "Loader er givet af serveren"
}
}
+33 -33
View File
@@ -210,7 +210,7 @@
"message": "Rechte Seitenleiste ausblenden"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Sicherheitswarnung vor der Installation eines Modrinth-Packs (.mrpack) anzeigen, das nicht auf Modrinth gehostet wird."
"message": "Zeige eine Sicherheitswarnung an, bevor ein Modrinth-Paket (.mrpack) installiert wird, welches nicht auf Modrinth gehostet wird."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Warne mich, bevor unbekannte Modpacks installiert werden"
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Update überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Ein Update ist erforderlich, um {name} zu spielen. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Ein Update ist verfügbar"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, inlusive deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -590,6 +581,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.refreshing": {
"message": "Wird aktualisiert..."
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Server entfernen"
},
@@ -701,15 +695,6 @@
"app.modal.install-to-play.report-reason": {
"message": "Gegen welche Regel verstößt diese Instanz?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Unangemessen"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Bösartig"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Geteilte Instanz melden"
},
@@ -849,7 +834,7 @@
"message": "Startet Instanzen im Vollbildmodus durch Aktualisieren ihrer options.txt-Datei."
},
"app.settings.default-instance-options.fullscreen.title": {
"message": "Vollbildschirm"
"message": "Vollbild"
},
"app.settings.default-instance-options.height.description": {
"message": "Die Höhe des Spielfensters beim Starten"
@@ -981,7 +966,7 @@
"message": "Neues App-Verzeichnis auswählen"
},
"app.settings.resource-management.app-directory.title": {
"message": "App Installation"
"message": "App-Verzeichnis"
},
"app.settings.resource-management.maximum-concurrent-downloads.description": {
"message": "Anzahl der Dateien, die die App auf einmal herunterladen kann. Senke dies, wenn downloads auf deiner Verbindung unzuverlässig sind. Benötigt einen App-Neustart."
@@ -1005,7 +990,7 @@
"message": "Verhalten"
},
"app.settings.tabs.default-instance-options": {
"message": "Standard Spieleinstellungen"
"message": "Standard-Spieloptionen"
},
"app.settings.tabs.java-installations": {
"message": "Java Installationen"
@@ -1188,10 +1173,10 @@
"message": "Das Feedback geht direkt an das Modrinth-Team und wird helfen, zukünftige Updates zu gestalten!"
},
"app.survey.no-thanks": {
"message": "Nein, danke"
"message": "Nein danke"
},
"app.survey.take-survey": {
"message": "Beantworten"
"message": "An Umfrage teilnehmen"
},
"app.survey.title": {
"message": "Hey, Modrinth Nutzer!"
@@ -1226,6 +1211,9 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.user.project.install-to-instance": {
"message": "In Instanz installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -1619,6 +1607,27 @@
"instance.settings.tabs.hooks.title": {
"message": "Spielstart Hooks"
},
"instance.settings.tabs.hooks.variables.description": {
"message": "Hooks werden im Arbeitsverzeichnis der Instanz mit den folgenden Variablen ausgeführt:"
},
"instance.settings.tabs.hooks.variables.inst-dir.description": {
"message": "$INST_DIR: Der absolute Pfad zum Ordner der Instanz"
},
"instance.settings.tabs.hooks.variables.inst-id.description": {
"message": "$INST_ID: Der Name des Ordners der Instanz"
},
"instance.settings.tabs.hooks.variables.inst-java-args.description": {
"message": "$INST_JAVA_ARGS: Die JVM-Argumente, die dem Spiel zur Verfügung gestellt werden"
},
"instance.settings.tabs.hooks.variables.inst-java.description": {
"message": "$INST_JAVA: Der absolute Pfad zur Java-Binärdatei"
},
"instance.settings.tabs.hooks.variables.inst-mc-dir.description": {
"message": "$INST_MC_DIR: Ein Alias für $INST_DIR"
},
"instance.settings.tabs.hooks.variables.inst-name.description": {
"message": "$INST_NAME: Der Name der Instanz"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
@@ -1895,18 +1904,9 @@
"search.filter.locked.instance.sync": {
"message": "Mit Instanz synchronisieren"
},
"search.filter.locked.server": {
"message": "Vom Server bereitgestellt"
},
"search.filter.locked.server-environment.title": {
"message": "Nur Clientseitige Mods können der Serverinstanz hinzugefügt werden"
},
"search.filter.locked.server-game-version.title": {
"message": "Spielversion wird vom Server bereitgestellt"
},
"search.filter.locked.server-loader.title": {
"message": "Loader wird vom Server bereitgestellt"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
+33 -33
View File
@@ -210,7 +210,7 @@
"message": "Rechte Seitenleiste ausblenden"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Sicherheitswarnung vor der Installation eines Modrinth-Packs (.mrpack) anzeigen, das nicht auf Modrinth gehostet wird."
"message": "Zeige eine Sicherheitswarnung an, bevor ein Modrinth-Paket (.mrpack) installiert wird, welches nicht auf Modrinth gehostet wird."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Warne mich, bevor unbekannte Modpacks installiert werden"
@@ -377,18 +377,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Update überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Ein Update ist erforderlich, um {name} zu spielen. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Ein Update ist verfügbar"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -590,6 +581,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.refreshing": {
"message": "Wird aktualisiert..."
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Server entfernen"
},
@@ -701,15 +695,6 @@
"app.modal.install-to-play.report-reason": {
"message": "Gegen welche Regel verstößt diese Instanz?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Unangemessen"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Bösartig"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Geteilte Instanz melden"
},
@@ -849,7 +834,7 @@
"message": "Startet Instanzen im Vollbildmodus durch Aktualisieren ihrer options.txt-Datei."
},
"app.settings.default-instance-options.fullscreen.title": {
"message": "Vollbildschirm"
"message": "Vollbild"
},
"app.settings.default-instance-options.height.description": {
"message": "Die Höhe des Spielfensters beim Starten"
@@ -981,7 +966,7 @@
"message": "Neues App-Verzeichnis auswählen"
},
"app.settings.resource-management.app-directory.title": {
"message": "App Installation"
"message": "App-Verzeichnis"
},
"app.settings.resource-management.maximum-concurrent-downloads.description": {
"message": "Anzahl der Dateien, die die App auf einmal herunterladen kann. Senke dies, wenn downloads auf deiner Verbindung unzuverlässig sind. Benötigt einen App-Neustart."
@@ -1005,7 +990,7 @@
"message": "Verhalten"
},
"app.settings.tabs.default-instance-options": {
"message": "Standard Spieleinstellungen"
"message": "Standard-Spieloptionen"
},
"app.settings.tabs.java-installations": {
"message": "Java-Installationen"
@@ -1188,10 +1173,10 @@
"message": "Das Feedback geht direkt an das Modrinth-Team und wird helfen, zukünftige Updates zu gestalten!"
},
"app.survey.no-thanks": {
"message": "Nein, danke"
"message": "Nein danke"
},
"app.survey.take-survey": {
"message": "Beantworten"
"message": "An Umfrage teilnehmen"
},
"app.survey.title": {
"message": "Hey, Modrinth Nutzer!"
@@ -1226,6 +1211,9 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.user.project.install-to-instance": {
"message": "In Instanz installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -1619,6 +1607,27 @@
"instance.settings.tabs.hooks.title": {
"message": "Start-Hooks"
},
"instance.settings.tabs.hooks.variables.description": {
"message": "Hooks werden im Arbeitsverzeichnis der Instanz mit den folgenden Variablen ausgeführt:"
},
"instance.settings.tabs.hooks.variables.inst-dir.description": {
"message": "$INST_DIR: Der absolute Pfad zum Ordner der Instanz"
},
"instance.settings.tabs.hooks.variables.inst-id.description": {
"message": "$INST_ID: Der Name des Ordners der Instanz"
},
"instance.settings.tabs.hooks.variables.inst-java-args.description": {
"message": "$INST_JAVA_ARGS: Die JVM-Argumente, die dem Spiel zur Verfügung gestellt werden"
},
"instance.settings.tabs.hooks.variables.inst-java.description": {
"message": "$INST_JAVA: Der absolute Pfad zur Java-Binärdatei"
},
"instance.settings.tabs.hooks.variables.inst-mc-dir.description": {
"message": "$INST_MC_DIR: Ein Alias für $INST_DIR"
},
"instance.settings.tabs.hooks.variables.inst-name.description": {
"message": "$INST_NAME: Der Name der Instanz"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
@@ -1895,18 +1904,9 @@
"search.filter.locked.instance.sync": {
"message": "Mit Instanz synchronisieren"
},
"search.filter.locked.server": {
"message": "Vom Server vorgegeben"
},
"search.filter.locked.server-environment.title": {
"message": "Nur clientseitige Mods können der Serverinstanz hinzugefügt werden"
},
"search.filter.locked.server-game-version.title": {
"message": "Spielversion vom Server vorgegeben"
},
"search.filter.locked.server-loader.title": {
"message": "Loader vom Server vorgegeben"
},
"settings.sidebar.label.account": {
"message": "Konto"
},
+44 -29
View File
@@ -8,6 +8,9 @@
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-downloads": {
"message": "Hide active downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Hide more running instances"
},
@@ -110,6 +113,9 @@
"app.action-bar.reload-to-update": {
"message": "Reload to update"
},
"app.action-bar.show-downloads": {
"message": "Show active downloads"
},
"app.action-bar.show-more-running-instances": {
"message": "Show more running instances"
},
@@ -267,7 +273,7 @@
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content."
"message": "Adding content may prevent you from joining this server. Any content you add will be removed when the managed server content is updated."
},
"app.browse.server.installing": {
"message": "Installing"
@@ -281,6 +287,12 @@
"app.export-modal.export-button": {
"message": "Export"
},
"app.export-modal.export-complete": {
"message": "Export complete"
},
"app.export-modal.export-complete-description": {
"message": "{name} was exported successfully."
},
"app.export-modal.header": {
"message": "Export modpack"
},
@@ -296,6 +308,18 @@
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.hosting.update-required.description": {
"message": "You need to update to use Modrinth Hosting through the Modrinth App"
},
"app.hosting.update-required.download": {
"message": "Download to update"
},
"app.hosting.update-required.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.hosting.update-required.title": {
"message": "Modrinth App update required"
},
"app.install.phase.downloading_content": {
"message": "Downloading content"
},
@@ -377,18 +401,9 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Review update"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Reviewing..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "An update is required to play {name}. Please update to latest version to launch the game."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "An update is available"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data for your instance will be permanently deleted, including your worlds, configs, and all installed content."
},
@@ -401,6 +416,12 @@
"app.instance.confirm-delete.header": {
"message": "Delete instance"
},
"app.instance.content.managed-content.modpack-header": {
"message": "Modpack content"
},
"app.instance.content.managed-content.shared-header": {
"message": "Shared content"
},
"app.instance.modpack-already-installed.body": {
"message": "This modpack is already installed in the <bold>{instanceName}</bold> instance. Are you sure you want to duplicate it?"
},
@@ -425,6 +446,9 @@
"app.instance.mods.content-type-project": {
"message": "project"
},
"app.instance.mods.freeze-content": {
"message": "Freeze version"
},
"app.instance.mods.locked-content": {
"message": "Content in locked instances cannot be changed."
},
@@ -443,6 +467,9 @@
"app.instance.mods.successfully-uploaded": {
"message": "Successfully uploaded"
},
"app.instance.mods.unfreeze-content": {
"message": "Unfreeze version"
},
"app.instance.share.empty.description": {
"message": "You can share this instance with your friends!"
},
@@ -686,6 +713,9 @@
"app.modal.install-to-play.invite-warning-with-creator": {
"message": "This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust."
},
"app.modal.install-to-play.managed-content.modpack-header": {
"message": "Modpack content"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
@@ -704,15 +734,6 @@
"app.modal.install-to-play.report-reason": {
"message": "Which rule does this instance violate?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Inappropriate"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Malicious"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Report shared instance"
},
@@ -723,7 +744,7 @@
"message": "For support requests, contact our <support-link>support team</support-link>. For bug reports, open a <github-link>GitHub issue</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "A file is only reviewed if its published to Modrinth, regardless of its file format (including .mrpack)."
"message": "Files that aren't published to Modrinth aren't reviewed."
},
"app.modal.install-to-play.shared-instance": {
"message": "Shared instance"
@@ -1457,6 +1478,9 @@
"instance.files.save-as": {
"message": "Save as..."
},
"instance.last-played": {
"message": "Last played"
},
"instance.locked.delete-button": {
"message": "Delete instance"
},
@@ -1922,18 +1946,9 @@
"search.filter.locked.instance.sync": {
"message": "Sync with instance"
},
"search.filter.locked.server": {
"message": "Provided by the server"
},
"search.filter.locked.server-environment.title": {
"message": "Only client-side mods can be added to the server instance"
},
"search.filter.locked.server-game-version.title": {
"message": "Game version is provided by the server"
},
"search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server"
},
"settings.sidebar.label.account": {
"message": "Account"
},
+428 -41
View File
@@ -174,7 +174,7 @@
"message": "Ocultar etiqueta de nombre"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Muestra los mundos recientes en la sección \"Volver a jugar\" en la página principal."
"message": "Muestra los mundos recientes en la sección \"Volver a jugar\" en la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Volver a jugar mundos"
@@ -186,7 +186,7 @@
"message": "Minimizar app"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa el borde de ventana de tu sistema. Requiere reiniciar la aplicación."
"message": "Usa el borde de ventana de tu sistema. Necesita reiniciar la aplicación."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -225,7 +225,7 @@
"message": "Confirmaciones"
},
"app.behavior-settings.content.title": {
"message": "Página principal y contenido"
"message": "Página de inicio y contenido"
},
"app.behavior-settings.startup-and-navigation.title": {
"message": "Inicio y navegación"
@@ -258,7 +258,7 @@
"message": "Descubrir servidores"
},
"app.browse.hide-added-servers": {
"message": "Ocultar los servidores ya agregados"
"message": "Ocultar los servidores ya añadidos"
},
"app.browse.hide-installed-modpacks": {
"message": "Esconder los ya instalados"
@@ -372,23 +372,14 @@
"message": "Enviar actualización"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Revisar los cambios de contenido que se compartirán con los usuarios de esta instancia."
"message": "Revisa los cambios de contenido que se compartirán con los usuarios de esta instancia."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisar cambios"
},
"app.instance.admonitions.shared-instance.review-update-button": {
"message": "Actualizar"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Actualizando..."
},
"app.instance.admonitions.shared-instance.update-available-body": {
"message": "Se requiere una actualización para jugar a {name}. por favor actualiza a la versión más reciente para abrir el juego."
},
"app.instance.admonitions.shared-instance.update-available-header": {
"message": "Hay una actualización disponible"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Todos los datos de tu instancia se eliminarán permanentemente, incluidos tus mundos, configuraciones y todo el contenido instalado."
},
@@ -483,7 +474,7 @@
"message": "Ningún usuario coincide con tus filtros."
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ya no recibirán actualizaciones para esta instancia compartida"
"message": "Ya no recibirán actualizaciones para esta instancia"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Cualquier copia que tengan instalada se quedará en su dispositivo"
@@ -510,10 +501,10 @@
"message": "Si revocas el acceso de {username} a esta instancia, tendrás que invitarlos otra vez para que sigan recibiendo actualizaciones."
},
"app.instance.share.sign-in.button": {
"message": "Iniciar sesión"
"message": "Inicia sesión"
},
"app.instance.share.unable-to-connect.description": {
"message": "El servicio de instancias compartidas no está disponible, inténtalo otra vez más tarde"
"message": "El servicio de instancias compartidas no está disponible, por favor inténtalo otra vez más tarde"
},
"app.instance.share.unable-to-connect.heading": {
"message": "No se puede conectar"
@@ -590,6 +581,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores ni mundos añadidos"
},
"app.instance.worlds.refreshing": {
"message": "Recargando..."
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Eliminar servidor"
},
@@ -701,15 +695,6 @@
"app.modal.install-to-play.report-reason": {
"message": "¿Qué regla rompe esta instancia?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Inapropiado"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Malicioso"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Reportar instancia compartida"
},
@@ -717,7 +702,7 @@
"message": "Reporte enviado"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Para solicitudes de ayuda, contacte a nuestro <support-link>equipo de soporte</support-link>. Para reportar un bug, abra una <github-link>incidencia (issue) en GitHub</github-link>."
"message": "Para solicitudes de ayuda, contacte a nuestro <support-link>equipo de soporte</support-link>. Para reportar un bug, abra una <github-link>issue en GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Un archivo solo se revisa si se publica en Modrinth, sin importar su formato (incluido el .mrpack)."
@@ -728,6 +713,9 @@
"app.modal.install-to-play.shared-instance-content": {
"message": "Contenido de la instancia compartida"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Esta instancia contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Este modpack para servidor contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
},
@@ -737,6 +725,9 @@
"app.modal.install-to-play.unrecognized-files": {
"message": "Archivos no reconocidos"
},
"app.modal.install-to-play.user-blocked": {
"message": "Usuario bloqueado"
},
"app.modal.install-to-play.view-contents": {
"message": "Ver contenidos"
},
@@ -744,11 +735,56 @@
"message": "Actualizar para jugar"
},
"app.modal.update-to-play.removed": {
"message": "Eliminado"
"message": "Se eliminó"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Esta actualización de modpack para servidor contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Se añadió"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Se eliminó"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Esta actualización de instancia contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
},
"app.modal.update-to-play.update-required": {
"message": "Es necesario actualizar"
},
"app.modal.update-to-play.update-required-description": {
"message": "Se necesita una actualización para jugar en {name}. Por favor actualiza a a la versión más reciente para iniciar el juego."
},
"app.nav.create-new-instance": {
"message": "Crear una instancia nueva"
},
"app.nav.home": {
"message": "Inicio"
},
"app.nav.library": {
"message": "Biblioteca"
},
"app.nav.modrinth-account": {
"message": "Cuenta Modrinth"
},
"app.nav.modrinth-hosting": {
"message": "Modrinth Hosting"
},
"app.nav.sign-in-to-modrinth-account": {
"message": "Inicia sesión con una cuenta Modrinth"
},
"app.nav.signed-in-as": {
"message": "Sesión iniciada como <user>{username}</user>"
},
"app.nav.upgrade-to-modrinth-plus": {
"message": "Mejorar a Modrinth+"
},
"app.news.title": {
"message": "Noticias"
},
"app.news.view-all": {
"message": "Ver todas las noticias"
},
"app.project.install-button.already-installed": {
"message": "Este proyecto ya está instalado"
},
@@ -758,6 +794,9 @@
"app.project.install-context.back-to-browse": {
"message": "Volver al explorador"
},
"app.project.install-context.back-to-instance": {
"message": "Volver a la instancia"
},
"app.project.version.all-versions": {
"message": "Todas las versiones"
},
@@ -770,21 +809,177 @@
"app.project.versions.already-installed": {
"message": "Ya instalado"
},
"app.quick-instance-switcher.drag-show-tooltip": {
"message": "Arrastra para mostrar las instancias recientes"
},
"app.quick-instance-switcher.drag-tooltip": {
"message": "Arrastra para cambiar el tamaño"
},
"app.restarting": {
"message": "Reiniciando..."
},
"app.settings.app-version": {
"message": "App de Modrinth {version}"
},
"app.settings.default-instance-options.environment-variables.description": {
"message": "Variables de entorno puestas al iniciar una instancia."
},
"app.settings.default-instance-options.environment-variables.placeholder": {
"message": "Ingresa variables de entorno..."
},
"app.settings.default-instance-options.environment-variables.title": {
"message": "Variables de entorno"
},
"app.settings.default-instance-options.fullscreen.description": {
"message": "Inicia instancias en pantalla completa al modificar su archivo options.txt."
},
"app.settings.default-instance-options.fullscreen.title": {
"message": "Pantalla completa"
},
"app.settings.default-instance-options.height.description": {
"message": "El largo que tendrá la ventana del juego al iniciar."
},
"app.settings.default-instance-options.height.placeholder": {
"message": "Ingresa la altura..."
},
"app.settings.default-instance-options.height.title": {
"message": "Largo"
},
"app.settings.default-instance-options.java-arguments.description": {
"message": "Los argumentos dados a Java al iniciar una instancia."
},
"app.settings.default-instance-options.java-arguments.placeholder": {
"message": "Ingresa los argumentos de Java..."
},
"app.settings.default-instance-options.java-arguments.title": {
"message": "Argumentos de Java"
},
"app.settings.default-instance-options.memory-allocation.description": {
"message": "La memoria máxima disponible para cada instancia."
},
"app.settings.default-instance-options.memory-allocation.title": {
"message": "Asignación de memoria"
},
"app.settings.default-instance-options.post-exit-hook.description": {
"message": "Se ejecutan tras cerrar el juego."
},
"app.settings.default-instance-options.post-exit-hook.placeholder": {
"message": "Ingresa comandos post-cierre..."
},
"app.settings.default-instance-options.post-exit-hook.title": {
"message": "Comandos post-cierre"
},
"app.settings.default-instance-options.pre-launch-hook.description": {
"message": "Se ejecutan antes de iniciar el juego."
},
"app.settings.default-instance-options.pre-launch-hook.placeholder": {
"message": "Ingresa comandos pre-inicio..."
},
"app.settings.default-instance-options.pre-launch-hook.title": {
"message": "Comandos pre-inicio"
},
"app.settings.default-instance-options.width.description": {
"message": "El ancho que tendrá la ventana del juego al iniciar."
},
"app.settings.default-instance-options.width.placeholder": {
"message": "Ingresa el ancho..."
},
"app.settings.default-instance-options.width.title": {
"message": "Ancho"
},
"app.settings.default-instance-options.wrapper-hook.description": {
"message": "Envuelven al proceso iniciador de Minecraft para añadir funcionalidades o configuraciones."
},
"app.settings.default-instance-options.wrapper-hook.placeholder": {
"message": "Ingresa comandos de envoltura..."
},
"app.settings.default-instance-options.wrapper-hook.title": {
"message": "Comandos de envoltura"
},
"app.settings.developer-mode-button.label": {
"message": "Alternar el modo desarrollador"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
"app.settings.downloading": {
"message": "Descargando v{version}"
},
"app.settings.java-installations.location.title": {
"message": "Directorio de Java {version, number}"
},
"app.settings.operating-system.macos": {
"message": "macOS"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Los anuncios hacen posible a Modrinth y financian los pagos a los creadores. Nuestros socios pueden guardar o acceder a cookies en la aplicación para personalizar anuncios y medir su rendimiento. Puedes rechazar esto o administrar tus preferencias abajo."
},
"app.settings.privacy.discord-rich-presence.description": {
"message": "Mostrar la aplicación de Modrinth como tu actividad actual en Discord. Esto no afecta a cualquier Rich Presence añadida a instancias con mods. Necesita reiniciar la app."
},
"app.settings.privacy.discord-rich-presence.title": {
"message": "Discord Rich Presence"
},
"app.settings.privacy.telemetry.description": {
"message": "Modrinth recolecta datos analíticos anónimos y datos de uso para mejorar la experiencia de nuestros usuarios y para personalizar tu experiencia. Al desactivar esta opción, tus datos dejarán de ser recolectados."
},
"app.settings.privacy.telemetry.title": {
"message": "Telemetría"
},
"app.settings.resource-management.always-show-copy-details.description": {
"message": "Muestra la opción 'Copiar detalles' mientras haya una instalación en cola o instalando. Siempre está disponible para las instalaciones fallidas o interrumpidas."
},
"app.settings.resource-management.always-show-copy-details.title": {
"message": "Siempre mostrar 'Copiar detalles'"
},
"app.settings.resource-management.app-cache.confirm.description": {
"message": "La app cargará más lento hasta que se reconstruya completamente el caché."
},
"app.settings.resource-management.app-cache.confirm.title": {
"message": "¿Limpiar el caché?"
},
"app.settings.resource-management.app-cache.description": {
"message": "Borra todos los datos en caché y los redescarga de Modrinth. La app cargará más lento hasta que se reconstruya completamente el caché."
},
"app.settings.resource-management.app-cache.purge": {
"message": "Limpiar caché"
},
"app.settings.resource-management.app-cache.title": {
"message": "Caché de la aplicación"
},
"app.settings.resource-management.app-database-backups.description": {
"message": "Los respaldos de los datos de aplicación importantes se guardan aquí, en caso de que los necesites luego."
},
"app.settings.resource-management.app-database-backups.open-folder": {
"message": "Abrir la carpeta de respaldos"
},
"app.settings.resource-management.app-database-backups.title": {
"message": "Respaldos de la base de datos de la aplicación"
},
"app.settings.resource-management.app-directory.browse": {
"message": "Buscar un directorio para la aplicación"
},
"app.settings.resource-management.app-directory.description": {
"message": "Aquí es donde la aplicación guardará las instancias y otros archivos. Los cambios entrarán en efecto al reiniciar la app."
},
"app.settings.resource-management.app-directory.select": {
"message": "Seleccionar un directorio nuevo para la app"
},
"app.settings.resource-management.app-directory.title": {
"message": "Directorio de la aplicación"
},
"app.settings.resource-management.maximum-concurrent-downloads.description": {
"message": "El número de archivos que la app puede descargar a la vez. Baja este número si las descargas no son estables con tu conexión. Necesita reiniciar la app."
},
"app.settings.resource-management.maximum-concurrent-downloads.title": {
"message": "Máximas descargas a la vez"
},
"app.settings.resource-management.maximum-concurrent-writes.description": {
"message": "El número de archivos que la app puede escribir en el disco a la vez. Baja este número si frecuentemente encuentras errores de I/O. Necesita reiniciar la app."
},
"app.settings.resource-management.maximum-concurrent-writes.title": {
"message": "Máximas escrituras a la vez"
},
"app.settings.sidebar.label.instances": {
"message": "Instancias"
},
@@ -794,6 +989,9 @@
"app.settings.tabs.behavior": {
"message": "Comportamiento"
},
"app.settings.tabs.default-instance-options": {
"message": "Opciones por defecto del juego"
},
"app.settings.tabs.java-installations": {
"message": "Instalaciones de Java"
},
@@ -968,11 +1166,17 @@
"app.skins.toggle-ears-features-on": {
"message": "Activar"
},
"app.survey.body": {
"message": "¿Te importaría responder unas cuantas preguntas sobre tu experiencia con la aplicación de Modrinth?"
},
"app.survey.footer": {
"message": "¡Tus comentarios llegarán directamente al equipo de Modrinth, y los ayudará a hacer mejores actualizaciones!"
},
"app.survey.no-thanks": {
"message": "No gracias"
"message": "No, gracias"
},
"app.survey.take-survey": {
"message": "Aceptar encuesta"
},
"app.survey.title": {
"message": "¡Hola, usuario de Modrinth!"
@@ -1007,6 +1211,9 @@
"app.update.complete-toast.title": {
"message": "¡La versión {version} se ha instalado correctamente!"
},
"app.user.project.install-to-instance": {
"message": "Instalar a instancia"
},
"app.world.server-modal.placeholder-address": {
"message": "ejemplo.modrinth.gg"
},
@@ -1025,18 +1232,39 @@
"app.world.world-item.players-online": {
"message": "{count} en línea"
},
"content.shared-instance.change-version-body": {
"message": "Cambiar la versión solo cambiará tu copia local. Las actualizaciones futuras a la instancia pueden restablecerla o cambiarla de nuevo."
},
"content.shared-instance.delete-bulk-body": {
"message": "Algunos proyectos seleccionados hacen parte de la instancia compartida. Borrarlos solo cambiará tu copia local, y las actualizaciones futuras a la instancia pueden restablecerlos o cambiarlos de nuevo."
},
"content.shared-instance.delete-button": {
"message": "Borrar de todas formas"
},
"content.shared-instance.delete-many-button": {
"message": "Borrar {count, number} proyectos de todas formas"
},
"content.shared-instance.delete-single-body": {
"message": "Borrarlo solo cambiará tu copia local, y las actualizaciones futuras a la instancia pueden restablecerlo o cambiarlo de nuevo."
},
"content.shared-instance.disable-bulk-body": {
"message": "Algunos proyectos seleccionados hacen parte de la instancia compartida. Desactivarlos solo cambiará tu copia local, y las actualizaciones futuras a la instancia pueden reactivarlos, restablecerlos o cambiarlos de nuevo."
},
"content.shared-instance.disable-button": {
"message": "Desactivar de todas formas"
},
"content.shared-instance.disable-many-button": {
"message": "Desactivar {count, number} proyectos de todas formas"
},
"content.shared-instance.disable-single-body": {
"message": "Desactivarlo solo cambiará tu copia local, y las actualizaciones futuras a la instancia pueden reactivarlo, restablecerlo o cambiarlo de nuevo."
},
"content.shared-instance.unlink-body": {
"message": "Desvincular solo cambiará tu copia local, y las actualizaciones futuras a la instancia pueden restablecerlo o cambiarlo de nuevo."
},
"content.shared-instance.warning-header": {
"message": "Esto es parte de la instancia compartida"
},
"friends.action.add-friend": {
"message": "Añadir un amigo"
},
@@ -1100,6 +1328,66 @@
"friends.sign-in-to-add-friends": {
"message": "<link>¡Inicia sesión en una cuenta de Modrinth</link> para añadir amigos y ver qué están jugando!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Vincular instancia compartida"
},
"installation-settings.shared-instance.title": {
"message": "Despublicar instancia"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Desvincular instancia compartida"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Desconecta esta instancia local de cualquier actualización futura."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Desvinculando..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Despublicar instancia compartida"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Borra esta instancia compartida de Modrinth y deja de enviar actualizaciones a cualquiera que la tenga. Tu instancia local no se verá afectada."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Despublicando..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Esto solo afecta a tu instancia local. Tu contenido instalado se quedará en este dispositivo, y la instancia compartida, junto a todos los que la estén usando, no se verán afectados."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Desvinculando instancia compartida"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Desvincular instancia compartida"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Esto borrará la instancia compartida de los servidores de Modrinth. La gente que esté usando esta instancia dejará de recibir actualizaciones, pero tu instancia local y sus contenidos se quedarán en este dispositivo."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Despublicando instancia compartida"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Despublicar instancia compartida"
},
"instance.action.create-shortcut": {
"message": "Crear acceso directo"
},
"instance.action.export-modpack": {
"message": "Exportar modpack"
},
"instance.action.launch-instance": {
"message": "Iniciar instancia"
},
"instance.action.more-actions": {
"message": "Más acciones"
},
"instance.action.open-folder": {
"message": "Abrir carpeta"
},
"instance.action.repair": {
"message": "Reparar"
},
"instance.action.settings": {
"message": "Ajustes de la instancia"
},
@@ -1178,6 +1466,12 @@
"instance.settings.sharing.active-invites.code": {
"message": "Link de invitación"
},
"instance.settings.sharing.active-invites.description": {
"message": "Cualquiera con alguno de estos links se puede unir mientras el link esté activo."
},
"instance.settings.sharing.active-invites.empty": {
"message": "No hay invitaciones activas."
},
"instance.settings.sharing.active-invites.expires": {
"message": "Expira"
},
@@ -1193,6 +1487,9 @@
"instance.settings.sharing.active-invites.uses": {
"message": "Usos"
},
"instance.settings.sharing.revoke-invite.admonition-body": {
"message": "El link de invitación <monospace>{code}</monospace> dejará de funcionar inmediatamente. Cualquiera que ya se haya unido mantendrá su acceso."
},
"instance.settings.sharing.revoke-invite.admonition-header": {
"message": "Esta acción no se puede deshacer"
},
@@ -1310,6 +1607,27 @@
"instance.settings.tabs.hooks.title": {
"message": "Hooks de inicio del juego"
},
"instance.settings.tabs.hooks.variables.description": {
"message": "Todas estas se ejecutan en el directorio de trabajo de la instancia, con las siguientes variables:"
},
"instance.settings.tabs.hooks.variables.inst-dir.description": {
"message": "$INST_DIR: La ruta absoluta a la carpeta de la instancia"
},
"instance.settings.tabs.hooks.variables.inst-id.description": {
"message": "$INST_ID: El nombre la carpeta de la instancia"
},
"instance.settings.tabs.hooks.variables.inst-java-args.description": {
"message": "$INST_JAVA_ARGS: Los Argumentos JVM dados al juego"
},
"instance.settings.tabs.hooks.variables.inst-java.description": {
"message": "$INST_JAVA: La ruta absoluta a el binario de Java"
},
"instance.settings.tabs.hooks.variables.inst-mc-dir.description": {
"message": "$INST_MC_DIR: Un alias que apunta a $INST_DIR"
},
"instance.settings.tabs.hooks.variables.inst-name.description": {
"message": "$INST_NAME: El nombre de la instancia"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
@@ -1325,6 +1643,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Versión de {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "Los ajustes de instalación no están disponibles mientras esta instancia esté bloqueada."
},
"instance.settings.tabs.java": {
"message": "Java y memoria"
},
@@ -1364,6 +1685,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/de/java"
},
"instance.settings.tabs.sharing": {
"message": "Compartir"
},
"instance.settings.tabs.window": {
"message": "Ventana"
},
@@ -1394,14 +1718,62 @@
"instance.settings.tabs.window.width.enter": {
"message": "Ingresa el ancho..."
},
"instance.shared-instance.error.title": {
"message": "Algo ha salido mal"
},
"instance.shared-instance.network-error.text": {
"message": "No se pudo conectar con la API de las instancias compartidas"
},
"instance.shared-instance.network-error.title": {
"message": "Error de conexión"
},
"instance.shared-instance.owner-tooltip": {
"message": "El contenido de la instancia se está compartiendo a otros usuarios."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Se añadió"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Enviar actualización a los jugadores"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Seleccionar archivos de configuración"
},
"instance.shared-instance.publish-review.description": {
"message": "Revisa los cambios de contenido y escoge cualquier archivo de configuración que quieras incluir en esta actualización."
},
"instance.shared-instance.publish-review.header": {
"message": "Revisar cambios"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Enviar actualización"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Se removió"
"message": "Se eliminó"
},
"instance.shared-instance.tooltip": {
"message": "El contenido de esta instancia está siendo administrado por alguien más."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Tu acceso a la instancia compartida fue revocado. Esta instancia seguirá disponible, pero ya no está vinculada, y no recibirás más actualizaciones."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "La instancia principal fue borrada. Esta instancia seguirá disponible, pero ya no está vinculada, y no recibirás más actualizaciones."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Esta instancia compartida fue bloqueada por el equipo de Moderación de Contenido. Ya no recibirá más actualizaciones de la instancia principal y tampoco se puede jugar."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instancia bloqueada"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "el administrador de la instancia"
},
"instance.shared-instance.unavailable.text": {
"message": "Tu instancia local sigue disponible, pero ya no está vinculada y no recibirá actualizaciones."
},
"instance.shared-instance.unavailable.title": {
"message": "Instancia compartida no disponible"
},
"instance.worlds.a_minecraft_server": {
"message": "Un servidor de Minecraft"
@@ -1466,6 +1838,12 @@
"minecraft-account.sign-in": {
"message": "Inicia sesión en Minecraft"
},
"minecraft-required.description": {
"message": "Necesitas una cuenta de Microsoft con Minecraft comprado para poder iniciar el juego."
},
"minecraft-required.description-header": {
"message": "Inicia sesión en Microsoft"
},
"minecraft-required.dont-have-account": {
"message": "¿No tienes una cuenta?"
},
@@ -1479,13 +1857,28 @@
"message": "Minecraft requerido"
},
"minecraft-required.sign-in": {
"message": "Iniciar sesión con Microsoft"
"message": "Inicia sesión en Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Se abrió una pestaña para que inicies sesión. Completa el proceso ahí, y luego regresa a la app."
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Continúa en el navegador"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Crea una cuenta"
},
"modal.modrinth-account-required.description": {
"message": "Necesitas iniciar sesión con tu cuenta Modrinth antes de que puedas usar esto."
},
"modal.modrinth-account-required.header": {
"message": "Cuenta requerida"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Abrir el navegador otra vez"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Iniciar sesión en Modrinth"
"message": "Inicia sesión en Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Iniciar sesión con una cuenta de Modrinth"
@@ -1496,6 +1889,9 @@
"modal.modrinth-account-required.support-prompt": {
"message": "¿Tienes problemas con al iniciar sesión? <support>Consigue ayuda</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Esperando la confirmación del navegador..."
},
"search.filter.locked.instance": {
"message": "Proporcionado por la instancia"
},
@@ -1508,18 +1904,9 @@
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
},
"search.filter.locked.server": {
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se pueden añadir mods que sean del lado del cliente a la instancia del servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "El loader es proporcionado por el servidor"
},
"settings.sidebar.label.account": {
"message": "Cuenta"
},

Some files were not shown because too many files have changed in this diff Show More