Compare commits

...
Author SHA1 Message Date
coolbot100s 4acf99ce58 Update misused-group.md 2026-07-11 14:00:30 -07:00
coolbot100s 40c4fd801e add quick reply for misused groups + organize 2026-07-11 13:58:37 -07:00
coolbotandGitHub 1741d12b7b Moderation: Update quick replies (#6693)
* Add new externals quick replies

* handle non commercially licensed external content.

* Move quick reply definitions

* fix linebreak issues in quick replies
2026-07-11 02:22:30 +00:00
ThatGravyBoatandGitHub 0b38cecc74 fix: keybind structuredClone only working the first time (#6691) 2026-07-11 02:22:05 +00:00
ThatGravyBoatandGitHub 8680e61883 chore: misc moderation changes (#6690)
* fix: tech card not updating project status correctly

* feat: pull out quick actions into buttons for reports

* fix: reports duplicating in the reports queue

* feat: allow for moderation keybinds to be rebindable

* run prepr
2026-07-10 20:25:39 +00:00
Prospector ea86db450b changelog 2026-07-10 10:43:00 -07:00
ProspectorandGitHub bbdaa171e3 fix: remove unused facets from documentation (#6685) 2026-07-10 10:37:48 -07:00
ProspectorandGitHub af8daca059 fix: make search revalidate & change cache to be 10 minutes (#6682)
* fix: make search revalidate & change cache to be 10 minutes

* prepr
2026-07-10 16:58:45 +00:00
Truman GaoandGitHub 2434f0c45b fix: ads consent close button color (#6678)
* fix: ads consent close button

* update color
2026-07-10 16:57:56 +00:00
332dfe1074 Fix: fix passkey translation keys (#6512)
* fix: common message mentioning passkey

* devex: split title and header translation keys

* chore: rerun pnpm prepr:frontend

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-10 16:35:59 +00:00
Calum H.andGitHub c07727aa49 feat: install flow improvements (#6669)
* feat: better error handling + copy details btn for info

* refactor: clean up error handling into diagnostics

* feat: extra info for failure states + queuing properly

* fix: cleanup

* fix: lint

* fix: fmt

* fix: cleanup

* fix: cleanup

* fix: lint

* feat: use bon builder
2026-07-10 11:54:56 +00:00
aecsocketandGitHub 57a977f7f3 feat: use v1 version manifest for Quilt (#6650)
* wip: app-side logic

* lints

* fmt

* fmt

* Split Daedalus changes out of frontend branch

* Remove Daedalus changes from frontend branch
2026-07-10 11:17:41 +00:00
NitrrineandGitHub 323d088ebd fix: light mode icon shows in dark mode (#6614)
* fix: light mode icon shows in dark mode

* feat: use os icons from assets repo

* revert initial changes

* fix: lint
2026-07-09 21:43:13 +00:00
Truman GaoandGitHub 46f94bd067 fix: download modal no compatible empty state (#6680) 2026-07-09 21:37:09 +00:00
aecsocketandGitHub e877167db7 feat: global trace database frontend + improvements to tech review status sync (#6671)
* wip

* ui edits

* better logic for syncing tech review state in a single centralized place

* never automatically finish a tech review report

* fix css

* fix clippy

* prepr
2026-07-09 16:09:02 +00:00
Prospector 623b51e6ea update ads.txt 2026-07-09 08:37:23 -07:00
Prospector 5face9e56a changelog 2026-07-08 15:58:31 -07:00
40aff1e8bb feat: download modal changes (#6652)
* update download with modrinth app button

* feat: implement compatible version card and update download dependency buttons

* fix: download multiple files immediately triggers blocking

* refactor: implement download modal provider

* fix: duplicate dependencies and style update for dependecy section

* feat: improve loading flow when there are dependencies, to not show compatible version until all data is loaded

* fix: fallback to download compatible version if dependency lookup fails

* feat: implement selecting compatible versions when there are multiple

* fix: update dependencies version table to any compatible version

* fix: version showing before depedencies loaded

* feat: add platform options sort order

* pnpm prepr

* feat: keep previous result showing until new one loads in when change compatibility options

* feat: implement better handling for showing required/recommended resource packs with additional files

* pnpm prepr

* fix: required resource pack not showing footer actions

* fix: download with deps is downloading all files

* feat: add joined button for downloading with recommended

* feat: add admontion for dependency that has resource pack to download

* pnpm prepr

* fix: lint

* feat: append "-EXTRACT_ME.zip" to zipped project with dependencies

* fix: date published capitalization

* pnpm prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-08 22:54:23 +00:00
SychicandGitHub 5f8837604d feat(labrinth): shared instance icon (#6665) 2026-07-08 22:45:15 +00:00
ProspectorandGitHub 795dad040f feat: add complementary installer source support (#6667) 2026-07-08 15:44:59 -07:00
Truman GaoandGitHub 9ca181dc1a fix: privacy popup in app bounded to tiny webview (#6657) 2026-07-08 16:29:41 +00:00
aecsocketandGitHub c84b658e41 feat: tech review global trace database (#6648)
* global detail verdicts system

* global traces frontend

* split global routes into own mod

* tweak migration to only do no-key check at app level

* prepr
2026-07-08 16:06:45 +00:00
Prospector 9006dce2b0 changelog 2026-07-08 08:06:13 -07:00
ProspectorandGitHub efdfb51149 fix: add DEI (downloads, equality, and inequality) (#6664)
fix: add DEI
2026-07-08 15:04:21 +00:00
ProspectorandGitHub 79cc5fef72 feat: exclude project types filter (#6660)
* feat: exclude project types filter

* fix merge issue
2026-07-08 14:30:47 +00:00
Truman GaoandGitHub ddefc28ca6 fix: instance edits not emitting edit event to reload state (#6658) 2026-07-08 09:16:32 +00:00
654757f9fb docs: replace broken loc count with static count (#6615)
* docs: replace broken loc count with static count

Signed-off-by: Nitrrine <43351072+Nitrrine@users.noreply.github.com>

* feat: update to use loctopus api

credits to @creeperkatze <3

Signed-off-by: Nitrrine <43351072+Nitrrine@users.noreply.github.com>

---------

Signed-off-by: Nitrrine <43351072+Nitrrine@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-08 05:21:47 +00:00
François-Xavier TalbotandGitHub 407641ee7c fix(labrinth): serde(default) new search document fields (#6656) 2026-07-07 15:28:54 -07:00
ProspectorandGitHub 96ab120cd0 feat: add all_project_types to search results (#6654)
* feat: add all_project_types to search results

* update docs
2026-07-07 21:30:42 +00:00
François-Xavier TalbotandGitHub 3ed75a146a fix(labrinth): fix routing config for /v3/content (#6649) 2026-07-07 16:48:14 +00:00
aecsocketandGitHub 5a1c2a46bc Generate different Daedalus Quilt templates for 26.x/non-26.x (#6554)
* Generate different Daedalus Quilt templates for 26.x/non-26.x

* add docs

* wip: daedalus version groups

* wip: app-side logic

* finish app-side logic, fix certain libs not included

* lints

* fmt

* fmt

* Split frontend changes out of Daedalus branch
2026-07-07 16:45:33 +00:00
ProspectorandGitHub df8ebbd3e0 feat: add /build route (#6644) 2026-07-07 15:43:26 +00:00
François-Xavier TalbotandGitHub 517c3d2d72 fix(labrinth): fix prefix stripping in mrpack scanning (#6646) 2026-07-07 01:31:07 +00:00
Prospector 7f15772f59 changelog 2026-07-06 15:19:29 -07:00
ProspectorandGitHub 0905ea72f6 fix: project page in app not receiving instance context & port version page to app (#6618)
* fix: project page in app not receiving instance context

* prepr

* feat: port version page to app

* commonize keys

* whoops bring those back
2026-07-06 15:17:25 -07:00
8d20fd82db fix: moderation/projects endpoint slow (#6639)
* fix: moderation/projects endpoint slow

* fix: re-add tech rev toggle off by default

* fix: i18n

* prepare

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-07-06 19:16:52 +00:00
aecsocketandGitHub c831e38e32 fix: file scans only consider published files as on-platform (#6638) 2026-07-06 18:13:21 +00:00
Calum H.andGitHub 40b8fb3a4a fix: use tanstack for server pinging (#6631) 2026-07-06 18:09:51 +00:00
François-Xavier TalbotandGitHub 977bb2ff58 ci: fix (#6640)
* ci: use apt

* fix(ci): sudo

* bruhj
2026-07-06 17:51:09 +00:00
Calum H.andGitHub 732bdfc79b fix: warm middleware on server-side (#6636) 2026-07-06 17:39:43 +00:00
Calum H.andGitHub 1cce1eb248 fix: strip runtime filters to be authoratative (#6635) 2026-07-06 17:39:17 +00:00
Calum H.andGitHub 76320f227f fix: dont prefetch file contents if it's not editable/viewable (#6632) 2026-07-06 17:06:41 +00:00
Calum H.andGitHub 34228a5b6c fix: use next tick in delete confirm (#6633) 2026-07-06 17:06:10 +00:00
Calum H.andGitHub 6d66aee4ec fix: properly select correct databack versions (#6634) 2026-07-06 16:59:49 +00:00
SychicandGitHub fc7be043c7 fix(labrinth): add invited_by field (#6625) 2026-07-06 16:33:00 +00:00
7fa88e5d4d feat: moderation modpack scan button (#6627)
* feat: moderation modpack scan button

* fix: ui

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-07-06 13:19:39 +00:00
ThatGravyBoatandGitHub 21508e0637 feat: move tech quick actions to dedicated buttons (#6628) 2026-07-06 08:31:51 +00:00
Prospector ad4912d64d changelog + one more fix 2026-07-04 19:09:58 -07:00
ProspectorandGitHub 0002ae2479 fix: env filter showing in hosting search (#6612) 2026-07-04 18:47:30 -07:00
Prospector e4568330a7 changelog 2026-07-04 16:16:02 -07:00
ProspectorandGitHub b0108e0624 fix: environment filters (#6611) 2026-07-04 16:15:28 -07:00
Prospector ac63abadb7 changelog 2026-07-04 16:00:11 -07:00
fa749b5aff fix: respect height attribute on images in markdown body (#6506)
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-07-04 22:47:17 +00:00
a2adbe18c4 Request: Add cdn/workflow.serilum.com to allowedHostnames whitelist to bypass wsrv.nl caching (#6567)
* Add cdn/workflow.serilum.com to allowedHostnames whitelist

Signed-off-by: Rick South <rick@natamus.com>

* Update parse.ts

Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>

---------

Signed-off-by: Rick South <rick@natamus.com>
Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-04 22:40:34 +00:00
Truman GaoandGitHub 0fcbe51816 fix: version table overflow issues (#6583)
* feat: shorten ModLoader

* fix: game version and platform tag overflow issues

* feat: add table version row below and move version files to version row below

* fix: platform plural

* pnpm prepr
2026-07-04 22:06:09 +00:00
Truman GaoandGitHub 23cfeca91d feat: project download modal (#6602)
* refactor: componentize project download modal

* feat: implement new project download modal

* fix: button trigger placeholder text styles

* feat: dependency download button

* refactor: move project download modal into folder

* refactor: separate out download dependencies section

* refactor: split out download project and install with modrinth app sections

* feat+refactor: add nested dependencies route and move logic into related subcomponents

* feat: handle download project dependencies recursively and properly pass all game versions/platforms compatible on the selected version

* feat: if only dependency project is listed, find latest compatible dependency version to download in version page

* refactor: use ts and also have modal own get project/versions queries

* feat: implement opening project download modal in place in version page

* feat: implement incompatible options due to base project

* feat: auto select loader after selecting a game version, if only one loader is left compatible

* feat: add prop to stop route selection being persisted in url param

* pnpm prepr

* feat: update tooltip copy

* feat: polish some styles

* fix: fabric dependency showing when quilt is selected

* fix: tooltip hover area

* fix: when there are multiple compatible of same game version + loader, give latest

* feat: add link to version page

* feat: add truncated tooltip

* pnpm prepr

* feat: add warning icon for incompatible selection

* feat: polish loading state and tooltip

* feat: implement download all dependencies

* pnpm prepr

* fix: light theme card

* fix: additional files label

* refactor: move additional files into download dependencies section

* feat: move download all button in heading with compatible version

* fix color

* fix: dependencies not appearing when open modal second time

* fix: dependency lines when 3 nested

* feat: remove duplicated dependencies

* feat: add download count indicateor on download all button

* feat: add tooltip for downloading file with filename and size

* pnpm prepr

* feat: only show duplicate tooltip if there are duplicates
2026-07-04 21:45:34 +00:00
Truman GaoandGitHub db830ef65c feat: prevent .mrpack being uploaded as additional file (#6582) 2026-07-04 21:35:08 +00:00
ArthurandGitHub 08f44b3582 fix: make update pill button size to content clean (#6574)
* Fix update pill sizing

* Refactor as component
2026-07-04 21:33:36 +00:00
ArthurandGitHub 8ffc4e2599 chore: add modfolio to allowed hostnames (#6584)
Add modfolio to allowed hostnames
2026-07-04 21:33:24 +00:00
coolbotandGitHub 58a5619073 add alt keybind for resetting moderation progress. (#6606) 2026-07-04 21:28:08 +00:00
4a6fa9fc3d feat: better api docs (#6586)
* feat: docs

* fix tombi

* chore: fix some of the routes rendering with missing /

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

* better hash examples, move openapi stuff to openapi.rs

* more utoipa param fixes

* request body docs

* chore: remove moderation route from v2, remove ingest & webhooks from v3 spec

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-07-04 14:19:21 +00:00
aecsocketandGitHub b26d048a63 Remove AutoMod code (#6604)
* 🦀 automod is gone 🦀

* prepare
2026-07-03 21:32:11 +00:00
François-Xavier TalbotandGitHub d5be2bd739 fix(labrinth): include SharedInstanceInvite notifications for ws (#6603)
* fix(labrinth): include SharedInstanceInvite notifications for ws

* chore: fmt
2026-07-03 17:06:58 +00:00
Prospector 0dac5684af changelog 2026-07-03 08:16:29 -07:00
coolbotandGitHub ca0b8d5ca4 add excessive languages warning nag (#6587) 2026-07-03 12:12:07 +00:00
coolbotandGitHub b6651efe52 Moderation Fix: Handle more license cases (#6590)
Apply correct behavior in edge cases
2026-07-02 17:26:10 +00:00
aecsocketandGitHub eb28121c0a fix: scan logic file whitelist + rescan route (#6585)
* explicitly only scan jar/zip/disabled files for attributions

* add route to force scan file

* prepare
2026-07-02 16:33:16 +00:00
SychicandGitHub a5ebc85356 refactor(labrinth): combine logic for external notification endpoints (#6578)
* refactor(labrinth): combine logic for external notification endpoints

also introduce new notification that doesn't send an email

* style(labrinth): derive default

* feat(labrinth): return more detailed failure information for external
notifications
2026-07-02 15:44:47 +00:00
Prospector c530671be1 fix collection widget on mobile 2026-07-01 18:09:12 -07:00
Prospector bb6d8a6fc4 changelog 2026-07-01 17:44:29 -07:00
ProspectorandGitHub 256e8f946a chore: pride month fundraiser conclusion (#6588)
* pride end blog post

* Fix drago link

* prepr oops
2026-07-01 17:43:44 -07:00
Calum H.andGitHub 7e5590b8b9 fix: improve observability during fetch failure (#6555)
* fix: improve observability during fetch failure

+ remove analytics log spam

* fix: rev comments
2026-07-01 15:58:20 +00:00
SychicandGitHub d20f05fb04 feat(labrinth): instance share notification (#6581) 2026-07-01 14:21:56 +00:00
Truman GaoandGitHub a00a7971d1 feat: add tooltip for unknown dependents (#6572) 2026-07-01 05:21:42 +00:00
aecsocketandGitHub 26e98b7155 fix: change Tiltify campaign errors to quieter log level v2 (#6576)
import
2026-06-30 21:36:47 +02:00
aecsocketandGitHub 77b06f679e fix: change Tiltify campaign errors to quieter log level (#6575) 2026-06-30 21:34:37 +02:00
Truman GaoandGitHub 50e9472dd9 fix: cannot set owner for creating projects (#6573) 2026-06-30 17:00:10 +00:00
ThatGravyBoatandGitHub b569354793 fix: packs not being validated correctly (#6533) 2026-06-30 14:02:58 +00:00
Michael H. b667b3ae9e fix: remove sccache s3 2026-06-30 11:41:08 +02:00
coolbotandGitHub 16de6182e4 Add nag for custom license info. (#6564)
* add nag for custom license info

* preppr
2026-06-30 02:25:13 +00:00
Calum H.andGitHub 364fed52dd feat: changelog (#6563) 2026-06-30 01:57:11 +02:00
Calum H.andGitHub 0fe695721a fix: try fix app freezing on instance page load (#6562)
* fix: app freezing fix

* fix: fmt
2026-06-30 01:55:55 +02:00
Calum H.andGitHub 692f22b749 feat: changelog 0.15.3 (#6560)
feat: changlog 0.15.3
2026-06-30 00:54:16 +02:00
Calum H.andGitHub ed7792097d fix: better handling of larger instances for ExportModal (#6559) 2026-06-30 00:51:25 +02:00
Prospector c56ce6c9ee changelog 2026-06-29 13:45:04 -07:00
Calum H.andGitHub 91bc58b8ee fix: cleanup stale db entry on rename instance file (#6549)
fix: race on renaming instance file
2026-06-29 17:36:46 +00:00
Truman GaoandGitHub 38d757b66a fix: analytics colour pick is not excluding unknown (#6552)
* fix: analytics colour picking not excluding unknown

* feat: improve colours, better accessibility
2026-06-29 17:17:03 +00:00
Calum H.andGitHub 862b54f00d refactor: instance export modal (#6543)
* refactor: export modal w the design system

* fix: lint

* fix: rev

* fix: lint

* fix: qa
2026-06-29 17:01:00 +00:00
Calum H.andGitHub c2f4835fe7 fix: spacing + friends (#6550) 2026-06-29 17:00:59 +00:00
ThatGravyBoatandGitHub b97c191d75 fix(docs): create version endpoint missing file_types field (#6551) 2026-06-29 16:22:08 +00:00
Truman GaoandGitHub b0c6a06909 feat: update versions table (#6502)
* feat: add new versions table, move environments column to be feature flag toggled, and add header/cell class props to Table.vue

* feat: implement much better table column widths

* feat: update version filter control to use multiselect

* feat: implement clicking table row to go to version

* feat: mobile pass on versions table

* feat: use multiselect for platform, and fix game version show all versions when dont exist

* pnpm prepr

* feat: replace dashboard projects table with new table

* feat: update projects so its out of card

* fix: version actions buttons column width

* feat: implement overflow on loaders cell

* update platform and version width

* feat: update project icon

* feat: change edit links to use floating action bar

* pnpm prepr
2026-06-29 15:01:07 +00:00
ThatGravyBoatandGitHub fc3e384f54 fix: tech review card not refreshing on status change (#6508)
fix: tech rev card not refreshing on status change
2026-06-29 14:40:20 +00:00
Calum H.andGitHub 87ff8fd4d9 fix: server panel not using correct endpoint for mrpack reupload (#6539)
fix: server panel not using correct endpoint for local modpack reupload
2026-06-29 14:39:03 +00:00
Calum H.andGitHub 288c1fb970 fix: reduce ping timeout in app to 5 sec not 30 sec (#6545)
* fix: reduce ping timeout in app to 5 sec not 30 sec

* feat: readd comment
2026-06-29 14:38:51 +00:00
Calum H.andGitHub f6e6ac28d4 fix: change fs watcher to rely more on instance ids than path (#6547)
* fix: change fs watcher to rely more on instance ids than path

* fix: fmt
2026-06-29 14:38:44 +00:00
Calum H.andGitHub 4a972bca73 fix: change logic when selecting filters in Worlds.vue (#6548) 2026-06-29 14:38:37 +00:00
Calum H.andGitHub b09c26f616 fix: console sizing issues (#6540)
fix: console sizing issues (for good)
2026-06-29 14:38:30 +00:00
ArthurandGitHub 204bd52a5a fix: modpack content modal toggle state (#6532)
Fix modpack content modal toggle state
2026-06-29 14:04:55 +00:00
Calum H.andGitHub b59f327b04 feat: add chaos cubed skins (#6463) 2026-06-29 12:18:15 +00:00
Modrinth BotandGitHub 89e643fa50 New translations from Crowdin (main) (#6542) 2026-06-29 12:04:10 +00:00
Calum H.andGitHub cee25fdbbc fix: unlinking local mrpack does not show content afterwards (#6541) 2026-06-29 11:58:43 +00:00
Calum H.andGitHub b95a2d875a feat: changelog app 0.15.1 (#6522) 2026-06-26 16:42:19 +00:00
Calum H.andGitHub 1067b02830 fix: logs using id not path (#6511)
* fix: logs using id not path

* fix: lint
2026-06-26 17:50:26 +02:00
Calum H.andGitHub ebf4a6ef5f fix: bulk update updating linked content (#6517)
* fix: bulk update updating linked content

* fix: disabled content sometimes desyncing

* fix: bad filter

* fix: fmt
2026-06-26 17:50:15 +02:00
Calum H.andGitHub e4376c2b15 fix: clamp negative playtime (#6519)
* fix: clamp negative playtime

* fix: remove triggers use instant
2026-06-26 17:50:03 +02:00
Michael H. 5fe07de187 chore: bump mold and add mold to other jobs 2026-06-26 17:22:46 +02:00
Prospector 80d66d3c6e update ads.txt 2026-06-25 21:13:42 -07:00
Calum H.andGitHub 64f7fb811e feat: changelog 25th june (#6507) 2026-06-25 23:25:07 +02:00
734720e11e feat: instances v2 (#6431)
* feat: base of instances v2

* feat: use old profiles with compat layer

* prototype: instances v2

* fix: install_from using profile

* fix: skins migration fix

* fix: frontend still using profile path

* fix: add update proj multiselect guard

* fix: cargo fmt

* fix: content missing fields

* feat: break up app-lib/api/instance.rs

* fix: check_content_updates mismatch

* fix: updater modal cleanup w/new structure

* feat: better update all handling

* fix: remove preview_update_all

* fix: feedback on bulk update + lint

* fix: rem transitions

* fix: change to jsonb

* feat: app db backup after update

* fix: lint

* fix: sqlx prepare + use sqlx macros

* fix: lint

* fix: bugs

* feat: defuck the installing process up

* fix: bug of hell

* fix: shear

* fix: fmt

* fix: install progress spacing + change mc/content/overrides to bytes

* fix: lint

* fix: prepr

* fix: navtabs anim not working in app

* fix: worlds.vue improvements + browse page fixes

* feat: optimise queries + adapter fns

* fix: lint

* fix: lint

* feat: shared modrinth-content-management crate (#6469)

* feat: disable warnings setting

* feat: add instances shortcuts (#6329)

* Add modrinth://launch deep link to start a profile

Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck.

* Change route to /launch/profile/{id} for future extensibility

* fix: ensure profile path is url decoded

* fix: URL-decode profile path from deep link

* fix: use urlencoding crate for URL decoding

* feat: implement app instance shortcuts

* feat: change windows shortcut creation to use windows api instead

* feat: implement creating a shortcut launching world/server

* format

* fmt

* fix multiline inline tables

* pnpm prepr

* feat: move create shortcut to last item

* refactor: split up shortcuts.rs for individual platforms

* refactor: turn profile launch url into url type

* use string literal and add safety comment

* pt2

* refactor: rename anything that's profile into instance

* update mac shortcut

---------

Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>

---------

Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
2026-06-25 21:19:29 +00:00
ef4044534f feat: update auth flow (#5790)
* Backend routes for choosing username in OAuth flow

* fix up oauth flow routes

* improve URL-related OAuth code

* Use user-provided callback addr instead of SELF_ADDR

* Revert "Use user-provided callback addr instead of SELF_ADDR"

This reverts commit 7ea0635d86.

* fix flow

* fix: backend response for create oauth account

* feat: new auth flow (#5840)

* update auth with new designs

* refactor: auth.js to auth.ts

* refactor: componentize auth pages

* fix: auth pages height

* feat: initial implementation of new sign-in oauth

* fix create account flow

* fix checkbox

* remove hard coded username

* implement create user validation endpoint and add more specific error responses

* feat: implement under 13 DOB guard and email/password validation route

* fix: TOCTOU issue

* refactor: pnpm prepr

* fix: make sure staging uses staging

* fix: hcaptcha styles

* fix: copy

* remove: auth/welcome page as its no longer used

* refactor: bring root page card styles into individual components and use tailwind

* fix: account settings modals to use new modal and fix lots of bad styles

* refactor: pnpm prepr

* feat: implement last signed in indicator

* fix: append number when generated name from email is taken

* refactor: pnpm prepr

* fix: last sign in badge color

* fix: qa issues

* refactor: pnpm prepr

* fix: hover effect on native date picker

* chore: temp staging undo

* Revert "chore: temp staging undo"

This reverts commit cad6bd4f92.

* feat: handle app create account

* fix: last signed in style

* fix: add initOnMounted for SSR race

* refactor: use typescript

* refactor: pnpm prepr

* refactor: use typescript for reset-password

* refactor: convert verify-email to use typescript

* refactor: convert authorize.vue to use typescript

* fix: authorize.vue error states

* feat: small style updates

* feat: implement date picker component

* feat: improve UX and styles for range select

* refactor: pnpm prepr

* fix: range select border styles

* feat: implement date picker component in create account

* feat: implement preserve date for date picker

* update rust toolchain

* increase recursion limit

* fix: date picker can be null

* fix: calculate age based on user's timezone

* fix: number input icons color

* fix: date picker icons

* feat: improve styles

* fix: add width on date

* fix: hover color bad on number input

* fix lints

* feat: add default date open view

* fmt

* fix: account.vue

* fix: remove default date to open 13 years ago

* fix: edit copy on info banner

* fix: cannot hover over project card tooltip items (#6071)

fix: cannot hover over project cards

* feat: improve add dependency flow (#6075)

* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

* feat: update dependency rows to use version number and update styles

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search

* fix: open modrinth project links in the app (#6072)

* pin tanstack versions + set pnpm min age to 7 days

* squash commits

* fix: 2 factor auth enter code screen styles

* update copy

* update copy

* improve reset password

* feat: update sign in screen

* fix: unused import

* Merge branch 'main' into boris/dev-908-backend-changes

* Revert "Merge branch 'main' into boris/dev-908-backend-changes"

This reverts commit b9b03796e3.

* fix: add stroke

* feat: add passkey support (#6375)

* feat: add passkey backend

* feat: passkey frontend

* invalidate sessions on compromised passkey

* chore: run sqlx prepare

* fix: make passkey button use both collumns to prevent empty space

* fix: correctly verify max passkeys in finish route

* fix: use structs for response

* fix: add rp name default

* style: use web::Json

* fmt

* feat: improve manage passkeys UI

* fix copy

* pnpm prepr

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Michael H. <michael@iptables.sh>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: DeDiamondPro <67508414+DeDiamondPro@users.noreply.github.com>
2026-06-25 20:36:19 +00:00
aecsocketandGitHub 6fc741f7c0 fix: file scan fix v2 (#6504)
* fix: file attribution scanning not marking completed files

* prepare

* Handle file scan failures gracefully with tracking

* conditional

* add more logging

* remove leftover files
2026-06-25 20:50:04 +02:00
aecsocketandGitHub 527757d158 fix: file attribution scanning not marking completed files (#6500)
* fix: file attribution scanning not marking completed files

* Clean up unused attribution files/groups on deletion

* prepare

* Handle file scan failures gracefully with tracking
2026-06-25 15:57:51 +00:00
aecsocketandGitHub f4a2999b20 Batch pending file scanning ops (#6491)
* Batch pending file scanning ops

* prepare

* move some stuff out of the txn

* scan concurrency

* prepr

* 🦀 automod attribution is dead, long live automod

* fix ci

* update scan file filtering
2026-06-24 21:46:07 +00:00
aecsocketandGitHub 586cdc90f9 fix: word separators in Typesense search fields regression (#6492) 2026-06-24 18:09:18 +00:00
Truman GaoandGitHub e56f3ae18a fix: package datapack as mod (#6494)
* feat: update package as mod copy

* fix: add environment field for v3

* pnpm prepr

* feat: use coloured tags for loaders in multiselect
2026-06-24 18:08:33 +00:00
aecsocketandGitHub c392c85917 perf: Attribution route perf improvements (#6489)
* Attribution route perf improvements

* fix clippy

* clear exemptions when user attempts to scan their file

* Increase Redis TTL/refresh time
2026-06-24 17:48:35 +02:00
Prospector 29110890e1 changelog 2026-06-24 00:22:41 -07:00
Prospector a18f67bf0c Merge branch 'main' into prospector/modpack-proj-fixes 2026-06-24 00:21:19 -07:00
Prospector 623e01dbff bunch more fixes lol 2026-06-24 00:20:02 -07:00
Prospector 14c728c8e3 Copy ID button 2026-06-23 23:03:16 -07:00
ThatGravyBoatandGitHub a1b2073f57 feat: add moderation status action button to tech review (#6484)
* feat: add moderation status action button to tech review

* chore: run prepr
2026-06-24 05:54:39 +00:00
François-Xavier TalbotandGitHub 45c417e406 fix query cache (#6488) 2026-06-24 06:42:14 +02:00
François-Xavier TalbotandGitHub 47c60788ff attributions fixes (#6487)
* feat(labrinth): begin & commit one transaction per file scans

* chore(labrinth): use ro pool for the query
2026-06-24 06:19:10 +02:00
Prospector 3c60cc6f08 project page gap 2026-06-23 15:20:19 -07:00
Michael H. d1e8a9c574 perf: split attribution_enforced_versions subquery to use indexes
The original view used a single correlated `not exists` with an `or`,
which prevented the partial unique indexes on `attributions_exemptions`
from being used. Splitting into two `not exists` clauses lets each
anti-join use its own index instead of a full sequential scan.
2026-06-24 00:19:16 +02:00
aecsocketandGitHub 7bf85239de Improve error handling in account balance notifications (#6478)
* Improve error handling in account balance notifications

* fix compile
2026-06-23 21:11:02 +00:00
Prospector 06353bbc14 move deps after compat 2026-06-23 13:50:35 -07:00
6380dd16d5 chore: modpack permissions blog (#6474)
* chore: add blog

* update bullets

* Update blog

* remove old files

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-23 20:24:38 +00:00
Prospector 2376d571bb changelog 2026-06-23 12:31:41 -07:00
e7926083fb feat: new modpack permissions system (#6005)
* Begin external projects moderator database frontend

* add copy link button

* begin project page permissions settings

* MEL database backend routes

* include filename in external files

* wip: when uploading a version file, fetch its overrides as a list

* wip: override license checks

* improve FileHost ref counting

* file host read capability

* scan files when inserting version file

* add dependency sha1 field

* clean up version files

* wip: attributions

* update s3 file host

* attribution scanning basic works

* works

* insert attribution info after resolving

* add routes

* remove dep sha1 stuff

* prepr

* wip: override file sources

* add files_missing_attributions to versions

* return extended version info + attributed at/by

* hook up frontend to backend (mostly)

* expose version date published

* withholding version visibility

* frontend work

* prepr

* use api-client for img upload

* moar frontend

* prepr

* Add schema to attribution resolution and Flame project results

* sqlx prepare

* changes

* remove feature flag, fix optional proof images

* fix schema

* fmt

* fix deletion and file fetch

* prepare

* fix admonition

* update frontend stuff to new schema

* prepr

* attribution on dependencies

* fixes

* sqlx prepare

* fixes

* routes

* fix routes

* Version grandfathering

* prepare

* wip: bulk routes

* pushing what i've got rn

* include link in NoPermission

* change hash insert to bulk route

* query flame even if entry in MEL

* delete file with weird name

* Prioritise putting override files in existing groups even with ExternalLicense

* fix how hex bytes are handled in route

* feat: coolbot moderation changes (#6215)

* Update moderator checklist

* move permissions stage order

* Updated nagContext.versions to v3, added nag for permissions

* Update permissions.vue default messages

* prepr

---------

Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>

* QA

* prepr

* should group by project

* return attribution resolution correctly

* updated by moderator info

* Track what moderator reviewed an attribution moderation status

* default deser FMA field

* new version page

* clean up fetching + add a couple missing features

* qa items

* prepr

* provide moderation package stuff with DI

* format?

* don't redact moderated_at

* move supplementary resources

* Reorganize moderation messages.

* Quick replies for external content permissions.

* prepare

* QA

* allow exempting projects

* Ignore Flame projects which 404

* fix ci

* fix cross project attribution stuff

* Fix permission error

* change what files get cscanned

* add more logging

* QA Jun 22

* fix

* idempotency

* Expose route for rescanning

* update blog link

---------

Co-authored-by: aecsocket <aecsocket@tutanota.com>
Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-06-23 21:27:51 +02:00
Michael H. a686a93858 fix: preview deployment incorrect branch data 2026-06-23 20:57:47 +02:00
Modrinth BotandGitHub 2876890fe5 New translations from Crowdin (main) (#6466) 2026-06-23 18:36:07 +00:00
Calum H.andGitHub 4c869d2a37 fix: broken icu script (#6480) 2026-06-23 14:43:12 +00:00
ThatGravyBoatandGitHub 525f60c389 fix(docs): missing environment field from v2 createVersion route (#6476)
* fix(docs): missing environment field from v2 createVersion route

* fix(docs): missing environment field from v2 createVersion route
2026-06-23 00:35:41 +00:00
Truman GaoandGitHub 042a27a17c fix: small breakdowns fixes (#6473)
* fix: members to member, no dependents to none

* feat: add fuzzy search to multiselect options search and projects dropdown

* fix: duplicate key
2026-06-22 20:40:53 +00:00
Prospector a747c2f290 changelog 2026-06-22 11:15:40 -07:00
Truman GaoandGitHub d756e5ab93 fix: homepage search relying on generated state (#6471) 2026-06-22 18:09:51 +00:00
Truman GaoandGitHub 1f8b1bd2bb feat: implement dependent projects + members breakdown/filter (#6395)
* feat: implement dependent project breakdown

* implement dependent project type filter

* feat: show dependent on column if there are multiple projects selected

* feat: different tooltip for when both project version and dependent breakdowns are on

* feat: implement dependents query filter

* feat: add project icon to query filter

* fix: dont show dependent on column when project breakdown is selected

* feat: add org icon

* remove: dependent on project column and project version project column

* refactor: pnpm prepr

* fix: all project selection gets unselected when reload page

* pnpm prepr

* fix: still using old search formatter

* feat: implement projects in table click to link to project page

* feat: handle analytics values that dont have a dependent project as a "No dependent" row

* pnpm prepr

* feat: remove org icon for dependents projects and change unknown label

* feat: do not include unknown rows for showing top 8

* feat: separate out no dependents based on download reason

* fix: use compatible dependencies for query filter options

* feat: implement members breakdown/filter frontend (#6470)

* feat: add link to user in table

* fix: do not allow breakdowns to occur without shared stats

* pnpm prepr

* fix: i18n and passing params for formatter
2026-06-22 18:09:00 +00:00
aecsocketandGitHub 23edb2351d Filter servers out from analytics dependents (#6445) 2026-06-22 15:07:02 +00:00
2c53e1240e feat: add analytics fetch users mapping (#6439)
* feat: add users mapping

* remove skip_serialize_if

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-06-22 15:06:12 +00:00
aecsocketandGitHub b7d097d74d Batch incremental search event consumption (#6468)
* Batch incremental search event consumption

* fmt

* fix
2026-06-22 14:54:02 +00:00
aecsocketandGitHub 0a0ef1ce81 Fix account deletion deleting incorrect report threads (#6465) 2026-06-22 14:27:31 +00:00
d7917048d5 fix: allow PAT scope labels to wrap on long strings (#6456)
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-06-21 23:03:11 +00:00
Emma F.andGitHub a0017cf58f fix: nested details tags in markdown bodies (#6458) 2026-06-21 18:56:24 +00:00
SychicandGitHub b188bbf617 fix(maven): return requested file extension (#6455)
* fix(maven): return requested file extension

closes #2428, #2663
why was this hard coded lol

* fix(maven): handle other file types properly
2026-06-21 13:09:14 +00:00
Michael H.andGitHub 79ce1adf9d chore: kebab-case topic name 2026-06-21 14:42:24 +02:00
aecsocketandGitHub c6ef10e798 Incremental search indexing (#6453)
* wip: initial kafka support

* wip: kafka search queue

* wip: kafka consumer

* wip: create/delete docs

* reindexing works

* reindex in more places

* also incrementally update server pings

* cleanup

* tombi

* fix ci

* update tombi

* prepare

* fix ci

* fix

* fix PLEASE

* 🙏

* add route to force reindex project
2026-06-21 10:03:20 +00:00
aecsocketandGitHub be65589e4d Allow v2 version creation route to accept environment (#6454)
* Allow v2 version creation route to accept environment

* fix
2026-06-20 19:30:32 +00:00
ThatGravyBoatandGitHub 86a224419c fix: users projects shifting around while in queue (#6452)
fix: users projects shifting around while in queue due to download count changes
2026-06-20 15:55:03 +00:00
Calum H.andGitHub 49dd844fe1 feat: changelog app 0.14.8 (#6450) 2026-06-19 20:34:28 +00:00
Calum H.andGitHub 56536abb24 fix: modal shift on close + readd settings preload (#6448)
* fix: NewModal layout shift on close

* fix: preload queries in settings modals

* fix: any
2026-06-19 20:23:35 +00:00
aecsocketandGitHub ac5daad280 Tiltify API query backoff (#6447)
* Tiltify API querying backoff

* fix backoff

* fix clippy

* fix2
2026-06-19 19:02:22 +00:00
checkrdandGitHub 87eec7741b fix: remove max-width cap on floating action bar for mobile screens (#6430) 2026-06-19 18:38:33 +00:00
Modrinth BotandGitHub 4c9ee5b1c4 New translations from Crowdin (main) (#6403) 2026-06-19 18:08:03 +00:00
Truman GaoandGitHub caeab46ca6 fix: clicking scrollbar closes combobox (#6443)
* fix: clicking overlay scrollbar closes combobox

* feat: adjust multiselect scrollbar style

* feat: for dropdown mutliselect too
2026-06-19 18:07:13 +00:00
Calum H.andGitHub 8e6004fdd5 fix: billing fix for medal servers (#6437)
* fix: billing fix for medal servers

* fix: lint
2026-06-19 18:04:25 +00:00
aecsocketandGitHub 50b2b9567c Fix token separators in game version search (#6435)
* Fix token separators in game version search

* fix
2026-06-19 15:50:02 +00:00
Truman GaoandGitHub e0ea14226e feat: add dawn launcher user agent (#6441)
* feat: add dawn launcher user agent

* rename to Dawn
2026-06-19 14:31:47 +00:00
Calum H.andGitHub d2b85c9f8e fix: pre-migration for skins drag and drop change (#6402) 2026-06-19 14:16:51 +00:00
aecsocketandGitHub d33f00d2b1 Tighten URL slug validation (#6442)
* Tighten URL slug validation

* slug sanitization in frontend
2026-06-19 13:52:24 +00:00
aecsocketandGitHub b3257a0614 Allow revenue analytics to bucket by user ID (#6428)
* adjust

* convert to sqlx macros

* Allow users on a team to see rev splits of other users

* prepare

* clarify comment
2026-06-18 12:12:56 +00:00
François-Xavier TalbotandGitHub 486b467af2 cont: fix(frontend): dont assume wss for panel pingtest as well (#6421)
* fix(frontend): dont assume wss for panel pingtest as well

* fix(frontend): wss assumptions

* chore: fix export

* chore: prettier
2026-06-17 22:54:09 +00:00
aecsocketandGitHub 336050f4df Clear owned projects cache when deleting an org with projects (#6429) 2026-06-17 19:24:44 +00:00
aecsocketandGitHub 2bb1ef775c Serve stale Tiltify data if API is not available (#6427) 2026-06-17 14:58:07 +00:00
François-Xavier TalbotandGitHub 5ed322d281 feat(frontend): don't assume secure protocol for kyros websocket/fs (#6420)
* feat(frontend): don't assume https as websocket/fs protocol

* fix(frontend): actually do the same for websocket

* fix(frontend): don't strip ws path
2026-06-16 21:39:38 +00:00
Calum H.andGitHub 8591bc8ebc feat: changelog app 0.14.7 (#6419) 2026-06-16 19:05:19 +00:00
01fd18d550 Fix: update modpack export filter (#6359)
* Update Modpack export filters

* fix: move file export filtering from vue to rust

* chore: remove unused import

---------

Co-authored-by: Gravy Boat <gravy@thatgravyboat.tech>
2026-06-16 18:51:45 +00:00
François-Xavier TalbotandGitHub 3aaa2ef071 feat(app-backend): key circuit breaker by URI path pattern (#6383)
* feat(app-lib): split `FetchFence` on base URI path

* chore: fmt

* fix(app-lib): fix test timing
2026-06-16 18:49:06 +00:00
Truman GaoandGitHub a3aeeac2c3 fix: babric mods incorrectly detected as fabric (#6407)
* fix: babric mods incorrectly detected as fabric

* remove: standard version, just check for fabric normalized beta version number

* add comment
2026-06-16 18:47:16 +00:00
Calum H.andGitHub dfba212be4 fix: update checking version match reliability (#6411) 2026-06-16 18:45:31 +00:00
Calum H.andGitHub 3877999e53 fix: update browse page when installing dependencies (#6414)
* fix: update browse page when installing dependencies

* fix: lint

* fix: vers page

* fix: vers page link query persistence
2026-06-16 18:36:42 +00:00
Calum H.andGitHub 46a7cf490d feat: implement dependency deletion modal for content tab (#6410)
* feat: implement dependency deletion modal for content tab

* fix: handle bulk delete state

* fix: lint
2026-06-16 18:36:30 +00:00
Truman GaoandGitHub b5f7406998 fix: game version filter in search using quotes instead of backticks (#6415) 2026-06-16 18:22:52 +00:00
ThatGravyBoatandGitHub 3679d2c786 fix: maven version ranges not matching correctly (#6416) 2026-06-16 17:28:50 +00:00
Calum H.andGitHub f48a2f64db fix: content modals missing nexttick (#6412) 2026-06-16 16:01:49 +00:00
Calum H.andGitHub fe80ac10fc fix: hide content selection floating bar modal issues (#6413) 2026-06-16 16:01:09 +00:00
Prospector cf0d260802 fix blog 2026-06-15 08:41:40 -07:00
Prospector 503d34ee0f add spark blog post 2026-06-15 07:34:31 -07:00
ProspectorandGitHub bfbe66f73b feat: add product metadata to admin billing (#6371)
* feat: add product metadata to admin billing

* prepr
2026-06-11 15:30:39 -07:00
aecsocketandGitHub 64e17c7c1b Include compatible dependencies field in project search (#6366)
* include compatible dependency projects field

* fix
2026-06-11 20:18:44 +00:00
Calum H.andGitHub c082594825 fix: underscored users not searchable (#6362) 2026-06-11 19:27:28 +00:00
Prospector e5831d38eb changelog 2026-06-11 12:08:54 -07:00
Truman GaoandGitHub 1cabfe3e85 fix: allow mojang skins to be draggable (#6365)
* fix: allow mojang skins to be draggable

* pnpm prepr
2026-06-11 19:02:28 +00:00
36423eb5b5 Feat: system theme live update (#6197)
* fix: restore fixed render mode for logs tab

* feat: react to system theme changes in real time

When the 'System' theme is selected, listen for OS-level
prefers-color-scheme changes and update the app theme immediately,
without requiring a restart.

* Apply suggestion from @IMB11

Signed-off-by: Calum H. <hendersoncal117@gmail.com>

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Calum H. <calum@modrinth.com>
2026-06-11 13:32:15 +00:00
ArthurandGitHub 7d15fd3ac0 devex: integrate i18n ally extension (#6328)
* Integrate with i18n ally

* Update .gitignore

* Cleanup

* Dont force display language
2026-06-11 12:52:44 +00:00
c1780eef7d feat: drag and drop skins to reorder (#6357)
* feat: drag and drop skins to reorder

* feat: implement drag to reorder skins

* fix: ci

* remove: backend implementation

* regenerate sqlx

* fix: remove v-if selectable

* feat: remove drag handle

* refactor: pnpm prepr

* cargo fmt

* fix: dragging disable hover, wrong evt for edit skin + remove back of skin hover

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-06-11 12:22:38 +00:00
aecsocketandGitHub d2a66bb2b0 Allow searching by project dependencies (#6350)
* Allow searching by project dependencies

* change field name

* use query macro
2026-06-10 17:30:03 +00:00
Calum H.andGitHub 98b1730e19 fix(skins): translucency issues with outer layer (#6345)
fix(skins): translucency issues with outer layer in main renderer
2026-06-10 13:28:21 +00:00
Calum H.andGitHub 180cef6eaa fix(skins): better offline handling (#6344) 2026-06-10 13:28:10 +00:00
Calum H.andGitHub b828fa17de feat(app-auth): add new error cases to mc auth error modal (#6349) 2026-06-10 13:28:00 +00:00
Truman GaoandGitHub 72a4e86c26 fix: analytics events page not in admin dropdown (#6352)
* fix: analytics events page not in admin dropdown

* pnpm prepr

* fix: add clearing date picker

* fix: date picker positioning not using rendered height
2026-06-09 21:01:42 +00:00
Calum H.andGitHub 93f8da1666 fix(app): notifications appearing with modal open (#6348)
fix: notifications appearing with modal open
2026-06-09 21:01:02 +00:00
Calum H.andGitHub f474940321 fix(skins): hardcoded skins not editable (#6347)
fix: hardcoded skins not editable
2026-06-09 18:23:19 +00:00
Calum H.andGitHub 83b0586fd2 fix(skins): loading state flash on delete (#6346) 2026-06-09 18:22:07 +00:00
Calum H.andGitHub 543d25e2d6 feat: use discord_id from discord sso for role grant (#6326)
* feat: properly use labrinth's linked discord accts sso for discord bot

* Update email for fixture user in SQL insert

Signed-off-by: Calum H. <calum@modrinth.com>

* fix: rev changes

* fix: copy on email

* fix: lint

* fix: rev

* fix: lint

---------

Signed-off-by: Calum H. <calum@modrinth.com>
2026-06-09 17:33:07 +00:00
aecsocketandGitHub bc5a761312 Add analytics meta for downloading dependent projects (#6318)
* Send dependent mod info to backend

* Parse meta from query

* condense dependent_on and modpack

* Analytics dependents response
2026-06-09 15:47:52 +00:00
Prospector 3258d7dbdf changelog 2026-06-08 15:56:14 -07:00
ProspectorandGitHub b5d1aeda85 feat: collapse uncommon plugin loaders behind "Show more" (#6342) 2026-06-08 15:53:42 -07:00
coolbotandGitHub 1cedbe5fda fix: upload a version nag (#6338)
Update core.ts

Use projectV3.versions instead of NagContext.versions to prevent the warning showing up when versions have not been loaded yet.
2026-06-08 22:06:47 +00:00
Truman GaoandGitHub 2c9bf58d1f feat: various analytics updates (#6330)
* feat: add button to view user analytics

* feat: add "Your projects" preset selection

* feat: fix revenue rounding for values under 1 and show full values for all statcards with tooltip

* fix: sum rounded value instead of raw value for tooltip total if it's under 1

* fix: show decimal in playtime statcard if under 1 hrs

* feat: disable playtime statcard for purely plugin projects

* refactor: pnpm prepr
2026-06-08 22:03:28 +00:00
Truman GaoandGitHub a92b5b08df fix: radius on inner button in Tabs (#6340) 2026-06-08 21:53:40 +00:00
Calum H.andGitHub 01d3fb47c4 feat: updater ui change + win restart fix (#6339)
* feat: updater ui change

* fix: fix width

* fix: impl fork tauri updater plugin

* fix: lint
2026-06-08 21:52:22 +00:00
Calum H.andGitHub 9404d46782 feat: release channels instance setting (#6252)
* feat: rough release channels impl draft

* feat: move to bottom + lint

* fix: invalidate content queries on channel change

* fix: change to chips

* fix: lint

* fix: copy
2026-06-08 17:10:59 +00:00
Calum H.andGitHub 926c72de42 fix: files tab drag and drop (#6325)
* fix: files drag drop

* fix: standardize drag and drop + fix files tab permissions
2026-06-08 17:03:30 +00:00
9729737d7d fix: replace save banner in env migration modal with physical bn (#6028)
* feat: dont use save banner for project env modal

* fix: modal btn gap

* fix: lint

* Update packages/ui/src/components/project/settings/environment/ProjectEnvironmentModal.vue

Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* fix: lint

* fix: import

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-08 16:52:35 +00:00
Truman GaoandGitHub 97a8c11b50 fix: incorrectly showing empty state for versions page (#6303)
* fix: incorrectly showing empty state for versions page

* refactor: pnpm prepr
2026-06-08 16:40:00 +00:00
Truman GaoandGitHub 4d654162eb refactor: rename query-filter.ts to query-filter-utils.ts (#6335) 2026-06-08 16:39:12 +00:00
Modrinth BotandGitHub 33b1419bdf New translations from Crowdin (main) (#6332) 2026-06-08 16:20:46 +00:00
Calum H.andGitHub c28ba2e6a4 fix: z index of console tab expanded (#6334)
fix: z index
2026-06-08 15:56:14 +00:00
aecsocketandGitHub c7ba6ba8b2 Bump frontend version upload timeout (#6333)
Bump upload timeout
2026-06-08 15:43:18 +00:00
Calum H.andGitHub 7366c32df3 feat: incompat modal improvement (#6256)
* feat: incompat modal improvement

* feat: use ContentUpdaterModal and remove IncompatibilityWarningModal

* fix: lint

* fix: lint
2026-06-05 15:56:05 +00:00
707e219ff8 feat: use multi select for moderation reports (#6312)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-05 14:55:05 +00:00
Calum H.andGitHub dfe12d4ecb feat: server access post release QA (#6316)
* fix: clicking users in table in app takes you to blank page instead of website

* fix: wrong loader icon on server panel

* fix: surface var misalignment

* fix: password managers still detecting username field as something to autofill

* feat: show users on backupitem components

* feat: seperators for filter sections

* fix: lint + change remove -> revoke

* fix: copy

* feat: align copy
2026-06-05 14:54:27 +00:00
Truman GaoandGitHub c653228fe7 fix: malformed versions causing versions list page to crash (#6315) 2026-06-05 09:49:59 +00:00
Prospector 352a196795 update blog date 2026-06-04 11:56:52 -07:00
Calum H.andGitHub cef9b1efe5 fix: release workflow (#6311) 2026-06-04 09:31:41 -07:00
Calum H.andGitHub 7d6c54cff9 feat: changelog (#6310) 2026-06-04 18:01:38 +02:00
bd97ace974 feat: hosting access tab (#5995)
* feat: implement access tab with dummy data

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-04 15:58:01 +00:00
58ad58f958 feat: user search (#6302)
* feat: user search

* refactor: use sqlx macro

* refactor: better name for escaped query

* style: cleanup unused import

* chore: update sqlx query cache

* fix: tests

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
2026-06-03 19:38:24 +00:00
Prospector d907083d83 changelog 2026-06-03 11:38:23 -07:00
Truman GaoandGitHub 8371ff641a fix: analytics post release bugs (#6291)
* fix: previous period data was included in the table

* fix: revenue displaying stale data when viewing it from different metric and grouped by 6 hour or 1 hour

* fix: remove staletime on analytics query so switching tabs does not refersh query

* feat: add monetization alert

* fix-small: missing space in tooltip

* fix: incorrect y-axis formatting for trailing decimal 0s

* fix: switching tabs resets table series selection due to other refetches

* fix: always show month first in chart tooltip

* fix: change all time start date to be project published date

* fix: increase length on project name column

* fix: unknown download source data points not showing for download source breakdown

* fix: double unknown for loader

* fix: no data on country labeling incorrectly as "Unknown" instead of "Other"

* fix: date picker number inputs showing arrows

* fix: stat card showing enormous percentage for prev period by switching it to absolute value difference after 1000%

* fix: decimal values for playtime being rounded badly, resulting in 0.04 becoming 0.0

* fix: chips having stroke

* refactor: pnpm prepr

* fix: spacing in annoucement link

* fix: legend scroll shadow on top of event tooltip
2026-06-03 18:27:31 +00:00
Prospector b1cd16f966 update ads.txt 2026-06-03 10:23:11 -07:00
Prospector 40a06921ea changelog 2026-06-03 07:00:46 -07:00
Aaron CorreaandGitHub a7dc063e08 fix: use correct organization route parameter (#6294) 2026-06-03 13:41:02 +00:00
ThatGravyBoatandGitHub 64b61d8fd0 chore: misc moderation changes (#6296)
* feat: report filter by target, issue type, project type

* fix: reply modal showing up for staff

* chore: change minimum class source from 10 to 2
2026-06-03 13:14:17 +00:00
5e7d4cc838 fix: notifs not live (#6299)
* fix: notifs not live

* Update apps/labrinth/src/routes/internal/external_notifications.rs

Co-authored-by: Sychic <47618543+Sychic@users.noreply.github.com>
Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* fix: fmt

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Sychic <47618543+Sychic@users.noreply.github.com>
2026-06-03 13:00:12 +00:00
ProspectorandGitHub a0c80b13a4 fix: no mention of Australia on hosting landing page (#6288) 2026-06-03 02:25:16 +00:00
Calum H.andGitHub 3c051f5b1d feat: add notifs onto friends ws temporarily (#6290)
* feat: add notifs onto friends ws temporarily

* fix: lint + styling

* fix: regressions
2026-06-02 19:47:37 +00:00
Calum H.andGitHub 940a796ba5 chore: changelog (#6286) 2026-06-02 18:39:29 +02:00
6ee5e4df19 feat: access labrinth backend (#6284)
* feat: redirect `/hosting` to archon

* feat: server invite notification type

* feat: direct email notification endpoint

* feat: revoke notification endpoint

* feat: specify users to remove notifications from

* refactor: insert notifications before sending emails

* refactor: rename endpoint

* refactor: remove archon redirect

* style: mark field unused

* feat: dedup external notifications

* feat: add server invite email templates

* style: remove unnecessary format

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
2026-06-02 16:34:04 +00:00
aecsocketandGitHub d61397097c Tilitfy webhook changes (#6267)
* Tilitfy webhook changes

* add env

* fix
2026-06-02 15:14:02 +02:00
Calum H.andGitHub cfe45b368c fix: memory issues when importing giant mrpack files (#6278)
* feat: dont load mrpacks into memory if they are local imports

* fix: frontend
2026-06-02 12:59:41 +00:00
Calum H.andGitHub 6b0a0c1897 fix: content update modal hiding available updates until reopen (#6277) 2026-06-02 04:23:19 +00:00
Calum H.andGitHub f27691340a fix: path copy issue files tab (#6276) 2026-06-02 04:19:05 +00:00
aecsocketandGitHub c3a58aba9e Fix clickhouse URI, country filtering (#6247)
* Switch to bind for long params

* Country filtering

* prepare

* playtime preservation
2026-06-01 16:11:44 +00:00
ProspectorandGitHub 1550dfb3f0 feat: add toggle for showing play time (#6240)
* feat: add toggle for showing play time

* prepr
2026-06-01 15:52:01 +00:00
Modrinth BotandGitHub 4762a0a725 New translations from Crowdin (main) (#6275) 2026-06-01 14:11:53 +00:00
Prospector cf82943766 fix double changelog 2026-05-31 17:55:20 -07:00
Prospector b527e8f8f2 changelog 2026-05-31 16:28:21 -07:00
Prospector 3c53b5793b fix ping test in app 2026-05-31 16:26:41 -07:00
Prospector e6afc6f4f3 fix some pride links 2026-05-31 16:22:16 -07:00
Prospector 05699a90d6 fix pride badge svg 2026-05-31 15:39:29 -07:00
Prospector 142d560f76 update blog post 2026-05-31 13:43:19 -07:00
Calum H.andGitHub e80e27884e fix: crowdin moment (#6239) 2026-05-31 19:25:20 +00:00
Prospector 75788938b5 update blog post 2026-05-31 10:12:20 -07:00
ProspectorandGitHub 2570cf1bd7 feat: add sydney region (#6258)
* feat: add sydney region

* prepr

* rename
2026-05-31 10:11:58 -07:00
Calum H.andGitHub b0cca873b6 feat: changelog (#6268) 2026-05-31 16:54:33 +00:00
Calum H.andGitHub 325926ad9b feat: pride 2026 frontend (#6205)
* feat: pride 2026 banner app sidebar

* feat: use ProgressBar component

* feat: pride skins

* feat: pride skins

* feat: blog post

* fix: blogpost

* fix: pride skin condition

* fix: types

* fix: show logic

* fix: qa

* fix: lint

* fix: unused var
2026-05-31 16:43:41 +00:00
34b87991bc feat: new user badges, ui consistency pass (#6262)
* feat: new user badges, ui consistency pass

* prepr

* fix: align with backend

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-05-31 15:25:31 +00:00
aecsocketandGitHub cc8d556448 Expose user campaign donation info (#6266)
expose extra donation info for users
2026-05-31 15:18:40 +00:00
Calum H.andGitHub 71286f7b2b fix: incorrect webhook schema for tiltify (#6260)
fix:
2026-05-31 01:11:22 +02:00
aecsocketandGitHub c29973ec1a Pride 26 campaign backend integration (#6254)
* wip: pride 2026 webhooks and stuff

* setup webhook and link to user

* fix up code

* improve donation resolution

* Pride 26 campaign

* idempotency

* wip: tiltify

* fix

* redis caching

* add num donators

* fix

* Revert openapi

* Prepare

* improve oauth token gen code
2026-05-30 19:21:33 +00:00
Prospector 8c95f0bb81 changelog 2026-05-29 16:10:27 -07:00
Truman GaoandGitHub 627ab6734b fix: revenue not showing for all time query (#6246) 2026-05-29 15:47:17 -07:00
Prospector 36e1cbbf3e changelog 2026-05-29 15:06:42 -07:00
Truman GaoandGitHub 93fe87e57f feat: handle errors in analytics (#6245)
* fix: uri too long error by temporarily batching in frontend

* feat: fix empty state with error
2026-05-29 15:06:04 -07:00
Truman GaoandGitHub 02363c27a2 fix: download threshold (#6242)
* fix: download threshold

* fix: download threshold for projects select

* refactor: pnpm prepr

* feat: handle facets not adding count

* feat: remove getting facets download count field entirely

* feat: update facets to match new backend shape
2026-05-29 14:41:43 -07:00
aecsocketandGitHub 67e1743d6c Flatten facets response format, remove detailed route (#6244)
* Flatten facets response format

* delete test file
2026-05-29 23:22:08 +02:00
1531 changed files with 153432 additions and 29074 deletions
@@ -81,6 +81,6 @@ Please follow these rules precisely:
Use existing patterns from our codebase:
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
- Rich-text link tags: see `apps/frontend/src/error.vue`
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2.75.29
- uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
with:
tool: tombi
- run: tombi lint
+3 -7
View File
@@ -76,13 +76,6 @@ jobs:
env:
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
needs: [skip-if-clean]
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
@@ -96,6 +89,9 @@ jobs:
rustflags: ''
cache: false
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+15 -5
View File
@@ -20,6 +20,14 @@ on:
required: true
type: string
description: 'The environment to deploy to (staging-preview or production-preview)'
head_sha:
required: false
type: string
description: 'The commit SHA to deploy (defaults to github.sha)'
head_ref:
required: false
type: string
description: 'The branch name to deploy (defaults to github.ref_name)'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment || 'push' }}
@@ -40,12 +48,14 @@ jobs:
- name: Configure environment
id: meta
env:
ENV_INPUT: ${{ inputs.environment }}
REF: ${{ github.ref }}
DEPLOY_SHA: ${{ inputs.head_sha || github.sha }}
run: |
echo "cmd=deploy" >> $GITHUB_OUTPUT
ENV_INPUT="${{ inputs.environment }}"
REF="${{ github.ref }}"
SHA_SHORT="${GITHUB_SHA::8}"
SHA_SHORT="${DEPLOY_SHA::8}"
if [ "$ENV_INPUT" = "staging-preview" ]; then
echo "env=staging" >> $GITHUB_OUTPUT
@@ -106,8 +116,8 @@ jobs:
working-directory: ./apps/frontend
run: pnpm build
env:
CF_PAGES_BRANCH: ${{ github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
BUILD_ENV: ${{ steps.meta.outputs.env }}
PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }}
+9 -3
View File
@@ -24,6 +24,8 @@ jobs:
environment: [staging-preview, production-preview]
with:
environment: ${{ matrix.environment }}
head_sha: ${{ github.event.pull_request.head.sha }}
head_ref: ${{ github.event.pull_request.head.ref }}
deploy-storybook:
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
@@ -68,7 +70,9 @@ jobs:
- name: Configure short SHA
id: meta
run: echo "sha_short=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: echo "sha_short=${HEAD_SHA::8}" >> $GITHUB_OUTPUT
- name: Deploy Storybook preview
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
@@ -93,6 +97,8 @@ jobs:
- name: Read deployment URLs
id: urls
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
@@ -102,7 +108,7 @@ jobs:
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
echo "storybook-preview-url=https://git-${GITHUB_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
- name: Find comment
if: github.event_name == 'pull_request'
@@ -123,7 +129,7 @@ jobs:
body: |
## Frontend previews
Last deployed commit is [${{ github.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.sha }})
Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }})
| Environment | URL |
|-------------|-----|
+8 -7
View File
@@ -78,14 +78,8 @@ jobs:
GIT_HASH: ${{ github.sha }}
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
RUST_MIN_STACK: '16777216'
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -99,6 +93,13 @@ jobs:
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake libcurl4-openssl-dev
#uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
#with:
# packages: cmake libcurl4-openssl-dev
# version: v2 # cache key
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+15 -4
View File
@@ -30,6 +30,10 @@ on:
- prod-with-staging-archon
default: prod
required: false
app-version-override:
description: Temporary app version override for updater testing
type: string
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -80,6 +84,10 @@ jobs:
rustflags: ''
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: Setup mold
if: contains(matrix.platform, 'ubuntu')
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
@@ -126,7 +134,10 @@ jobs:
- name: Set application version and environment
shell: bash
run: |
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
APP_VERSION="${{ inputs.app-version-override }}"
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
fi
BUILD_ENVIRONMENT="${{ inputs.environment || 'prod' }}"
echo "Setting application version to $APP_VERSION"
echo "Using environment $BUILD_ENVIRONMENT"
@@ -153,7 +164,7 @@ jobs:
fi
- name: Build macOS app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: contains(matrix.platform, 'macos')
env:
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
@@ -168,7 +179,7 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Build Linux app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
if: contains(matrix.platform, 'ubuntu')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -179,7 +190,7 @@ jobs:
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: contains(matrix.platform, 'windows')
env:
+4 -1
View File
@@ -57,8 +57,11 @@ jobs:
- name: Extract app changelog
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.VERSION_TAG }}
run: npx --yes tsx scripts/build-theseus-release-notes.ts
run: |
LAST_GITHUB_RELEASE_PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.published_at // ""' 2>/dev/null || true)
LAST_GITHUB_RELEASE_PUBLISHED_AT="$LAST_GITHUB_RELEASE_PUBLISHED_AT" npx --yes tsx scripts/build-theseus-release-notes.ts
- name: Generate version manifest
run: |
+6 -10
View File
@@ -81,13 +81,6 @@ jobs:
# blacksmith runner)
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
steps:
@@ -99,8 +92,8 @@ jobs:
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
version: v1 # cache key
packages: cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
version: v2 # cache key
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -129,6 +122,9 @@ jobs:
components: clippy, rustfmt
cache: false
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5
with:
@@ -153,7 +149,7 @@ jobs:
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
- name: Setup nextest
run: cargo binstall --no-confirm --secure cargo-nextest@0.9.133
run: cargo binstall --no-confirm --secure --locked cargo-nextest@0.9.133
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
# back to a cached cargo install
+1
View File
@@ -22,6 +22,7 @@ node_modules
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/i18n-ally-custom-framework.yml
# IDE - IntelliJ
.idea/*
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer"]
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer", "lokalise.i18n-ally"]
}
+10
View File
@@ -0,0 +1,10 @@
languageIds:
- vue
- typescript
- javascript
- typescriptreact
usageMatchRegex:
- id:\s*['"]({key})['"]
monopoly: true
+12 -2
View File
@@ -9,7 +9,7 @@
"files.insertFinalNewline": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "always"
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
@@ -31,5 +31,15 @@
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore"
"scss.lint.unknownAtRules": "ignore",
"i18n-ally.localesPaths": [
"packages/ui/src/locales",
"apps/frontend/src/locales",
"packages/moderation/src/locales"
],
"i18n-ally.pathMatcher": "{locale}/index.{ext}",
"i18n-ally.keystyle": "flat",
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.namespace": false,
"i18n-ally.includeSubfolders": true
}
+3 -3
View File
@@ -50,7 +50,7 @@ Run these from the **root** folder before opening a pull request - do not run th
- **App frontend:** `pnpm prepr:frontend:app`
- **Frontend libs:** `pnpm prepr:frontend:lib`
- **All frontend (app+web):** `pnpm prepr`
- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md`
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
The website and app `prepr` commands
@@ -62,9 +62,9 @@ The website and app `prepr` commands
## Project-Specific Instructions
Each project may have its own `CLAUDE.md` with detailed instructions:
Each project may have its own file with detailed instructions:
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
## Code Guidelines
Generated
+718 -126
View File
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -9,6 +9,7 @@ members = [
"packages/ariadne",
"packages/daedalus",
"packages/labrinth-derive",
"packages/modrinth-content-management",
"packages/modrinth-log",
"packages/modrinth-maxmind",
"packages/modrinth-util",
@@ -32,7 +33,6 @@ actix-ws = "0.3.0"
arc-swap = "1.7.1"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
async-compression = { version = "0.4.32", default-features = false }
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
async-recursion = "1.1.1"
async-stripe = { version = "0.41.0", default-features = false, features = [
@@ -51,6 +51,7 @@ aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [
] }
base64 = "0.22.1"
bitflags = "2.9.4"
bon = "3.9.3"
bytemuck = "1.24.0"
bytes = "1.10.1"
censor = "0.3.0"
@@ -85,6 +86,7 @@ eyre = "0.6.12"
flate2 = "1.1.4"
fs4 = { version = "0.13.1", default-features = false }
futures = "0.3.31"
futures-lite = "2.6.1"
futures-util = "0.3.31"
heck = "0.5.0"
hex = "0.4.3"
@@ -118,6 +120,7 @@ lettre = { version = "0.11.19", default-features = false, features = [
] }
maxminddb = "0.26.0"
meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-content-management = { path = "packages/modrinth-content-management" }
modrinth-log = { path = "packages/modrinth-log" }
modrinth-util = { path = "packages/modrinth-util" }
muralpay = { path = "packages/muralpay" }
@@ -139,6 +142,7 @@ quick-xml = "0.38.3"
quote = { version = "1.0" }
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
rdkafka = { version = "0.36.2", features = ["cmake-build"] }
redis = "0.32.7"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
@@ -155,6 +159,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
scalar_api_reference = { version = "0.2.2", default-features = false }
secrecy = "0.10.3"
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
@@ -191,7 +196,7 @@ tauri-plugin-http = "2.5.7"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3.1"
tauri-plugin-single-instance = "2.3.4"
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
tauri-plugin-updater = { git = "https://github.com/modrinth/plugins-workspace", rev = "0d30f2aa28ec668ce187d527da1c475da3c01cbc", default-features = false, features = [
"rustls-tls",
"zip",
] }
@@ -214,10 +219,10 @@ typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
utoipa-actix-web = { version = "0.1.2" }
utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
webauthn-rs = "0.5.5"
webauthn-rs-proto = "0.5.5"
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whoami = "1.6.1"
@@ -268,6 +273,7 @@ redundant_type_annotations = "warn"
result_large_err = "allow"
todo = "warn"
too_many_arguments = "allow"
type_complexity = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"
+2 -2
View File
@@ -3,13 +3,13 @@
![Issues](https://img.shields.io/github/issues-raw/Modrinth/code?color=c78aff&label=issues&style=for-the-badge)
![Pull Requests](https://img.shields.io/github/issues-pr-raw/Modrinth/code?color=c78aff&label=PRs&style=for-the-badge)
![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=c78aff&label=contributors&style=for-the-badge)
![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/code/badge?style=flat&logoColor=white&color=c78aff&style=for-the-badge)
![Lines of Code](https://img.shields.io/endpoint?url=https://loctopus.creeperkatze.dev/github/modrinth/code/badge?style=flat&logoColor=white&color=c78aff&style=for-the-badge)
![Commit Activity](https://img.shields.io/github/commit-activity/m/Modrinth/code?color=c78aff&label=commits&style=for-the-badge)
![Last Commit](https://img.shields.io/github/last-commit/Modrinth/code?color=c78aff&label=last%20commit&style=for-the-badge)
## Modrinth Monorepo
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains ![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/olympus/badge?logoColor=white&color=black&label=) lines of code and has ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=black&label=) contributors!
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains ![Lines of Code](https://img.shields.io/endpoint?url=https://loctopus.creeperkatze.dev/github/modrinth/code/badge%3Fformat%3Dhuman&logoColor=white&color=black&label=) lines of code and has ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=black&label=) contributors!
If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app).
+2 -1
View File
@@ -44,7 +44,8 @@
"vue": "^3.5.13",
"vue-i18n": "^10.0.0",
"vue-router": "^4.6.0",
"vue-virtual-scroller": "v2.0.0-beta.8"
"vue-virtual-scroller": "v2.0.0-beta.8",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@eslint/compat": "^1.1.1",
+328 -133
View File
@@ -12,7 +12,6 @@ import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
CompassIcon,
DownloadIcon,
ExternalIcon,
HomeIcon,
LeftArrowIcon,
@@ -36,6 +35,7 @@ import {
ButtonStyled,
commonMessages,
ContentInstallModal,
ContentUpdaterModal,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
@@ -44,7 +44,6 @@ import {
NotificationPanel,
OverflowMenu,
PopupNotificationPanel,
ProgressSpinner,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
@@ -75,7 +74,6 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
@@ -84,6 +82,7 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
@@ -94,13 +93,14 @@ import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads
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, warning_listener } from '@/helpers/events.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 { list, run } from '@/helpers/instance'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
import {
areUpdatesEnabled,
enqueueUpdateForInstallation,
@@ -110,7 +110,18 @@ import {
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
import i18n from '@/i18n.config'
import {
appUpdateState,
downloadAvailableAppUpdate,
getNextAppUpdatePopupTime,
installAvailableAppUpdate,
markAppUpdateActionable,
markAppUpdatePopupShown,
openAppUpdateChangelog,
setAppUpdateActions,
} from '@/providers/app-update.ts'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
import {
provideAppUpdateDownloadProgress,
@@ -134,6 +145,7 @@ const route = useRoute()
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
@@ -144,6 +156,9 @@ const forceSidebar = computed(
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
)
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
@@ -192,6 +207,12 @@ const tauriApiClient = new TauriModrinthClient({
],
})
provideModrinthClient(tauriApiClient)
const { data: authenticatedModrinthUser } = useQuery({
queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]),
queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(),
enabled: () => !!credentials.value?.session,
retry: false,
})
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
@@ -229,6 +250,7 @@ const {
const news = ref([])
const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -284,6 +306,7 @@ onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
unsubscribeSidebarToggle()
clearDelayedUpdatePopup()
await unlistenUpdateDownload?.()
})
@@ -300,18 +323,6 @@ const messages = defineMessages({
id: 'app.update.complete-toast.text',
defaultMessage: 'Click here to view the changelog.',
},
reloadToUpdate: {
id: 'app.update.reload-to-update',
defaultMessage: 'Reload to install update',
},
downloadUpdate: {
id: 'app.update.download-update',
defaultMessage: 'Download update',
},
downloadingUpdate: {
id: 'app.update.downloading-update',
defaultMessage: 'Downloading update ({percent}%)',
},
authUnreachableHeader: {
id: 'app.auth-servers.unreachable.header',
defaultMessage: 'Cannot reach authentication servers',
@@ -599,6 +610,16 @@ const {
handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance,
setIncompatibilityWarningModal: setContentIncompatibilityWarningModal,
incompatibilityWarningVersions: contentInstallIncompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion: contentInstallIncompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader: contentInstallIncompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType: contentInstallIncompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl: contentInstallIncompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName: contentInstallIncompatibilityWarningProjectName,
incompatibilityWarningMessage: contentInstallIncompatibilityWarningMessage,
incompatibilityWarningInstalling: contentInstallIncompatibilityWarningInstalling,
handleIncompatibilityWarningInstall: handleContentInstallIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
@@ -620,6 +641,12 @@ const updateToPlayModal = ref()
const modrinthLoginFlowWaitModal = ref()
watch(incompatibilityWarningModal, (modal) => {
if (modal) {
setContentIncompatibilityWarningModal(modal)
}
})
setupAuthProvider(credentials, async (_redirectPath) => {
await signIn()
})
@@ -676,12 +703,11 @@ async function logOut() {
await fetchCredentials()
}
const MIDAS_BITFLAG = 1 << 0
const hasPlus = computed(
() =>
credentials.value &&
credentials.value.user &&
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
!!credentials.value?.user &&
(hasMidasBadge(credentials.value.user) ||
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
)
const showAd = computed(
@@ -741,19 +767,116 @@ const accounts = ref(null)
provide('accountsCard', accounts)
command_listener(handleCommand)
notification_listener(handleLiveNotification)
async function markLiveNotificationRead(notification) {
try {
await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id)
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) {
console.warn(`notification ${notification.id} could not be marked as read`, error)
return
}
throw error
}
}
async function respondToServerInvite(notification, action) {
const serverId = notification.body?.server_id
if (typeof serverId !== 'string') {
throw new Error('Missing server ID for invite notification.')
}
await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, {
api: 'archon',
version: 1,
method: 'POST',
})
await markLiveNotificationRead(notification)
return serverId
}
async function acceptServerInviteNotification(notification) {
try {
const serverId = await respondToServerInvite(notification, 'accept')
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
queryClient.invalidateQueries({ queryKey: ['servers'] })
} catch (error) {
handleError(error)
}
}
async function declineServerInviteNotification(notification) {
try {
await respondToServerInvite(notification, 'decline')
} catch (error) {
handleError(error)
}
}
function openServerInviteInviterProfile(inviterName) {
if (!inviterName) return
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
}
async function handleLiveNotification(notification) {
if (notification?.body?.type !== 'server_invite' || notification.read) return
if (displayedServerInviteNotifications.has(notification.id)) return
displayedServerInviteNotifications.add(notification.id)
const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
}
async function handleCommand(e) {
if (!e) return
if (e.event === 'RunMRPack') {
// RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) {
await create_profile_and_install_from_file(e.path, (createProfile, fileName) =>
unknownPackWarningModal.value?.show(createProfile, fileName),
).catch(handleError)
const location = { type: 'fromFile', path: e.path }
const preview = await install_get_modpack_preview(location).catch(handleError)
if (preview?.unknownFile) {
const splitPath = e.path.split(/[\\/]/)
const fileName = splitPath ? splitPath[splitPath.length - 1] : e.path
unknownPackWarningModal.value?.show(
() => install_create_modpack_instance(location).then(() => undefined),
fileName,
)
} else {
await install_create_modpack_instance(location).catch(handleError)
}
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
} else if (e.event === 'LaunchInstance') {
if (e.server) {
await start_join_server(e.id, e.server).catch(handleError)
} else if (e.singleplayer_world) {
await start_join_singleplayer_world(e.id, e.singleplayer_world).catch(handleError)
} else {
await run(e.id).catch(handleError)
}
} else if (e.event === 'InstallServer') {
await router.push(`/project/${e.id}`)
await playServerProject(e.id).catch(handleError)
@@ -774,20 +897,21 @@ async function handleCommand(e) {
}
const appUpdateDownload = {
progress: ref(0),
progress: appUpdateState.progress,
version: ref(),
}
let unlistenUpdateDownload
const downloadProgress = computed(() => appUpdateDownload.progress.value)
const downloadPercent = computed(() => Math.trunc(appUpdateDownload.progress.value * 100))
const metered = ref(true)
const finishedDownloading = ref(false)
const restarting = ref(false)
const availableUpdate = ref(null)
const updateSize = ref(null)
const updatesEnabled = ref(true)
const {
metered,
finishedDownloading,
downloading,
restarting,
availableUpdate,
updateSize,
updatesEnabled,
} = appUpdateState
let delayedUpdatePopupTimeout = null
const updatePopupMessages = defineMessages({
updateAvailable: {
@@ -798,11 +922,6 @@ const updatePopupMessages = defineMessages({
id: 'app.update-popup.download-complete',
defaultMessage: 'Download complete',
},
body: {
id: 'app.update-popup.body',
defaultMessage:
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
},
meteredBody: {
id: 'app.update-popup.body.metered',
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
@@ -818,7 +937,7 @@ const updatePopupMessages = defineMessages({
},
reload: {
id: 'app.update-popup.reload',
defaultMessage: 'Reload',
defaultMessage: 'Reload to update',
},
download: {
id: 'app.update-popup.download',
@@ -830,6 +949,106 @@ const updatePopupMessages = defineMessages({
},
})
function clearDelayedUpdatePopup() {
if (delayedUpdatePopupTimeout !== null) {
clearTimeout(delayedUpdatePopupTimeout)
delayedUpdatePopupTimeout = null
}
}
function getCurrentUpdatePromptStage() {
return finishedDownloading.value ? 'downloaded' : 'available'
}
function scheduleDelayedUpdatePopup() {
clearDelayedUpdatePopup()
const version = availableUpdate.value?.version
if (!version) {
return
}
const nextPopupTime = getNextAppUpdatePopupTime(version, getCurrentUpdatePromptStage())
if (nextPopupTime === null) {
return
}
const delay = nextPopupTime - Date.now()
if (delay <= 0) {
showDelayedUpdatePopup()
return
}
delayedUpdatePopupTimeout = setTimeout(showDelayedUpdatePopup, Math.min(delay, 2_147_483_647))
}
function showDelayedUpdatePopup() {
const update = availableUpdate.value
if (!update) {
return
}
const stage = getCurrentUpdatePromptStage()
const nextPopupTime = getNextAppUpdatePopupTime(update.version, stage)
if (nextPopupTime === null) {
return
}
if (Date.now() < nextPopupTime) {
scheduleDelayedUpdatePopup()
return
}
if (metered.value && !finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else if (finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: update.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else {
scheduleDelayedUpdatePopup()
return
}
markAppUpdatePopupShown(update.version, stage)
}
async function checkUpdates() {
if (!(await areUpdatesEnabled())) {
console.log('Skipping update check as updates are disabled in this build or environment')
@@ -853,11 +1072,15 @@ async function checkUpdates() {
if (isExistingUpdate) {
console.log('Update is already known')
scheduleDelayedUpdatePopup()
return
}
appUpdateDownload.progress.value = 0
finishedDownloading.value = false
downloading.value = false
updateSize.value = null
availableUpdate.value = update
console.log(`Update ${update.version} is available.`)
@@ -867,34 +1090,11 @@ async function checkUpdates() {
downloadUpdate(update)
} else {
console.log(`Metered connection detected, not auto-downloading update.`)
getUpdateSize(update.rid).then((size) => {
updateSize.value = size
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
keepOpen: true,
},
],
})
})
markAppUpdateActionable(update.version)
scheduleDelayedUpdatePopup()
}
getUpdateSize(update.rid).then((size) => (updateSize.value = size))
availableUpdate.value = update
}
await performCheck()
@@ -916,12 +1116,17 @@ async function checkLinuxUpdates() {
const latestVersion = updates?.version
if (latestVersion && latestVersion !== currentVersion) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
markAppUpdateActionable(latestVersion)
const nextPopupTime = getNextAppUpdatePopupTime(latestVersion)
if (nextPopupTime !== null && Date.now() >= nextPopupTime) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
markAppUpdatePopupShown(latestVersion)
}
}
} catch (e) {
console.error('Failed to check for updates:', e)
@@ -935,55 +1140,48 @@ async function downloadAvailableUpdate() {
async function downloadUpdate(versionToDownload) {
if (!versionToDownload) {
handleError(`Failed to download update: no version available`)
return
}
if (appUpdateDownload.progress.value !== 0) {
if (downloading.value || appUpdateDownload.progress.value !== 0) {
console.error(`Update ${versionToDownload.version} already downloading`)
return
}
console.log(`Downloading update ${versionToDownload.version}`)
downloading.value = true
try {
enqueueUpdateForInstallation(versionToDownload.rid).then(() => {
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
enqueueUpdateForInstallation(versionToDownload.rid)
.then(() => {
downloading.value = false
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
})
console.log('Finished downloading!')
markAppUpdateActionable(versionToDownload.version, 'downloaded')
scheduleDelayedUpdatePopup()
})
console.log('Finished downloading!')
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: versionToDownload.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
keepOpen: true,
},
],
.catch((e) => {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
})
})
unlistenUpdateDownload = await subscribeToDownloadProgress(
appUpdateDownload,
versionToDownload.version,
)
} catch (e) {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
}
}
async function installUpdate() {
restarting.value = true
try {
await setRestartAfterPendingUpdate(true)
} catch (e) {
@@ -996,6 +1194,12 @@ async function installUpdate() {
}, 250)
}
setAppUpdateActions({
download: downloadAvailableUpdate,
install: installUpdate,
changelog: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
})
async function openModrinthProjectLinkInApp(parsed) {
const { slug, pathSuffix, url } = parsed
const loadToken = loading.begin()
@@ -1137,12 +1341,11 @@ async function processPendingSurveys() {
const creds = await getCreds().catch(handleError)
const userId = creds?.user_id
const instances = await list().catch(handleError)
const isActivePlayer =
instances.findIndex(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
) >= 0
const instances = (await list().catch(handleError)) ?? []
const isActivePlayer = instances.some(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
)
let surveys = []
try {
@@ -1265,33 +1468,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<PlusIcon />
</NavButton>
<div class="flex flex-grow"></div>
<Transition name="nav-button-animated">
<div v-if="availableUpdate && !restarting && (finishedDownloading || metered)">
<NavButton
v-tooltip.right="
formatMessage(
finishedDownloading
? messages.reloadToUpdate
: downloadProgress === 0
? messages.downloadUpdate
: messages.downloadingUpdate,
{
percent: downloadPercent,
},
)
"
:to="finishedDownloading ? installUpdate : downloadAvailableUpdate"
>
<ProgressSpinner
v-if="downloadProgress > 0 && downloadProgress < 1"
class="text-brand"
:progress="downloadProgress"
/>
<RefreshCwIcon v-else-if="finishedDownloading" class="text-brand" />
<DownloadIcon v-else class="text-brand" />
</NavButton>
</div>
</Transition>
<NavButton
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
:to="() => $refs.settingsModal.show()"
@@ -1479,6 +1655,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
</suspense>
</div>
<PrideFundraiserBanner
v-if="prideFundraiserEnabled"
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
/>
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
<div class="space-y-4 flex flex-col items-center w-full">
@@ -1535,7 +1715,22 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@go-to-instance="handleModpackDuplicateGoToInstance"
/>
<AddServerToInstanceModal ref="addServerToInstanceModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<ContentUpdaterModal
ref="incompatibilityWarningModal"
mode="incompatibility-warning"
:versions="contentInstallIncompatibilityWarningVersions"
:current-game-version="contentInstallIncompatibilityWarningCurrentGameVersion"
:current-loader="contentInstallIncompatibilityWarningCurrentLoader"
current-version-id=""
:is-app="true"
:project-type="contentInstallIncompatibilityWarningProjectType"
:project-icon-url="contentInstallIncompatibilityWarningProjectIconUrl"
:project-name="contentInstallIncompatibilityWarningProjectName"
:warning="contentInstallIncompatibilityWarningMessage"
:action-loading="contentInstallIncompatibilityWarningInstalling"
@update="handleContentInstallIncompatibilityWarningInstall"
@cancel="handleContentInstallIncompatibilityWarningCancel"
/>
<ModpackAlreadyInstalledModal
ref="contentInstallModpackAlreadyInstalledModal"
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@@ -24,7 +24,8 @@ import { computed, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { duplicate, remove } from '@/helpers/profile.js'
import { install_duplicate_instance } from '@/helpers/install'
import { remove } from '@/helpers/instance'
const { handleError } = injectNotificationManager()
@@ -48,21 +49,21 @@ const instanceComponents = ref(null)
const currentDeleteInstance = ref(null)
const confirmModal = ref(null)
async function deleteProfile() {
async function deleteInstance() {
if (currentDeleteInstance.value) {
instanceComponents.value = instanceComponents.value.filter(
(x) => x.instance.path !== currentDeleteInstance.value,
(x) => x.instance.id !== currentDeleteInstance.value,
)
await remove(currentDeleteInstance.value).catch(handleError)
}
}
async function duplicateProfile(p) {
await duplicate(p).catch(handleError)
async function duplicateInstance(p) {
await install_duplicate_instance(p).catch(handleError)
}
const handleRightClick = (event, profilePathId) => {
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
const handleRightClick = (event, instanceId) => {
const item = instanceComponents.value.find((x) => x.instance.id === instanceId)
const baseOptions = [
{ name: 'add_content' },
{ type: 'divider' },
@@ -114,16 +115,16 @@ const handleOptionsClick = async (args) => {
break
case 'duplicate':
if (args.item.instance.install_stage == 'installed')
await duplicateProfile(args.item.instance.path)
await duplicateInstance(args.item.instance.id)
break
case 'open':
await args.item.openFolder()
break
case 'copy':
await navigator.clipboard.writeText(args.item.instance.path)
await navigator.clipboard.writeText(args.item.instance.id)
break
case 'delete':
currentDeleteInstance.value = args.item.instance.path
currentDeleteInstance.value = args.item.instance.id
confirmModal.value.show()
break
}
@@ -321,13 +322,13 @@ const filteredResults = computed(() => {
<Instance
v-for="instance in instanceSection.value"
ref="instanceComponents"
:key="instance.path + instance.install_stage"
:key="instance.id + instance.install_stage"
:instance="instance"
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.id)"
/>
</section>
</Accordion>
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteProfile" />
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteInstance" />
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
+18 -19
View File
@@ -21,9 +21,10 @@ import Instance from '@/components/ui/Instance.vue'
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_by_profile_path } from '@/helpers/process.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import { install_duplicate_instance } from '@/helpers/install'
import { kill, remove, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process.js'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
import { handleSevereError } from '@/store/error.js'
@@ -60,14 +61,14 @@ const deleteConfirmModal = ref(null)
const currentDeleteInstance = ref(null)
async function deleteProfile() {
async function deleteInstance() {
if (currentDeleteInstance.value) {
await remove(currentDeleteInstance.value).catch(handleError)
}
}
async function duplicateProfile(p) {
await duplicate(p).catch(handleError)
async function duplicateInstance(p) {
await install_duplicate_instance(p).catch(handleError)
}
const handleInstanceRightClick = async (event, passedInstance) => {
@@ -85,7 +86,7 @@ const handleInstanceRightClick = async (event, passedInstance) => {
},
]
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
const runningProcesses = await get_by_instance_id(passedInstance.id).catch(handleError)
const options =
runningProcesses.length > 0
@@ -126,16 +127,14 @@ const handleProjectClick = (event, passedInstance) => {
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
await run(args.item.path).catch((err) =>
handleSevereError(err, { profilePath: args.item.path }),
)
await run(args.item.id).catch((err) => handleSevereError(err, { instanceId: args.item.id }))
trackEvent('InstanceStart', {
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'stop':
await kill(args.item.path).catch(handleError)
await kill(args.item.id).catch(handleError)
trackEvent('InstanceStop', {
loader: args.item.loader,
game_version: args.item.game_version,
@@ -144,26 +143,26 @@ const handleOptionsClick = async (args) => {
case 'add_content':
await router.push({
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: args.item.path },
query: { i: args.item.id },
})
break
case 'edit':
await router.push({
path: `/instance/${encodeURIComponent(args.item.path)}`,
path: `/instance/${encodeURIComponent(args.item.id)}`,
})
break
case 'duplicate':
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
if (args.item.install_stage == 'installed') await duplicateInstance(args.item.id)
break
case 'delete':
currentDeleteInstance.value = args.item.path
currentDeleteInstance.value = args.item.id
deleteConfirmModal.value.show()
break
case 'open_folder':
await showProfileInFolder(args.item.path)
await showInstanceInFolder(args.item.id)
break
case 'copy_path':
await navigator.clipboard.writeText(args.item.path)
await navigator.clipboard.writeText(args.item.id)
break
case 'install': {
await installVersion(
@@ -239,7 +238,7 @@ onUnmounted(() => {
</script>
<template>
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteProfile" />
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteInstance" />
<div ref="rowContainer" class="flex flex-col gap-4">
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
<HeadingLink class="mt-1" :to="row.route">
@@ -256,7 +255,7 @@ onUnmounted(() => {
0,
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
)"
:key="row.label + instance.path"
:key="row.label + instance.id"
:instance="instance"
:compact="row.compact"
:first="instanceIndex === 0"
@@ -4,7 +4,7 @@ import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth
import { open } from '@tauri-apps/plugin-dialog'
import { useRouter } from 'vue-router'
import { add_project_from_path } from '@/helpers/profile.js'
import { add_project_from_path } from '@/helpers/instance'
const { handleError } = injectNotificationManager()
@@ -22,14 +22,14 @@ const handleAddContentFromFile = async () => {
if (!newProject) return
for (const project of newProject) {
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
await add_project_from_path(props.instance.id, project.path ?? project).catch(handleError)
}
}
const handleSearchContent = async () => {
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
query: { i: props.instance.path },
query: { i: props.instance.id },
})
}
</script>
@@ -1,5 +1,5 @@
<template>
<div class="flex gap-4 items-center">
<div class="flex gap-2 items-center">
<ButtonStyled
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
color="brand"
@@ -14,6 +14,7 @@
<UnplugIcon class="text-secondary" />
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
</div>
<AppUpdateButton />
<div
class="flex border-solid border-surface-5 text-sm items-center gap-2 py-1.5 px-3 rounded-xl border"
>
@@ -22,28 +23,28 @@
<div class="text-contrast flex items-center gap-2">
<router-link
v-tooltip="formatMessage(messages.viewInstance)"
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
:to="`/instance/${encodeURIComponent(selectedProcess.instance.id)}`"
class="hover:underline"
>
{{ selectedProcess.profile.name }}
{{ selectedProcess.instance.name }}
</router-link>
<Dropdown
v-if="currentProcesses.length > 1"
placement="bottom"
:triggers="['click']"
:hide-triggers="['click']"
@show="showProfiles = true"
@hide="showProfiles = false"
@show="showInstances = true"
@hide="showInstances = false"
>
<ButtonStyled type="transparent" circular size="small">
<button
v-tooltip="
showProfiles
showInstances
? formatMessage(messages.hideMoreRunningInstances)
: formatMessage(messages.showMoreRunningInstances)
"
>
<DropdownIcon :class="{ 'rotate-180': !!showProfiles }" />
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
</button>
</ButtonStyled>
<template #popper>
@@ -68,7 +69,7 @@
>
<OnlineIndicatorIcon />
<span class="mr-auto text-contrast flex items-center gap-2">
{{ process.profile.name }}
{{ process.instance.name }}
<StarIcon v-if="process.uuid === selectedProcess.uuid" class="text-orange" />
</span>
</button>
@@ -82,7 +83,7 @@
<button
v-tooltip="formatMessage(messages.viewLogs)"
class="active:scale-95 flex"
@click.stop="goToTerminal(process.profile.path)"
@click.stop="goToTerminal(process.instance.id)"
>
<TerminalSquareIcon class="text-secondary size-5" />
</button>
@@ -133,14 +134,17 @@ import {
type PopupNotificationProgressItem,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { Dropdown } from 'floating-vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import AppUpdateButton from '@/components/ui/app-update-button/index.vue'
import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications'
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 { get_many as getInstances } from '@/helpers/profile.js'
import type { LoadingBar } from '@/helpers/state'
import { progress_bars_list } from '@/helpers/state'
import type { GameInstance } from '@/helpers/types'
@@ -151,12 +155,12 @@ const { formatMessage } = useVIntl()
const router = useRouter()
const showProfiles = ref(false)
const showInstances = ref(false)
interface RunningProcess {
uuid: string
profile_path: string
profile: GameInstance
instance_id: string
instance: GameInstance
}
const messages = defineMessages({
@@ -217,22 +221,22 @@ const refresh = async () => {
const processes = ((await getRunningProcesses().catch((error) => {
handleError(error)
return []
})) ?? []) as Array<{ uuid: string; profile_path: string }>
const paths = processes.map((process) => process.profile_path)
const profiles: GameInstance[] = await getInstances(paths).catch((error) => {
})) ?? []) as Array<{ uuid: string; instance_id: string }>
const instanceIds = processes.map((process) => process.instance_id)
const instances: GameInstance[] = await getInstances(instanceIds).catch((error) => {
handleError(error)
return []
})
currentProcesses.value = processes
.map((process) => {
const profile = profiles.find((item) => process.profile_path === item.path)
if (!profile) {
const instance = instances.find((item) => process.instance_id === item.id)
if (!instance) {
return null
}
return {
...process,
profile,
instance,
}
})
.filter((process): process is RunningProcess => process !== null)
@@ -265,8 +269,8 @@ const stop = async (process: RunningProcess) => {
await killProcess(process.uuid).catch(handleError)
trackEvent('InstanceStop', {
loader: process.profile.loader,
game_version: process.profile.game_version,
loader: process.instance.loader,
game_version: process.instance.game_version,
source: 'AppBar',
})
} catch (e) {
@@ -275,15 +279,16 @@ const stop = async (process: RunningProcess) => {
await refresh()
}
function goToTerminal(path?: string) {
const selectedPath = path ?? selectedProcess.value?.profile.path
if (!selectedPath) {
function goToTerminal(instanceId?: string) {
const selectedInstanceId = instanceId ?? selectedProcess.value?.instance.id
if (!selectedInstanceId) {
return
}
router.push(`/instance/${encodeURIComponent(selectedPath)}/logs`)
router.push(`/instance/${encodeURIComponent(selectedInstanceId)}/logs`)
}
const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const dismissed = ref(false)
@@ -299,8 +304,17 @@ function getLoadingProgress(loadingBar: LoadingBar): number {
}
function getLoadingText(loadingBar: LoadingBar): string {
const percent = Math.floor(getLoadingProgress(loadingBar) * 100)
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
return loadingBar.message ?? ''
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) {
return null
}
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) {
return icon
}
return convertFileSrc(icon)
}
function getNotification(): PopupNotification | null {
@@ -322,24 +336,33 @@ function removeNotification(): void {
}
function buildDownloadItems(): PopupNotificationProgressItem[] {
return currentLoadingBars.value.map((bar) => ({
id: getLoadingBarKey(bar),
title: bar.title ?? '',
text: getLoadingText(bar),
progress: getLoadingProgress(bar),
waiting: !bar.total || bar.total <= 0,
}))
return [
...installJobNotifications.progressItems.value,
...currentLoadingBars.value.map((bar) => ({
id: getLoadingBarKey(bar),
title: bar.title ?? '',
text: getLoadingText(bar),
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
progress: getLoadingProgress(bar),
waiting: !bar.total || bar.total <= 0,
progressType: 'percentage',
progressCurrent: bar.current,
progressTotal: bar.total,
})),
]
}
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
const hasActiveLoadingBars = computed(() => currentLoadingBars.value.length > 0)
const hasActiveLoadingBars = computed(
() => currentLoadingBars.value.length > 0 || installJobNotifications.active.value,
)
function updateNotification(resummon = false): void {
if (resummon) {
dismissed.value = false
}
if (currentLoadingBars.value.length === 0) {
if (currentLoadingBars.value.length === 0 && !installJobNotifications.active.value) {
removeNotification()
dismissed.value = false
return
@@ -358,17 +381,23 @@ function updateNotification(resummon = false): void {
const progressItems = buildDownloadItems()
if (notif) {
notif.title = formatMessage(messages.downloads)
notif.title = installJobNotifications.active.value
? installJobNotifications.title.value
: formatMessage(messages.downloads)
notif.text = undefined
notif.progressItems = progressItems
notif.buttons = installJobNotifications.buttons.value
notif.progress = undefined
notif.waiting = undefined
} else {
notif = popupNotificationManager.addPopupNotification({
title: formatMessage(messages.downloads),
title: installJobNotifications.active.value
? installJobNotifications.title.value
: formatMessage(messages.downloads),
type: 'download',
autoCloseMs: null,
progressItems,
buttons: installJobNotifications.buttons.value,
})
notificationId.value = notif.id
}
@@ -381,8 +410,8 @@ function formatLoadingBars(loadingBar: LoadingBar): LoadingBar {
version: formatted.bar_type.version,
})
}
if (formatted.bar_type?.profile_path) {
formatted.title = formatted.bar_type.profile_path
if (formatted.bar_type?.instance_id) {
formatted.title = formatted.bar_type.instance_name ?? formatted.bar_type.instance_id
}
if (formatted.bar_type?.pack_name) {
formatted.title = formatted.bar_type.pack_name
@@ -398,7 +427,43 @@ async function refreshLoadingBars() {
currentLoadingBars.value = Object.values(bars)
.map(formatLoadingBars)
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
.filter(
(bar) =>
bar?.bar_type?.type !== 'launcher_update' &&
![
'java_download',
'pack_file_download',
'pack_download',
'minecraft_download',
'copy_instance',
].includes(bar?.bar_type?.type ?? ''),
)
const instanceIds = Array.from(
new Set(
currentLoadingBars.value
.map((bar) => bar.bar_type?.instance_id)
.filter((instanceId): instanceId is string => !!instanceId),
),
)
const instances = instanceIds.length
? await getInstances(instanceIds).catch((error) => {
handleError(error)
return []
})
: []
const instanceIconUrls = new Map(
instances.map((instance) => [instance.id, getDisplayIconUrl(instance.icon_path)]),
)
currentLoadingBarIconUrls.value = Object.fromEntries(
currentLoadingBars.value.map((bar) => {
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
const instanceIconUrl = bar.bar_type?.instance_id
? instanceIconUrls.get(bar.bar_type.instance_id)
: null
return [getLoadingBarKey(bar), barIconUrl ?? instanceIconUrl ?? null]
}),
)
currentLoadingBars.value.sort((a, b) => {
const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}`
@@ -409,6 +474,12 @@ async function refreshLoadingBars() {
updateNotification()
}
const installJobNotifications = await useInstallJobNotifications({
router,
handleError,
onChange: updateNotification,
})
await refreshLoadingBars()
const unlistenLoading = await loading_listener(async () => {
@@ -430,5 +501,6 @@ onBeforeUnmount(() => {
window.removeEventListener('online', handleOnline)
unlistenProcess()
unlistenLoading()
installJobNotifications.dispose()
})
</script>
@@ -12,7 +12,7 @@
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
<hr v-if="option.type === 'divider'" class="divider" />
<div
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
v-else-if="!(isInstanceLink(item) && option.name === `add_content`)"
class="item clickable"
:class="[option.color ?? 'base']"
>
@@ -64,10 +64,10 @@ defineExpose({
},
})
const isLinkedData = (item) => {
if (item.instance != undefined && item.instance.linked_data) {
const isInstanceLink = (item) => {
if (item.instance != undefined && item.instance.link) {
return true
} else if (item != undefined && item.linked_data) {
} else if (item != undefined && item.link) {
return true
}
return false
@@ -16,7 +16,7 @@ import { ChatIcon } from '@/assets/icons'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { install } from '@/helpers/profile.js'
import { install_existing_instance } from '@/helpers/install'
import { cancel_directory_change } from '@/helpers/settings.ts'
import { handleSevereError } from '@/store/error.js'
@@ -72,7 +72,7 @@ defineExpose({
title.value = 'No loader selected'
errorType.value = 'no_loader_version'
supportLink.value = 'https://support.modrinth.com'
metadata.value.profilePath = context.profilePath
metadata.value.instanceId = context.instanceId
} else if (source === 'state_init') {
title.value = 'Error initializing Modrinth App'
errorType.value = 'state_init'
@@ -125,7 +125,7 @@ const loadingRepair = ref(false)
async function repairInstance() {
loadingRepair.value = true
try {
await install(metadata.value.profilePath, false)
await install_existing_instance(metadata.value.instanceId, false)
errorModal.value.hide()
} catch (err) {
handleSevereError(err)
@@ -1,28 +1,32 @@
<script setup>
import { WrenchIcon, XIcon } from '@modrinth/assets'
import { XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
Checkbox,
commonMessages,
defineMessages,
FileTreeSelect,
injectNotificationManager,
NewModal,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
import { readDir, stat } from '@tauri-apps/plugin-fs'
import { ref } from 'vue'
import { PackageIcon, VersionIcon } from '@/assets/icons'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
import { PackageIcon } from '@/assets/icons'
import {
export_instance_mrpack,
get_full_path,
get_pack_export_candidates,
} from '@/helpers/instance'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
header: { id: 'app.export-modal.header', defaultMessage: 'Export modpack' },
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack Name' },
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack name' },
modpackNamePlaceholder: {
id: 'app.export-modal.modpack-name-placeholder',
defaultMessage: 'Modpack name',
@@ -39,15 +43,7 @@ const messages = defineMessages({
id: 'app.export-modal.description-placeholder',
defaultMessage: 'Enter modpack description...',
},
selectFilesLabel: {
id: 'app.export-modal.select-files-label',
defaultMessage: 'Configure which files are included in this export',
},
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
includeFile: {
id: 'app.export-modal.include-file-accessibility-label',
defaultMessage: 'Include "{file}"?',
},
})
const props = defineProps({
@@ -59,8 +55,9 @@ const props = defineProps({
defineExpose({
show: () => {
resetExportState()
exportModal.value.show()
initFiles()
void initFiles().catch(handleError)
},
})
@@ -69,67 +66,33 @@ const nameInput = ref(props.instance.name)
const exportDescription = ref('')
const versionInput = ref('1.0.0')
const files = ref([])
const folders = ref([])
const selectedFilePaths = ref([])
const fileTreeKey = ref(0)
const filesLoadId = ref(0)
const instanceRoot = ref('')
const loadedDirectories = ref(new Set())
const initFiles = async () => {
const newFolders = new Map()
const sep = '/'
files.value = []
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
filePaths
.map((folder) => ({
path: folder,
name: folder.split(sep).pop(),
selected:
folder.startsWith('mods') ||
folder.startsWith('datapacks') ||
folder.startsWith('resourcepacks') ||
folder.startsWith('shaderpacks') ||
folder.startsWith('config'),
disabled:
folder === 'profile.json' ||
folder.startsWith('modrinth_logs') ||
folder.startsWith('.fabric'),
}))
.filter(
(pathData) =>
!pathData.path.includes('.DS_Store') &&
pathData.path !== 'mods/.connector' &&
!pathData.path.startsWith('mods/.connector/'),
)
.forEach((pathData) => {
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
if (parent !== '') {
if (newFolders.has(parent)) {
newFolders.get(parent).push(pathData)
} else {
newFolders.set(parent, [pathData])
}
} else {
files.value.push(pathData)
}
}),
)
folders.value = [...newFolders.entries()].map(([name, value]) => [
{
name,
showingMore: false,
},
value,
async function initFiles() {
const loadId = ++filesLoadId.value
const [filePaths, root] = await Promise.all([
get_pack_export_candidates(props.instance.id),
get_full_path(props.instance.id),
])
if (loadId !== filesLoadId.value) return
instanceRoot.value = root
const exportCandidates = await Promise.all(
filePaths.map((path) => buildExportCandidateItem(root, path)),
)
if (loadId !== filesLoadId.value) return
files.value = exportCandidates
selectedFilePaths.value = files.value
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
.map((file) => file.path)
}
await initFiles()
const exportPack = async () => {
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
folders.value.forEach((args) => {
args[1].forEach((child) => {
if (child.selected) {
filesToExport.push(child.path)
}
})
})
const outputPath = await save({
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
filters: [
@@ -141,10 +104,10 @@ const exportPack = async () => {
})
if (outputPath) {
export_profile_mrpack(
props.instance.path,
export_instance_mrpack(
props.instance.id,
outputPath,
filesToExport,
selectedFilePaths.value,
versionInput.value,
exportDescription.value,
nameInput.value,
@@ -152,94 +115,176 @@ const exportPack = async () => {
exportModal.value.hide()
}
}
function resetExportState() {
nameInput.value = props.instance.name
exportDescription.value = ''
versionInput.value = '1.0.0'
files.value = []
selectedFilePaths.value = []
fileTreeKey.value += 1
instanceRoot.value = ''
loadedDirectories.value = new Set()
}
async function loadExportDirectory(path) {
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
const loadId = filesLoadId.value
loadedDirectories.value.add(path)
try {
const entries = await readDir(`${instanceRoot.value}/${path}`)
const childItems = await Promise.all(
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
)
if (loadId !== filesLoadId.value) return
appendExportItems(childItems)
} catch {
loadedDirectories.value.delete(path)
}
}
async function buildExportCandidateItem(instanceRoot, path) {
try {
const entries = await readDir(`${instanceRoot}/${path}`)
const metadata = await getExportCandidateMetadata(instanceRoot, path)
return {
path,
type: 'directory',
disabled: isExportCandidateDisabled(path),
modified: metadata.modified,
count: entries.length,
}
} catch {
return buildExportFileItem(instanceRoot, path)
}
}
async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) {
const path = `${parentPath}/${entry.name}`
if (entry.isDirectory) {
const metadata = await getExportCandidateMetadata(instanceRoot, path)
return {
path,
type: 'directory',
disabled: isExportCandidateDisabled(path),
modified: metadata.modified,
}
}
return buildExportFileItem(instanceRoot, path)
}
async function buildExportFileItem(instanceRoot, path) {
const metadata = await getExportCandidateMetadata(instanceRoot, path)
return {
path,
type: 'file',
disabled: isExportCandidateDisabled(path),
size: metadata.size,
modified: metadata.modified,
}
}
function appendExportItems(items) {
const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file]))
for (const item of items) {
nextFiles.set(normalizeExportPath(item.path), item)
}
files.value = [...nextFiles.values()]
}
async function getExportCandidateMetadata(instanceRoot, path) {
try {
const metadata = await stat(`${instanceRoot}/${path}`)
return {
size: metadata.size,
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
}
} catch {
return {}
}
}
function normalizeExportPath(path) {
return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/')
}
function isDefaultSelectedExportCandidate(path) {
return (
path.startsWith('mods') ||
path.startsWith('datapacks') ||
path.startsWith('resourcepacks') ||
path.startsWith('shaderpacks') ||
path.startsWith('config')
)
}
function isExportCandidateDisabled(path) {
return (
path === 'profile.json' ||
path.startsWith('modrinth_logs') ||
path.startsWith('.fabric') ||
path.startsWith('__MACOSX')
)
}
</script>
<template>
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
<div class="flex flex-col gap-4 w-[40rem]">
<NewModal
ref="exportModal"
:header="formatMessage(messages.header)"
scrollable
width="46rem"
max-width="calc(100vw - 2rem)"
>
<div class="flex flex-col gap-4">
<div class="grid grid-cols-2 gap-4">
<div class="labeled_input">
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
<div class="labeled_input w-full">
<p class="text-contrast font-semibold">{{ formatMessage(messages.modpackNameLabel) }}</p>
<StyledInput
v-model="nameInput"
:icon="PackageIcon"
type="text"
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
clearable
wrapper-class="w-full"
/>
</div>
<div class="labeled_input">
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
<div class="labeled_input w-full">
<p class="text-contrast font-semibold">
{{ formatMessage(messages.versionNumberLabel) }}
</p>
<StyledInput
v-model="versionInput"
:icon="VersionIcon"
type="text"
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
clearable
wrapper-class="w-full"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
<div class="flex flex-col gap-2 min-w-0">
<p class="m-0 text-contrast font-semibold">
{{ formatMessage(commonMessages.descriptionLabel) }}
</p>
<StyledInput
v-model="exportDescription"
multiline
:placeholder="formatMessage(messages.descriptionPlaceholder)"
wrapper-class="w-full"
/>
</div>
<Accordion
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
>
<template #title>
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
Configure which files are included in this export
</span>
</template>
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
<Accordion
class="flex flex-col"
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
>
<template #title>
<Checkbox
:model-value="children.every((child) => child.selected)"
:indeterminate="
!children.every((child) => child.selected) &&
children.some((child) => child.selected)
"
:description="formatMessage(messages.includeFile, { file: path.name })"
class="pl-4 py-2"
:disabled="children.every((x) => x.disabled)"
@update:model-value="
(newValue) => children.forEach((child) => (child.selected = newValue))
"
@click.stop
/>
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
</template>
<div v-for="child in children" :key="child.path">
<Checkbox
v-model="child.selected"
:label="child.name"
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
:disabled="child.disabled"
/>
</div>
</Accordion>
</div>
<Checkbox
v-for="file in files"
:key="file.path"
v-model="file.selected"
:label="file.name"
:disabled="file.disabled"
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
/>
</div>
</Accordion>
<FileTreeSelect
:key="fileTreeKey"
v-model="selectedFilePaths"
class="min-w-0"
:items="files"
@navigate="loadExportDirectory"
/>
</div>
<template #actions>
<div class="flex items-center justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="exportModal.hide">
@@ -254,6 +299,6 @@ const exportPack = async () => {
</button>
</ButtonStyled>
</div>
</div>
</ModalWrapper>
</template>
</NewModal>
</template>
@@ -15,9 +15,10 @@ import { useRouter } from 'vue-router'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, kill, run } from '@/helpers/profile'
import { showProfileInFolder } from '@/helpers/utils.js'
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'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
const { handleError } = injectNotificationManager()
@@ -54,11 +55,11 @@ const installed = computed(() => props.instance.install_stage === 'installed')
const router = useRouter()
const seeInstance = async () => {
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
await router.push(`/instance/${encodeURIComponent(props.instance.id)}`)
}
const checkProcess = async () => {
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
const runningProcesses = await get_by_instance_id(props.instance.id).catch(handleError)
playing.value = runningProcesses.length > 0
}
@@ -66,8 +67,8 @@ const checkProcess = async () => {
const play = async (e, context) => {
e?.stopPropagation()
loading.value = true
await run(props.instance.path)
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
.finally(() => {
trackEvent('InstanceStart', {
loader: props.instance.loader,
@@ -82,7 +83,7 @@ const stop = async (e, context) => {
e?.stopPropagation()
playing.value = false
await kill(props.instance.path).catch(handleError)
await kill(props.instance.id).catch(handleError)
trackEvent('InstanceStop', {
loader: props.instance.loader,
@@ -94,17 +95,30 @@ const stop = async (e, context) => {
const repair = async (e) => {
e?.stopPropagation()
await finish_install(props.instance).catch(handleError)
if (
props.instance.install_stage !== 'pack_installed' &&
(props.instance.link?.type === 'modrinth_modpack' ||
props.instance.link?.type === 'server_project_modpack')
) {
await install_pack_to_existing_instance(props.instance.id, {
type: 'fromVersionId',
project_id: props.instance.link.project_id ?? props.instance.link.server_project_id ?? '',
version_id: props.instance.link.version_id ?? props.instance.link.content_version_id ?? '',
title: props.instance.name,
}).catch(handleError)
} else {
await install_existing_instance(props.instance.id, false).catch(handleError)
}
}
const openFolder = async () => {
await showProfileInFolder(props.instance.path)
await showInstanceInFolder(props.instance.id)
}
const addContent = async () => {
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.path },
query: { i: props.instance.id },
})
}
@@ -120,7 +134,7 @@ defineExpose({
const currentEvent = ref(null)
const unlisten = await process_listener((e) => {
if (e.profile_path_id === props.instance.path) {
if (e.instance_id === props.instance.id) {
currentEvent.value = e.event
if (e.event === 'finished') {
playing.value = false
@@ -142,7 +156,7 @@ onUnmounted(() => unlisten())
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
:tint-by="instance.id"
alt="Mod card"
/>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
@@ -191,7 +205,7 @@ onUnmounted(() => unlisten())
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
:tint-by="instance.id"
alt="Mod card"
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
@@ -22,7 +22,7 @@ const props = withDefaults(
)
const instanceLink = computed(() => {
const base = `/instance/${encodeURIComponent(props.instance.path)}`
const base = `/instance/${encodeURIComponent(props.instance.id)}`
return props.backTab ? `${base}/${props.backTab}` : base
})
</script>
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue'
import { SwapIcon } from '@/assets/icons/index.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { update_managed_modrinth_version } from '@/helpers/profile'
import { update_managed_modrinth_version } from '@/helpers/instance'
import { releaseColor } from '@/helpers/utils'
const props = defineProps({
@@ -32,14 +32,14 @@ const filteredVersions = computed(() => {
})
const modpackVersionModal = ref(null)
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installedVersion = computed(() => props.instance?.link?.version_id)
const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false)
const switchVersion = async (versionId) => {
modpackVersionModal.value.hide()
inProgress.value = true
await update_managed_modrinth_version(props.instance.path, versionId)
await update_managed_modrinth_version(props.instance.id, versionId)
inProgress.value = false
emit('finish-install')
}
@@ -59,7 +59,7 @@ const onHide = () => {
:on-hide="onHide"
>
<div class="modal-body">
<div v-if="instance.linked_data" class="mod-card">
<div v-if="instance.link" class="mod-card">
<div class="table">
<div class="table-row with-columns table-head">
<div class="table-cell table-text download-cell" />
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { CalendarIcon, UsersIcon, XIcon } from '@modrinth/assets'
import { injectModrinthClient, ProgressBar } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref } from 'vue'
const DISMISSED_STORAGE_KEY = 'pride-fundraiser-2026-dismissed'
const client = injectModrinthClient()
const dismissed = ref(localStorage.getItem(DISMISSED_STORAGE_KEY) === 'true')
const { data: campaignInfo } = useQuery({
queryKey: ['campaign', 'pride-26'],
queryFn: () => client.labrinth.campaign_internal.getPride26(),
enabled: () => !dismissed.value,
staleTime: 15 * 60 * 1000,
refetchInterval: 15 * 60 * 1000,
retry: false,
})
const shouldShowBanner = computed(
() => !dismissed.value && Number(campaignInfo.value?.target_usd) > 0,
)
async function openPrideFundraiser() {
await openUrl('https://modrinth.com/pride?from=app')
}
function dismissBanner() {
dismissed.value = true
localStorage.setItem(DISMISSED_STORAGE_KEY, 'true')
}
function formatUsd(amount: string | number) {
return Number(amount).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
})
}
function daysLeft() {
return Math.max(
0,
Math.ceil((new Date('2026-07-01T00:00:00Z').getTime() - Date.now()) / (24 * 60 * 60 * 1000)),
)
}
</script>
<template>
<div v-if="shouldShowBanner && campaignInfo">
<section
role="link"
tabindex="0"
class="flex w-full cursor-pointer flex-col gap-3 rounded-xl border border-solid border-surface-5 bg-button-bg p-3 text-primary transition-[border-color,filter] hover:border-surface-6 hover:brightness-125 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
aria-label="Open Pride fundraiser"
@click="openPrideFundraiser"
@keydown.enter="openPrideFundraiser"
@keydown.space.prevent="openPrideFundraiser"
>
<div class="flex w-full items-center justify-between gap-2">
<h2 class="m-0 min-w-0 truncate text-base font-semibold text-contrast">
Pride Fundraiser 2026
</h2>
<button
type="button"
class="m-0 flex size-5 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-primary transition-colors hover:text-contrast focus-visible:text-contrast"
aria-label="Dismiss Pride fundraiser"
@click.stop="dismissBanner"
@keydown.stop
>
<XIcon aria-hidden="true" class="size-5" />
</button>
</div>
<div class="h-px w-full bg-surface-5" />
<div class="flex w-full flex-col gap-2.5">
<div class="flex items-end gap-1 whitespace-nowrap">
<span class="text-base font-semibold leading-5 text-contrast">
{{ formatUsd(campaignInfo.total_donations_usd) }}
</span>
<span class="text-xs font-medium leading-4">
of {{ formatUsd(campaignInfo.target_usd) }}
</span>
</div>
<ProgressBar
class="pride-fundraiser-banner__progress"
:progress="Number(campaignInfo.total_donations_usd)"
:max="Number(campaignInfo.target_usd)"
color="purple"
full-width
:gradient-border="false"
:aria-label="`${formatUsd(campaignInfo.total_donations_usd)} of ${formatUsd(
campaignInfo.target_usd,
)} raised`"
/>
<div class="flex flex-wrap items-center gap-2 text-xs font-medium leading-4">
<span class="flex items-center gap-1">
<UsersIcon aria-hidden="true" class="size-4 shrink-0" />
{{ campaignInfo.num_donators.toLocaleString('en-US') }}
{{ campaignInfo.num_donators === 1 ? 'supporter' : 'supporters' }}
</span>
<span class="flex items-center gap-1">
<CalendarIcon aria-hidden="true" class="size-4 shrink-0" />
{{ daysLeft() }} {{ daysLeft() === 1 ? 'day left' : 'days left' }}
</span>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.pride-fundraiser-banner__progress :deep(.progress-bar) {
background: linear-gradient(
90deg,
var(--color-red) 0%,
var(--color-orange) 20%,
var(--color-green) 50%,
var(--color-blue) 75%,
var(--color-purple) 100%
);
}
</style>
@@ -6,16 +6,16 @@ import dayjs from 'dayjs'
import { onUnmounted, ref } from 'vue'
import NavButton from '@/components/ui/NavButton.vue'
import { profile_listener } from '@/helpers/events.js'
import { list } from '@/helpers/profile'
import { instance_listener } from '@/helpers/events.js'
import { list } from '@/helpers/instance'
const { handleError } = injectNotificationManager()
const recentInstances = ref([])
const getInstances = async () => {
const profiles = await list().catch(handleError)
const instances = await list().catch(handleError)
recentInstances.value = profiles
recentInstances.value = instances
.sort((a, b) => {
const dateACreated = dayjs(a.created)
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
@@ -37,24 +37,24 @@ const getInstances = async () => {
await getInstances()
const unlistenProfile = await profile_listener(async (event) => {
const unlistenInstance = await instance_listener(async (event) => {
if (event.event !== 'synced') {
await getInstances()
}
})
onUnmounted(() => {
unlistenProfile()
unlistenInstance()
})
</script>
<template>
<div v-for="instance in recentInstances" :key="instance.id" v-tooltip.right="instance.name">
<NavButton :to="`/instance/${encodeURIComponent(instance.path)}`" class="relative">
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
size="28px"
:tint-by="instance.path"
:tint-by="instance.id"
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div
@@ -65,10 +65,7 @@ onUnmounted(() => {
</div>
</NavButton>
</div>
<div
v-if="instances && recentInstances.length > 0"
class="h-px w-6 mx-auto my-2 bg-divider"
></div>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,124 @@
<template>
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
<button
v-if="showUpdatePill"
type="button"
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out"
:class="{
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
}"
:disabled="isUpdateDownloading"
:aria-busy="isUpdateDownloading"
@click="handleUpdateClick"
>
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
<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>{{ updateLabel }}</span>
</button>
</ButtonStyled>
</template>
<script setup lang="ts">
import { DownloadIcon, RefreshCwIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import {
appUpdateState,
downloadAvailableAppUpdate,
installAvailableAppUpdate,
} from '@/providers/app-update'
const { formatMessage } = useVIntl()
const messages = defineMessages({
update: {
id: 'app.action-bar.update',
defaultMessage: 'Update',
},
downloadingUpdate: {
id: 'app.action-bar.downloading-update',
defaultMessage: 'Downloading update',
},
reloadToUpdate: {
id: 'app.action-bar.reload-to-update',
defaultMessage: 'Reload to update',
},
})
const {
downloading,
downloadPercent,
downloadProgress,
finishedDownloading,
isVisible: isUpdateVisible,
metered,
restarting,
} = appUpdateState
const isUpdateDownloading = computed(
() =>
downloading.value ||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
)
const showUpdatePill = computed(
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
)
const animateReadyPill = ref(false)
const updateLabel = computed(() => {
if (isUpdateDownloading.value) {
return formatMessage(messages.downloadingUpdate)
}
if (finishedDownloading.value) {
return formatMessage(messages.reloadToUpdate)
}
return formatMessage(messages.update)
})
let readyPillAnimationFrame: number | null = null
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
if (readyPillAnimationFrame !== null) {
cancelAnimationFrame(readyPillAnimationFrame)
readyPillAnimationFrame = null
}
if (!show || !ready) {
animateReadyPill.value = false
return
}
if (wasShown && wasReady) {
return
}
animateReadyPill.value = false
await nextTick()
readyPillAnimationFrame = requestAnimationFrame(() => {
animateReadyPill.value = true
readyPillAnimationFrame = null
})
})
async function handleUpdateClick() {
if (isUpdateDownloading.value) {
return
}
if (finishedDownloading.value) {
await installAvailableAppUpdate()
} else {
await downloadAvailableAppUpdate()
}
}
onBeforeUnmount(() => {
if (readyPillAnimationFrame !== null) {
cancelAnimationFrame(readyPillAnimationFrame)
}
})
</script>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
@@ -301,11 +301,12 @@ const messages = defineMessages({
</ButtonStyled>
<StyledInput
v-model="search"
:icon="SearchIcon"
type="text"
:placeholder="formatMessage(messages.searchFriends)"
clearable
variant="outlined"
wrapper-class="flex-1"
input-class="!bg-transparent !border !border-solid !border-button-bg !text-primary !placeholder:text-primary"
wrapper-class="flex-1 [&>svg]:!text-primary [&>svg]:!opacity-100"
@keyup.esc="search = ''"
/>
</template>
@@ -393,7 +394,6 @@ const messages = defineMessages({
<FriendsSection
v-if="pendingFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="pendingFriends"
:heading="formatMessage(messages.pending)"
:remove-friend="removeFriend"
@@ -7,26 +7,28 @@ import {
injectNotificationManager,
StyledInput,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { list } from '@/helpers/profile'
import { add_server_to_profile, get_profile_worlds } from '@/helpers/worlds.ts'
import { list } from '@/helpers/instance'
import { add_server_to_instance, get_instance_worlds } from '@/helpers/worlds.ts'
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const modal = ref()
const searchFilter = ref('')
const profiles = ref([])
const instances = ref([])
const serverName = ref('')
const serverAddress = ref('')
const shownProfiles = computed(() =>
profiles.value.filter((profile) => {
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
const shownInstances = computed(() =>
instances.value.filter((instance) => {
return instance.name.toLowerCase().includes(searchFilter.value.toLowerCase())
}),
)
@@ -36,15 +38,15 @@ defineExpose({
serverAddress.value = address
searchFilter.value = ''
const profilesVal = await list().catch(handleError)
const instanceValues = await list().catch(handleError)
await Promise.allSettled(
profilesVal.map(async (profile) => {
profile.adding = false
profile.added = false
instanceValues.map(async (instance) => {
instance.adding = false
instance.added = false
try {
const worlds = await get_profile_worlds(profile.path)
profile.added = worlds.some(
const worlds = await get_instance_worlds(instance.id)
instance.added = worlds.some(
(w) => w.type === 'server' && w.address === serverAddress.value,
)
} catch {
@@ -53,28 +55,29 @@ defineExpose({
}),
)
profiles.value = profilesVal
instances.value = instanceValues
modal.value.show()
trackEvent('AddServerToInstanceStart', { source: 'AddServerToInstanceModal' })
},
})
async function addServer(profile) {
profile.adding = true
async function addServer(instance) {
instance.adding = true
try {
await add_server_to_profile(profile.path, serverName.value, serverAddress.value, 'prompt')
profile.added = true
await add_server_to_instance(instance.id, serverName.value, serverAddress.value, 'prompt')
instance.added = true
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.id] })
trackEvent('AddServerToInstance', {
server_name: serverName.value,
instance_name: profile.name,
instance_name: instance.name,
source: 'AddServerToInstanceModal',
})
} catch (err) {
handleError(err)
}
profile.adding = false
instance.adding = false
}
</script>
@@ -91,26 +94,26 @@ async function addServer(profile) {
/>
<div class="max-h-[21rem] overflow-y-auto">
<div
v-for="profile in shownProfiles"
:key="profile.path"
v-for="instance in shownInstances"
:key="instance.id"
class="flex w-full items-center justify-between gap-2 bg-bg-raised text-icon shadow-none"
>
<router-link
class="btn btn-transparent p-2 text-left"
:to="`/instance/${encodeURIComponent(profile.path)}`"
:to="`/instance/${encodeURIComponent(instance.id)}`"
@click="modal.hide()"
>
<Avatar
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
class="mr-2 [--size:2rem]"
/>
{{ profile.name }}
{{ instance.name }}
</router-link>
<ButtonStyled>
<button :disabled="profile.added || profile.adding" @click="addServer(profile)">
<PlusIcon v-if="!profile.added && !profile.adding" />
<CheckIcon v-else-if="profile.added" />
{{ profile.adding ? 'Adding...' : profile.added ? 'Added' : 'Add' }}
<button :disabled="instance.added || instance.adding" @click="addServer(instance)">
<PlusIcon v-if="!instance.added && !instance.adding" />
<CheckIcon v-else-if="instance.added" />
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
</button>
</ButtonStyled>
</div>
@@ -1,185 +0,0 @@
<template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
installed.
</p>
<table>
<thead>
<tr class="header">
<th>{{ instance?.name }}</th>
<th>{{ project.title }}</th>
</tr>
</thead>
<tbody>
<tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<Combobox
v-if="versions?.length > 1"
v-model="selectedVersionId"
:options="versionOptions"
:searchable="true"
placeholder="Select version"
force-direction="up"
:max-height="150"
/>
<span v-else>
<span>{{ selectedVersionLabel }}</span>
</span>
</td>
</tr>
</tbody>
</table>
<div class="button-group">
<ButtonStyled type="outlined">
<button @click="() => incompatibleModal.hide()"><XIcon />Cancel</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="installing" @click="install()">
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
</button>
</ButtonStyled>
</div>
</div>
</ModalWrapper>
</template>
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
formatLoader,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { add_project_from_version as installMod } from '@/helpers/profile'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const instance = ref(null)
const project = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
const selectedVersionLabel = computed(() => {
if (!selectedVersion.value) return ''
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
})
const versionOptions = computed(() =>
(versions.value ?? []).map((version) => ({
value: version.id,
label: `${version.name} (${version.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version.game_versions.join(', ')})`,
})),
)
const selectedVersionId = computed({
get: () => selectedVersion.value?.id ?? null,
set: (value) => {
if (!value) return
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
},
})
defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal
versions.value = projectVersions ?? []
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
project.value = projectVal
onInstall.value = callback
installing.value = false
incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
const install = async () => {
installing.value = true
await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError)
installing.value = false
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
trackEvent('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
</script>
<style lang="scss" scoped>
.data {
text-transform: capitalize;
}
table {
width: 100%;
border-radius: var(--radius-lg);
border-collapse: collapse;
box-shadow: 0 0 0 1px var(--color-button-bg);
}
th {
text-align: left;
padding: 1rem;
background-color: var(--color-bg);
overflow: hidden;
border-bottom: 1px solid var(--color-button-bg);
}
th:first-child {
border-top-left-radius: var(--radius-lg);
border-right: 1px solid var(--color-button-bg);
}
th:last-child {
border-top-right-radius: var(--radius-lg);
}
td {
padding: 1rem;
}
td:first-child {
border-right: 1px solid var(--color-button-bg);
}
.button-group {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
@@ -4,12 +4,14 @@ import {
Avatar,
ButtonStyled,
Checkbox,
Chips,
defineMessages,
injectNotificationManager,
OverflowMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, type Ref, ref, watch } from 'vue'
@@ -17,7 +19,8 @@ import { useRouter } from 'vue-router'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
import { install_duplicate_instance } from '@/helpers/install'
import { edit, edit_icon, list, remove } from '@/helpers/instance'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
@@ -25,21 +28,29 @@ import type { GameInstance } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const router = useRouter()
const queryClient = useQueryClient()
const deleteConfirmModal = ref()
const { instance } = injectInstanceSettings()
type ReleaseChannel = GameInstance['update_channel']
const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
const title = ref(instance.value.name)
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
const groups = ref([...instance.value.groups])
const savingReleaseChannel = ref(false)
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.update_channel)
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
)
const newCategoryInput = ref('')
const installing = computed(() => instance.value.install_stage !== 'installed')
async function duplicateProfile() {
await duplicate(instance.value.path).catch(handleError)
async function duplicateInstance() {
await install_duplicate_instance(instance.value.id).catch(handleError)
trackEvent('InstanceDuplicate', {
loader: instance.value.loader,
game_version: instance.value.game_version,
@@ -51,9 +62,55 @@ const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
function formatReleaseChannelLabel(channel: ReleaseChannel) {
switch (channel) {
case 'release':
return formatMessage(messages.updateChannelRelease)
case 'beta':
return formatMessage(messages.updateChannelBeta)
case 'alpha':
return formatMessage(messages.updateChannelAlpha)
}
}
function formatReleaseChannelDescription(channel: ReleaseChannel) {
switch (channel) {
case 'release':
return formatMessage(messages.updateChannelReleaseDescription)
case 'beta':
return formatMessage(messages.updateChannelBetaDescription)
case 'alpha':
return formatMessage(messages.updateChannelAlphaDescription)
}
}
watch(
() => [instance.value.id, instance.value.update_channel] as const,
() => {
if (!savingReleaseChannel.value) {
selectedReleaseChannel.value = instance.value.update_channel
}
},
)
watch(selectedReleaseChannel, async (channel, previousChannel) => {
const previousReleaseChannel = previousChannel ?? instance.value.update_channel
if (channel === instance.value.update_channel) return
savingReleaseChannel.value = true
const instanceId = instance.value.id
await edit(instanceId, { update_channel: channel })
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instanceId] }))
.catch((error) => {
selectedReleaseChannel.value = previousReleaseChannel
handleError(error)
})
savingReleaseChannel.value = false
})
async function resetIcon() {
icon.value = undefined
await edit_icon(instance.value.path, null).catch(handleError)
await edit_icon(instance.value.id, null).catch(handleError)
trackEvent('InstanceRemoveIcon')
}
@@ -71,12 +128,12 @@ async function setIcon() {
if (!value) return
icon.value = value
await edit_icon(instance.value.path, icon.value).catch(handleError)
await edit_icon(instance.value.id, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
}
const editProfileObject = computed(() => ({
const editInstanceObject = computed(() => ({
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
}))
@@ -102,15 +159,15 @@ watch(
[title, groups, groups],
async () => {
if (removing.value) return
await edit(instance.value.path, editProfileObject.value).catch(handleError)
await edit(instance.value.id, editInstanceObject.value).catch(handleError)
},
{ deep: true },
)
const removing = ref(false)
async function removeProfile() {
async function removeInstance() {
removing.value = true
const path = instance.value.path
const path = instance.value.id
trackEvent('InstanceRemove', {
loader: instance.value.loader,
@@ -175,6 +232,38 @@ const messages = defineMessages({
id: 'instance.settings.tabs.general.duplicate-button',
defaultMessage: 'Duplicate',
},
updateChannel: {
id: 'instance.settings.tabs.general.update-channel',
defaultMessage: 'Update channel',
},
updateChannelReleaseDescription: {
id: 'instance.settings.tabs.general.update-channel.release.description',
defaultMessage: 'Only release versions will be shown as available updates.',
},
updateChannelBetaDescription: {
id: 'instance.settings.tabs.general.update-channel.beta.description',
defaultMessage: 'Release and beta versions will be shown as available updates.',
},
updateChannelAlphaDescription: {
id: 'instance.settings.tabs.general.update-channel.alpha.description',
defaultMessage: 'Release, beta, and alpha versions will be shown as available updates.',
},
updateChannelRelease: {
id: 'instance.settings.tabs.general.update-channel.release',
defaultMessage: 'Release',
},
updateChannelBeta: {
id: 'instance.settings.tabs.general.update-channel.beta',
defaultMessage: 'Beta',
},
updateChannelAlpha: {
id: 'instance.settings.tabs.general.update-channel.alpha',
defaultMessage: 'Alpha',
},
selectUpdateChannelAriaLabel: {
id: 'instance.settings.tabs.general.update-channel.select',
defaultMessage: 'Select update channel',
},
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
@@ -196,7 +285,7 @@ const messages = defineMessages({
</script>
<template>
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeProfile" />
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeInstance" />
<div class="block">
<div class="float-end ml-10 relative group w-fit">
<div class="flex flex-col gap-1">
@@ -222,7 +311,7 @@ const messages = defineMessages({
:src="icon ? convertFileSrc(icon) : icon"
size="108px"
class="transition-[filter] group-hover:brightness-75"
:tint-by="instance.path"
:tint-by="instance.id"
no-shadow
/>
<div
@@ -262,7 +351,7 @@ const messages = defineMessages({
aria-labelledby="duplicate-instance-label"
:disabled="installing"
class="w-max !shadow-none"
@click="duplicateProfile"
@click="duplicateInstance"
>
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
</button>
@@ -304,6 +393,23 @@ const messages = defineMessages({
</p>
</div>
<div class="flex flex-col gap-2.5 mt-6">
<h2 class="m-0 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.updateChannel) }}
</h2>
<Chips
v-model="selectedReleaseChannel"
:items="releaseChannelOptions"
:format-label="formatReleaseChannelLabel"
:capitalize="false"
:disabled-items="releaseChannelDisabledItems"
:aria-label="formatMessage(messages.selectUpdateChannelAriaLabel)"
/>
<p class="m-0">
{{ formatReleaseChannelDescription(selectedReleaseChannel) }}
</p>
</div>
<div class="flex flex-col gap-2.5 mt-6">
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.deleteInstance) }}
@@ -8,7 +8,7 @@ import {
} from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { edit } from '@/helpers/profile'
import { edit } from '@/helpers/instance'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
@@ -28,21 +28,21 @@ const overrideHooks = ref(
)
const hooks = ref(instance.value.hooks ?? globalSettings.hooks)
const editProfileObject = computed(() => {
const editProfile: {
const editInstanceObject = computed(() => {
const editInstancePatch: {
hooks?: Hooks
} = {}
// When hooks are not overridden per-instance, we want to clear them
editProfile.hooks = overrideHooks.value ? hooks.value : {}
editInstancePatch.hooks = overrideHooks.value ? hooks.value : {}
return editProfile
return editInstancePatch
})
watch(
[overrideHooks, hooks],
async () => {
await edit(instance.value.path, editProfileObject.value)
await edit(instance.value.id, editInstanceObject.value)
},
{ deep: true },
)
@@ -4,80 +4,124 @@ import {
commonMessages,
defineMessages,
formatLoaderLabel,
injectFilePicker,
injectNotificationManager,
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, shallowRef } from 'vue'
import { computed, ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache'
import { get_loader_versions } from '@/helpers/metadata'
import {
duplicate,
install_duplicate_instance,
install_existing_instance,
install_pack_to_existing_instance,
installJobInstanceId,
wait_for_install_job,
} from '@/helpers/install'
import {
edit,
get_linked_modpack_info,
install,
list,
update_managed_modrinth_version,
update_repair_modrinth,
} from '@/helpers/profile'
} from '@/helpers/instance'
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { injectInstanceSettings } from '@/providers/instance-settings'
import { useTheming } from '@/store/state'
import type { Manifest } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const filePicker = injectFilePicker()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
const themeStore = useTheming()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const skipNonEssentialWarnings = computed(() =>
themeStore.getFeatureFlag('skip_non_essential_warnings'),
)
const [
fabric_versions,
forge_versions,
quilt_versions,
neoforge_versions,
all_game_versions,
loaders,
] = await Promise.all([
get_loader_versions('fabric')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('forge')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('quilt')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('neo')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_game_versions()
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
.catch(handleError),
get_loaders()
.then((value: PlatformTag[]) =>
value
.filter(
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
)
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
.then((loader: PlatformTag[]) => ref(loader))
.catch(handleError),
])
const { data: modpackInfo } = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
enabled: computed(() => !!instance.value.linked_data?.project_id && !offline),
debug('metadata load: start', {
instanceId: instance.value.id,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
installStage: instance.value.install_stage,
})
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
const fabricVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric') as Promise<Manifest>,
})
const forgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge') as Promise<Manifest>,
})
const quiltVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt') as Promise<Manifest>,
})
const neoforgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo') as Promise<Manifest>,
})
const gameVersionsQuery = useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: () => get_game_versions() as Promise<GameVersionTag[]>,
})
const loadersQuery = useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
const metadataLoading = computed(() =>
[
fabricVersionsQuery,
forgeVersionsQuery,
quiltVersionsQuery,
neoforgeVersionsQuery,
gameVersionsQuery,
loadersQuery,
].some((query) => query.isLoading.value),
)
debug('metadata queries configured', {
instanceId: instance.value.id,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
})
const isModrinthLinkedModpack = computed(
() =>
instance.value.link?.type === 'modrinth_modpack' ||
instance.value.link?.type === 'server_project_modpack',
)
const isImportedModpack = computed(() => instance.value.link?.type === 'imported_modpack')
const modpackInfoQuery = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.id]),
queryFn: () => get_linked_modpack_info(instance.value.id, 'must_revalidate'),
enabled: computed(() => isModrinthLinkedModpack.value && !offline),
})
const modpackInfo = modpackInfoQuery.data
const repairing = ref(false)
const reinstalling = ref(false)
@@ -89,32 +133,60 @@ const messages = defineMessages({
})
function getManifest(loader: string) {
const map: Record<string, typeof fabric_versions> = {
fabric: fabric_versions,
forge: forge_versions,
quilt: quilt_versions,
neoforge: neoforge_versions,
const map: Record<string, Manifest | undefined> = {
fabric: fabricVersionsQuery.data.value,
forge: forgeVersionsQuery.data.value,
quilt: quiltVersionsQuery.data.value,
neoforge: neoforgeVersionsQuery.data.value,
}
return map[loader]
const manifest = map[loader]
debug('getManifest:', {
loader,
hasManifest: !!manifest,
gameVersions: manifest?.gameVersions?.length ?? 0,
})
return manifest
}
async function installLocalModpackFromPicker() {
const picked = await filePicker.pickModpackFile({ readFile: false })
if (!picked?.path) return false
const job = await install_pack_to_existing_instance(instance.value.id, {
type: 'fromFile',
path: picked.path,
}).catch(handleError)
if (!job) return false
const completed = await wait_for_install_job(job.job_id).catch(handleError)
return !!completed
}
provideAppBackup({
async createBackup() {
const allProfiles = await list()
debug('createBackup: start', {
instanceId: instance.value.id,
instanceName: instance.value.name,
})
const allInstances = await list()
const prefix = `${instance.value.name} - Backup #`
const existingNums = allProfiles
const existingNums = allInstances
.filter((p) => p.name.startsWith(prefix))
.map((p) => parseInt(p.name.slice(prefix.length), 10))
.filter((n) => !isNaN(n))
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
const newPath = await duplicate(instance.value.path)
await edit(newPath, { name: `${prefix}${nextNum}` })
const job = await install_duplicate_instance(instance.value.id)
const newInstanceId = installJobInstanceId(job)
if (newInstanceId) {
await edit(newInstanceId, { name: `${prefix}${nextNum}` })
}
debug('createBackup: done', { newInstanceId, backupName: `${prefix}${nextNum}` })
},
})
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
loading: computed(() => metadataLoading.value || modpackInfoQuery.isLoading.value),
installationInfo: computed(() => {
const rows = [
{
@@ -136,7 +208,7 @@ provideInstallationSettings({
}
return rows
}),
isLinked: computed(() => !!instance.value.linked_data?.locked),
isLinked: computed(() => isModrinthLinkedModpack.value || isImportedModpack.value),
isBusy: computed(
() =>
instance.value.install_stage !== 'installed' ||
@@ -144,7 +216,16 @@ provideInstallationSettings({
reinstalling.value ||
!!offline,
),
skipNonEssentialWarnings,
modpack: computed(() => {
if (isImportedModpack.value && instance.value.link?.type === 'imported_modpack') {
return {
iconUrl: instance.value.icon_path,
title: instance.value.link.name ?? instance.value.name,
versionNumber: instance.value.link.version_number ?? undefined,
filename: instance.value.link.filename ?? undefined,
}
}
if (!modpackInfo.value) return null
return {
iconUrl: modpackInfo.value.project.icon_url,
@@ -156,114 +237,212 @@ provideInstallationSettings({
currentPlatform: computed(() => instance.value.loader),
currentGameVersion: computed(() => instance.value.game_version),
currentLoaderVersion: computed(() => instance.value.loader_version ?? ''),
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
availablePlatforms: computed(() => loadersQuery.data.value?.map((x) => x.name) ?? []),
resolveGameVersions(loader, showSnapshots) {
const versions = all_game_versions?.value ?? []
const versions = gameVersionsQuery.data.value ?? []
const filtered = versions.filter((item) => {
if (loader === 'vanilla') return true
const manifest = getManifest(loader)
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
return !!manifest?.gameVersions?.some((x) => item.version === x.id)
})
return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map(
(x) => ({ value: x.version, label: x.version }),
)
const result = (
showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')
).map((x) => ({ value: x.version, label: x.version }))
debug('resolveGameVersions:', {
loader,
showSnapshots,
totalVersions: versions.length,
filteredVersions: filtered.length,
resultVersions: result.length,
})
return result
},
resolveLoaderVersions(loader, gameVersion) {
if (loader === 'vanilla' || !gameVersion) return []
const manifest = getManifest(loader)
if (!manifest?.value) return []
if (loader === 'fabric' || loader === 'quilt') {
return manifest.value.gameVersions[0]?.loaders ?? []
if (loader === 'vanilla' || !gameVersion) {
debug('resolveLoaderVersions: skipped', { loader, gameVersion })
return []
}
return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
const manifest = getManifest(loader)
if (!manifest) {
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
return []
}
const entry = manifest.gameVersions?.find((item) => item.id === gameVersion)
if (entry?.versionGroup) {
const result =
manifest.versionGroups?.find((group) => group.id === entry.versionGroup)?.loaders ?? []
debug('resolveLoaderVersions: version group result', {
loader,
gameVersion,
versionGroup: entry.versionGroup,
count: result.length,
})
return result
}
const placeholder = manifest.gameVersions?.find((item) => item.id === '${modrinth.gameVersion}')
if (placeholder) {
const result = manifest.gameVersions?.some((item) => item.id === gameVersion)
? placeholder.loaders
: []
debug('resolveLoaderVersions: placeholder result', {
loader,
gameVersion,
count: result.length,
})
return result
}
const result = entry?.loaders ?? []
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
return result
},
resolveHasSnapshots(loader) {
const versions = all_game_versions?.value ?? []
if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release')
const versions = gameVersionsQuery.data.value ?? []
if (loader === 'vanilla') {
const result = versions.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots: vanilla', { loader, result })
return result
}
const manifest = getManifest(loader)
const supported = versions.filter(
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
(item) => !!manifest?.gameVersions?.some((x) => item.version === x.id),
)
return supported.some((x) => x.version_type !== 'release')
const result = supported.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots:', {
loader,
totalVersions: versions.length,
supportedVersions: supported.length,
result,
})
return result
},
async save(platform, gameVersion, loaderVersionId) {
const editProfile: Record<string, string | undefined> = {
debug('save: called', {
instanceId: instance.value.id,
platform,
gameVersion,
loaderVersionId,
})
const editInstancePatch: Record<string, string | undefined> = {
loader: platform,
game_version: gameVersion,
}
if (platform !== 'vanilla' && loaderVersionId) {
editProfile.loader_version = loaderVersionId
editInstancePatch.loader_version = loaderVersionId
}
await edit(instance.value.path, editProfile).catch(handleError)
await edit(instance.value.id, editInstancePatch).catch(handleError)
debug('save: edit complete', { editInstancePatch })
},
afterSave: async () => {
await install(instance.value.path, false).catch(handleError)
debug('afterSave: installing', { instanceId: instance.value.id })
await install_existing_instance(instance.value.id, false).catch(handleError)
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('afterSave: done')
},
async repair() {
debug('repair: called', { instanceId: instance.value.id })
repairing.value = true
await install(instance.value.path, true).catch(handleError)
await install_existing_instance(instance.value.id, true).catch(handleError)
repairing.value = false
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('repair: done')
},
async reinstallModpack() {
debug('reinstallModpack: called', { instanceId: instance.value.id })
reinstalling.value = true
await update_repair_modrinth(instance.value.path).catch(handleError)
reinstalling.value = false
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
let shouldTrack = false
try {
if (isImportedModpack.value) {
shouldTrack = await installLocalModpackFromPicker()
} else {
await update_repair_modrinth(instance.value.id).catch(handleError)
shouldTrack = true
}
} finally {
reinstalling.value = false
}
if (shouldTrack) {
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
}
debug('reinstallModpack: done')
},
async swapModpack() {
debug('swapModpack: called', { instanceId: instance.value.id })
reinstalling.value = true
try {
const installed = await installLocalModpackFromPicker()
if (installed) {
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
}
} finally {
reinstalling.value = false
}
debug('swapModpack: done')
},
async unlinkModpack() {
await edit(instance.value.path, {
linked_data: null as unknown as undefined,
debug('unlinkModpack: called', { instanceId: instance.value.id })
await edit(instance.value.id, {
link: null as unknown as undefined,
})
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', instance.value.path],
queryKey: ['linkedModpackInfo', instance.value.id],
})
onUnlinked()
debug('unlinkModpack: done')
},
getCachedModpackVersions: () => null,
async fetchModpackVersions() {
const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch(
handleError,
)
debug('fetchModpackVersions: called', {
projectId: instance.value.link?.project_id,
})
const versions = await get_project_versions(instance.value.link!.project_id!).catch(handleError)
debug('fetchModpackVersions: done', { count: versions?.length ?? 0 })
return (versions ?? []) as Labrinth.Versions.v2.Version[]
},
async getVersionChangelog(versionId: string) {
debug('getVersionChangelog: called', { versionId })
return (await get_version(versionId, 'must_revalidate').catch(
() => null,
)) as Labrinth.Versions.v2.Version | null
},
async onModpackVersionConfirm(version) {
await update_managed_modrinth_version(instance.value.path, version.id)
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', instance.value.path],
debug('onModpackVersionConfirm: called', {
versionId: version.id,
instanceId: instance.value.id,
})
await update_managed_modrinth_version(instance.value.id, version.id)
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', instance.value.id],
})
debug('onModpackVersionConfirm: done')
},
updaterModalProps: computed(() => ({
isApp: true,
currentVersionId:
modpackInfo.value?.update_version_id ?? instance.value.linked_data?.version_id ?? '',
currentVersionId: modpackInfo.value?.update_version_id ?? instance.value.link?.version_id ?? '',
projectIconUrl: modpackInfo.value?.project?.icon_url,
projectName: modpackInfo.value?.project?.title ?? 'Modpack',
currentGameVersion: instance.value.game_version,
@@ -272,7 +451,10 @@ provideInstallationSettings({
isServer: false,
isApp: true,
showModpackVersionActions: !isMinecraftServer.value,
showModpackVersionActions: computed(
() => isModrinthLinkedModpack.value && !isMinecraftServer.value,
),
isLocalFile: isImportedModpack,
repairing,
reinstalling,
})
@@ -23,7 +23,7 @@ import { computed, readonly, ref, watch } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { edit, get_optimal_jre_key } from '@/helpers/instance'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
@@ -36,7 +36,7 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const optimalJava = readonly(await get_optimal_jre_key(instance.value.id).catch(handleError))
const overrideJavaInstall = ref(!!instance.value.java_path)
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
@@ -104,7 +104,7 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
snapPoints: number[]
}
const editProfileObject = computed(() => {
const editInstanceObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaPath.value
@@ -136,7 +136,7 @@ watch(
memory,
],
async () => {
await edit(instance.value.path, editProfileObject.value)
await edit(instance.value.id, editInstanceObject.value)
},
{ deep: true },
)
@@ -9,7 +9,7 @@ import {
} from '@modrinth/ui'
import { computed, type Ref, ref, watch } from 'vue'
import { edit } from '@/helpers/profile'
import { edit } from '@/helpers/instance'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
@@ -32,7 +32,7 @@ const fullscreenSetting: Ref<boolean> = ref(
instance.value.force_fullscreen ?? globalSettings.force_fullscreen,
)
const editProfileObject = computed(() => {
const editInstanceObject = computed(() => {
if (!overrideWindowSettings.value) {
return {
force_fullscreen: null,
@@ -48,7 +48,7 @@ const editProfileObject = computed(() => {
watch(
[overrideWindowSettings, resolution, fullscreenSetting],
async () => {
await edit(instance.value.path, editProfileObject.value)
await edit(instance.value.id, editInstanceObject.value)
},
{ deep: true },
)
@@ -14,7 +14,7 @@ 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'
import { type MinecraftAuthError, minecraftAuthErrors } from './minecraft-auth-errors'
import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
const modal = ref<InstanceType<typeof NewModal>>()
const rawError = ref<string>('')
@@ -26,7 +26,7 @@ const loadingSignIn = ref(false)
function show(errorVal: { message?: string }) {
rawError.value = errorVal?.message ?? String(errorVal)
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
matchedError.value = findMinecraftAuthError(rawError.value)
debugCollapsed.value = true
hide_ads_window()
@@ -1,10 +1,93 @@
export interface MinecraftAuthError {
errorCode: string
errorCode?: string
errorMatchers?: string[]
matches?: (message: string) => boolean
whatHappened: string
stepsToFix: string[]
}
export const minecraftAuthErrors: MinecraftAuthError[] = [
{
errorMatchers: ['Failed to deserialize response to JSON during step RefreshOAuthToken:'],
whatHappened:
'Your saved Microsoft sign-in token has expired or was revoked, so Modrinth App cannot refresh your Minecraft session.',
stepsToFix: [
'Sign out of the affected Minecraft account in Modrinth App',
'Sign in to the account again',
'Once the new sign-in finishes, try launching Minecraft again',
],
},
{
errorMatchers: ['Failed to deserialize response to JSON during step SisuAuthenticate:'],
whatHappened:
'Xbox services rejected the first sign-in response. This is most often caused by your system clock or time zone being out of sync.',
stepsToFix: [
'Open your system date and time settings',
'Turn on automatic time zone and automatic time, if available',
'Use the sync option in your system settings to synchronize the clock',
'Restart Modrinth App',
'Try signing in again',
],
},
{
matches: (message) =>
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
message.includes('429 Too Many Requests'),
whatHappened:
'Microsoft or Minecraft temporarily blocked the sign-in request because there were too many recent attempts.',
stepsToFix: [
'Wait about an hour before trying again',
'Restart Modrinth App after waiting',
'Try signing in once more',
'If the same message appears, wait longer before retrying so the temporary limit can clear',
],
},
{
matches: (message) =>
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
/Status Code: 5\d\d/.test(message),
whatHappened:
"Minecraft's authentication service is returning a server error, so Modrinth App cannot finish signing you in right now.",
stepsToFix: [
'Wait a few minutes and try signing in again',
'Check <a href="https://support.xbox.com/xbox-live-status">Xbox Status</a> for current service issues',
'Try signing in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a> to confirm whether Minecraft sign-in is also affected there',
'If the service is healthy and this keeps happening, contact support with the debug information below',
],
},
{
errorMatchers: ['Failed to fetch player profile'],
whatHappened:
'Minecraft services could not return a Java Edition profile for this account. This most often happens when the game was purchased recently, the Java profile has not finished being created, or the wrong Microsoft account is being used.',
stepsToFix: [
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
'Launch Minecraft: Java Edition once from the official launcher',
'Wait up to an hour if the purchase or profile setup was recent',
'Make sure you are using the Microsoft account that owns Minecraft. See <a href="https://support.modrinth.com/en/articles/9409136-finding-the-right-xbox-account">Finding the right Xbox account</a> for help',
'Try signing in to Modrinth App again',
],
},
{
matches: (message) =>
message.includes('error sending request for url (') &&
[
'minecraft.net',
'minecraftservices.com',
'mojang.com',
'xbox.com',
'xboxlive.com',
'live.com',
].some((domain) => message.includes(domain)),
whatHappened:
'Modrinth App could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually caused by a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.',
stepsToFix: [
'Restart Modrinth App and try signing in again',
'Check that your internet connection is working',
'Allow Modrinth App through your firewall, antivirus, proxy, VPN, and hosts file rules',
'Try a different network or temporarily disable VPN/proxy software if you use one',
'If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help',
],
},
{
errorCode: '2148916222',
whatHappened:
@@ -87,4 +170,31 @@ export const minecraftAuthErrors: MinecraftAuthError[] = [
'Once finished, try signing in again',
],
},
{
errorMatchers: ['Failed to deserialize response to JSON during step XstsAuthorize:'],
whatHappened:
'Xbox services rejected the request to authorize this account for Minecraft services, but did not return a specific account restriction that Modrinth App recognizes.',
stepsToFix: [
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
'Complete any prompts shown by Microsoft, Xbox, or Minecraft',
'Try signing in to Modrinth App again',
'If the official launcher also fails, follow the error shown there or contact Xbox Support',
],
},
]
export function findMinecraftAuthError(message: string): MinecraftAuthError | null {
return (
minecraftAuthErrors.find((error) => {
if (error.errorCode && message.includes(error.errorCode)) {
return true
}
if (error.errorMatchers?.some((matcher) => message.includes(matcher))) {
return true
}
return error.matches?.(message) ?? false
}) ?? null
)
}
@@ -14,7 +14,7 @@ defineProps<{
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
size="24px"
:tint-by="instance.path"
:tint-by="instance.id"
/>
{{ instance.name }} <ChevronRightIcon />
</span>
@@ -16,7 +16,8 @@ import {
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import type { PlatformTag } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, nextTick, ref, watch } from 'vue'
@@ -26,12 +27,15 @@ import InstallationSettings from '@/components/ui/instance_settings/Installation
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import { get_project_v3 } from '@/helpers/cache'
import { get_linked_modpack_info } from '@/helpers/profile'
import { get_linked_modpack_info } from '@/helpers/instance'
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { provideInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const props = defineProps<{
instance: GameInstance
@@ -45,7 +49,6 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function hide() {
@@ -64,8 +67,8 @@ watch(
() => props.instance,
(instance) => {
isMinecraftServer.value = false
if (instance.linked_data?.project_id) {
get_project_v3(instance.linked_data.project_id, 'must_revalidate')
if (instance.link?.project_id) {
get_project_v3(instance.link.project_id, 'must_revalidate')
.then((project: Labrinth.Projects.v3.Project | undefined) => {
if (project?.minecraft_server != null) {
isMinecraftServer.value = true
@@ -120,11 +123,50 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
// Preload
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo'),
})
useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: get_game_versions,
})
useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
useQuery({
queryKey: computed(() => ['linkedModpackInfo', props.instance.id]),
queryFn: () => get_linked_modpack_info(props.instance.id, 'stale_while_revalidate'),
enabled: computed(() => !!props.instance.link?.project_id && !props.offline),
})
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
if (props.instance.link?.project_id) {
queryClient.prefetchQuery({
queryKey: ['linkedModpackInfo', props.instance.path],
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
queryKey: ['linkedModpackInfo', props.instance.id],
queryFn: () => get_linked_modpack_info(props.instance.id, 'stale_while_revalidate'),
})
}
tabbedModal.value?.show()
@@ -147,7 +189,7 @@ defineExpose({ show, hide })
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
size="24px"
:tint-by="props.instance.path"
:tint-by="props.instance.id"
/>
{{ instance.name }} <ChevronRightIcon />
<span class="font-extrabold text-contrast">{{
@@ -68,17 +68,17 @@ const messages = defineMessages({
})
const emit = defineEmits<{
(e: 'go-to-instance', instancePath: string): void
(e: 'go-to-instance', instanceId: string): void
(e: 'create-anyway'): void
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const instanceName = ref('')
const instancePath = ref('')
const instanceId = ref('')
function show(name: string, path: string) {
function show(name: string, id: string) {
instanceName.value = name
instancePath.value = path
instanceId.value = id
modal.value?.show()
}
@@ -88,7 +88,7 @@ function handleCancel() {
function handleGoToInstance() {
modal.value?.hide()
emit('go-to-instance', instancePath.value)
emit('go-to-instance', instanceId.value)
}
function handleCreateAnyway() {
@@ -31,7 +31,8 @@ import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue'
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
import { update_managed_modrinth_version } from '@/helpers/profile'
import { wait_for_install_job } from '@/helpers/install'
import { update_managed_modrinth_version } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { injectServerInstall } from '@/providers/server-install'
@@ -202,15 +203,15 @@ async function computeDependencyDiffs(
}
async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
if (!inst.linked_data) return null
if (!modpackVersionId.value || !inst.linked_data.version_id) return null
if (!inst.link) return null
if (!modpackVersionId.value || !inst.link.version_id) return null
try {
// For server projects, linked_data.project_id is the server project but
// linked_data.version_id references a content modpack version from a different project.
// Detect this by comparing the version's project_id with linked_data.project_id.
// For server projects, link.project_id is the server project but
// link.version_id references a content modpack version from a different project.
// Detect this by comparing the version's project_id with link.project_id.
modpackVersion.value = await get_version(modpackVersionId.value, 'bypass')
const instanceModpackVersion = await get_version(inst.linked_data.version_id, 'bypass')
const instanceModpackVersion = await get_version(inst.link.version_id, 'bypass')
// Compute dependency diffs between current and latest version
if (instanceModpackVersion && modpackVersion.value) {
@@ -238,11 +239,12 @@ watch(
async function handleUpdate() {
hide()
const serverProjectId = instance.value?.linked_data?.project_id
const serverProjectId = instance.value?.link?.project_id
if (serverProjectId) startInstallingServer(serverProjectId)
try {
if (modpackVersionId.value && instance.value) {
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
await wait_for_install_job(job.job_id)
await onUpdateComplete.value()
}
} catch (error) {
@@ -253,10 +255,8 @@ async function handleUpdate() {
}
function handleReport() {
if (instance.value?.linked_data?.project_id) {
openUrl(
`https://modrinth.com/report?item=project&itemID=${instance.value.linked_data.project_id}`,
)
if (instance.value?.link?.project_id) {
openUrl(`https://modrinth.com/report?item=project&itemID=${instance.value.link.project_id}`)
}
}
@@ -297,11 +297,8 @@ const messages = defineMessages({
})
const hasUpdate = computed(() => {
if (!instance.value?.linked_data) return false
return (
modpackVersionId.value != null &&
modpackVersionId.value !== instance.value.linked_data.version_id
)
if (!instance.value?.link) return false
return modpackVersionId.value != null && modpackVersionId.value !== instance.value.link.version_id
})
defineExpose({ show, hide, hasUpdate })
@@ -10,8 +10,10 @@ import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFeatureFlag = 'worlds_in_home' as FeatureFlag
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
const messages = defineMessages({
colorThemeTitle: {
@@ -100,6 +102,23 @@ const messages = defineMessages({
defaultMessage:
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Displays how much time you've spent playing an instance.`,
},
})
const os = ref(await getOS())
@@ -153,25 +172,6 @@ watch(
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -192,6 +192,44 @@ watch(
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -226,12 +264,12 @@ watch(
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(worldsInHomeFeatureFlag)"
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFeatureFlag)
themeStore.featureFlags[worldsInHomeFeatureFlag] = newValue
settings.feature_flags[worldsInHomeFeatureFlag] = newValue
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
@@ -245,13 +283,32 @@ watch(
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
</div>
<Toggle
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)"
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
@@ -1,5 +1,5 @@
<script setup>
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
@@ -7,9 +7,13 @@ import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
watch(
settings,
@@ -48,6 +52,19 @@ async function purgeCache() {
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
@@ -97,7 +114,7 @@ async function findLauncherDir() {
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<TrashIcon />
Purge cache
</button>
@@ -136,5 +153,16 @@ async function findLauncherDir() {
value if you are frequently getting I/O errors. (app restart required to take effect)
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
<FolderOpenIcon />
Open backups folder
</button>
<p class="m-0 leading-tight text-secondary">
Backups of important app data are stored here in case you need to recover them later.
</p>
</div>
</div>
</template>
@@ -148,7 +148,6 @@ import { arrayBufferToBase64 } from '@modrinth/utils'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import {
add_and_equip_custom_skin,
type Cape,
determineModelType,
equip_skin,
@@ -440,9 +439,22 @@ async function save() {
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
if (mode.value === 'new') {
const addedSkin = await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
const addedSkin = await save_custom_skin(
{
texture_key: '',
variant: variant.value,
cape_id: selectedCape.value?.id,
texture: textureUrl,
source: 'custom',
is_equipped: false,
},
bytes,
variant.value,
selectedCape.value,
true,
)
emit('saved', {
applied: true,
applied: false,
skin: addedSkin,
})
} else {
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
@@ -11,7 +11,9 @@ import {
useVIntl,
} from '@modrinth/ui'
import { useElementSize, useWindowSize } from '@vueuse/core'
import { Tooltip } from 'floating-vue'
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import Draggable from 'vuedraggable'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Skin } from '@/helpers/skins.ts'
@@ -24,6 +26,7 @@ type AddSkinButtonRef = SkinLikeTextButtonExpose | SkinLikeTextButtonExpose[]
interface DefaultSkinSection {
title: string
infoTooltip?: string
skins: Skin[]
}
@@ -31,6 +34,7 @@ interface SkinSection {
key: string
title: string
kind: SkinSectionKind
infoTooltip?: string
skins: Skin[]
}
@@ -79,12 +83,14 @@ const props = defineProps<{
isSkinSelected: (skin: Skin) => boolean
isSkinActive: (skin: Skin) => boolean
isAddSkinButtonDragActive: boolean
readOnly?: boolean
}>()
const emit = defineEmits<{
select: [skin: Skin]
edit: [skin: Skin, event: MouseEvent]
delete: [skin: Skin]
'reorder-saved-skins': [skins: Skin[]]
'add-skin': []
'add-skin-dragenter': [event: DragEvent]
'add-skin-dragover': [event: DragEvent]
@@ -145,10 +151,16 @@ const sections = computed<SkinSection[]>(() => [
key: defaultSkinSectionKey(section.title),
title: section.title,
kind: 'default' as const,
infoTooltip: section.infoTooltip,
skins: section.skins,
})),
])
const draggableSavedSkins = ref<Skin[]>([])
const isDraggingSavedSkin = ref(false)
const canReorderSavedSkins = computed(() => draggableSavedSkins.value.length > 1)
const fixedSavedSkins = computed(() => props.savedSkins.filter((skin) => !canDragSavedSkin(skin)))
const sectionLayouts = computed(() => {
const layouts: Array<{ section: SkinSection; top: number; height: number; index: number }> = []
let top = 0
@@ -205,6 +217,18 @@ watch(
{ immediate: true },
)
watch(
() => props.savedSkins,
(nextSkins) => {
if (isDraggingSavedSkin.value) {
return
}
draggableSavedSkins.value = nextSkins.filter(canDragSavedSkin)
},
{ immediate: true },
)
watch(
listWidth,
(width) => {
@@ -253,6 +277,40 @@ function skinKey(skin: Skin, prefix: string) {
return `${prefix}-${skin.source}-${skin.texture_key}-${skin.variant}-${skin.cape_id ?? 'no-cape'}`
}
function savedSkinKey(skin: Skin) {
return skinKey(skin, 'saved-skin')
}
function canDragSavedSkin(skin: Skin) {
return skin.source === 'custom' || skin.source === 'custom_external'
}
function doSkinOrdersMatch(firstSkins: Skin[], secondSkins: Skin[]) {
const draggableSecondSkins = secondSkins.filter(canDragSavedSkin)
return (
firstSkins.length === draggableSecondSkins.length &&
firstSkins.every(
(skin, index) => savedSkinKey(skin) === savedSkinKey(draggableSecondSkins[index]),
)
)
}
function onSavedSkinDragStart() {
isDraggingSavedSkin.value = true
}
function onSavedSkinDragEnd() {
isDraggingSavedSkin.value = false
if (doSkinOrdersMatch(draggableSavedSkins.value, props.savedSkins)) {
draggableSavedSkins.value = props.savedSkins.filter(canDragSavedSkin)
return
}
emit('reorder-saved-skins', [...draggableSavedSkins.value])
}
function isSectionOpen(key: string) {
return openSectionKeys.value.has(key)
}
@@ -330,38 +388,160 @@ defineExpose({ getAddSkinButtonElement })
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
{{ section.title }}
</span>
<Tooltip
v-if="section.infoTooltip"
theme="dismissable-prompt"
placement="top"
:triggers="['hover', 'focus']"
>
<span
class="inline-flex size-6 shrink-0 items-center justify-center text-secondary transition-colors group-hover:text-primary"
@click.stop
>
<UnknownIcon class="size-5" />
</span>
<template #popper>
<p class="m-0 max-w-96 text-wrap text-sm font-medium leading-tight">
{{ section.infoTooltip }}
</p>
</template>
</Tooltip>
</template>
<div
<Draggable
v-if="section.kind === 'saved'"
:list="draggableSavedSkins"
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
:item-key="savedSkinKey"
:disabled="readOnly || !canReorderSavedSkins"
:animation="250"
:swap-threshold="1"
:invert-swap="false"
:force-fallback="true"
:fallback-on-body="true"
:fallback-tolerance="4"
ghost-class="skin-reorder-ghost"
chosen-class="skin-reorder-chosen"
drag-class="skin-reorder-drag"
fallback-class="skin-reorder-fallback"
@start="onSavedSkinDragStart"
@end="onSavedSkinDragEnd"
>
<template #header>
<SkinLikeTextButton
ref="addSkinButton"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
dropzone
:disabled="readOnly"
:drag-active="!readOnly && isAddSkinButtonDragActive"
@click="emit('add-skin')"
@dragenter="emit('add-skin-dragenter', $event)"
@dragover="emit('add-skin-dragover', $event)"
@dragleave="emit('add-skin-dragleave', $event)"
@drop="emit('add-skin-drop', $event)"
>
<template #icon>
<PlusIcon class="size-8" />
</template>
{{ formatMessage(messages.addSkinButton) }}
<template #subtitle>{{ formatMessage(messages.dragAndDropSubtitle) }}</template>
</SkinLikeTextButton>
</template>
<template #item="{ element: skin }">
<div
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
<SkinButton
class="h-full w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@select="emit('select', skin)"
>
<template v-if="!readOnly" #overlay-buttons>
<ButtonStyled color="brand">
<button
:aria-label="formatMessage(messages.editSkinButton)"
class="pointer-events-auto"
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
>
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
<button
v-tooltip="formatMessage(messages.deleteSkinButton)"
:aria-label="formatMessage(messages.deleteSkinButton)"
class="!rounded-[100%] pointer-events-auto"
@click.stop="emit('delete', skin)"
>
<TrashIcon />
</button>
</ButtonStyled>
</template>
</SkinButton>
</div>
</template>
<template #footer>
<div
v-for="skin in fixedSavedSkins"
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
<SkinButton
class="h-full w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@select="emit('select', skin)"
>
<template v-if="!readOnly" #overlay-buttons>
<ButtonStyled color="brand">
<button
:aria-label="formatMessage(messages.editSkinButton)"
class="pointer-events-auto"
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
>
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
<button
v-tooltip="formatMessage(messages.deleteSkinButton)"
:aria-label="formatMessage(messages.deleteSkinButton)"
class="!rounded-[100%] pointer-events-auto"
@click.stop="emit('delete', skin)"
>
<TrashIcon />
</button>
</ButtonStyled>
</template>
</SkinButton>
</div>
</template>
</Draggable>
<div
v-else
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinLikeTextButton
ref="addSkinButton"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
dropzone
:drag-active="isAddSkinButtonDragActive"
@click="emit('add-skin')"
@dragenter="emit('add-skin-dragenter', $event)"
@dragover="emit('add-skin-dragover', $event)"
@dragleave="emit('add-skin-dragleave', $event)"
@drop="emit('add-skin-drop', $event)"
>
<template #icon>
<PlusIcon class="size-8" />
</template>
{{ formatMessage(messages.addSkinButton) }}
<template #subtitle>{{ formatMessage(messages.dragAndDropSubtitle) }}</template>
</SkinLikeTextButton>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, 'saved-skin')"
:key="skinKey(skin, section.key)"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:tooltip="skin.name"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@select="emit('select', skin)"
>
<template #overlay-buttons>
@@ -374,37 +554,25 @@ defineExpose({ getAddSkinButtonElement })
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
<button
v-tooltip="formatMessage(messages.deleteSkinButton)"
:aria-label="formatMessage(messages.deleteSkinButton)"
class="!rounded-[100%] pointer-events-auto"
@click.stop="emit('delete', skin)"
>
<TrashIcon />
</button>
</ButtonStyled>
</template>
</SkinButton>
</div>
<div
v-else
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, section.key)"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:tooltip="skin.name"
@select="emit('select', skin)"
/>
</div>
</Accordion>
</div>
</div>
</template>
<style scoped>
:global(.skin-reorder-ghost) {
opacity: 0.35;
}
:global(.skin-reorder-drag) {
cursor: grabbing;
}
:global(.skin-reorder-fallback) {
opacity: 0.9;
pointer-events: none;
}
</style>
@@ -27,10 +27,10 @@ import { useRouter } from 'vue-router'
import { trackEvent } from '@/helpers/analytics'
import { get_project } from '@/helpers/cache'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, run } from '@/helpers/profile'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils'
import { showInstanceInFolder } from '@/helpers/utils'
import { handleSevereError } from '@/store/error'
const { handleError } = injectNotificationManager()
@@ -52,13 +52,13 @@ const props = defineProps<{
last_played: Dayjs
}>()
const loadingModpack = ref(!!props.instance.linked_data)
const loadingModpack = ref(!!props.instance.link)
const modpack = ref()
if (props.instance.linked_data) {
if (props.instance.link) {
nextTick().then(async () => {
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
modpack.value = await get_project(props.instance.link?.project_id, 'must_revalidate')
loadingModpack.value = false
})
}
@@ -81,8 +81,8 @@ const playing = ref(false)
const play = async (event: MouseEvent) => {
event?.stopPropagation()
loading.value = true
await run(props.instance.path)
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
.finally(() => {
trackEvent('InstanceStart', {
loader: props.instance.loader,
@@ -97,7 +97,7 @@ const play = async (event: MouseEvent) => {
const stop = async (event: MouseEvent) => {
event?.stopPropagation()
loading.value = true
await kill(props.instance.path).catch(handleError)
await kill(props.instance.id).catch(handleError)
trackEvent('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
@@ -112,7 +112,7 @@ const unlistenProcesses = await process_listener(async () => {
})
const checkProcess = async () => {
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
const runningProcesses = await get_by_instance_id(props.instance.id).catch(handleError)
playing.value = runningProcesses.length > 0
}
@@ -130,7 +130,7 @@ onUnmounted(() => {
<template #clickable>
<router-link
class="no-click-animation"
:to="`/instance/${encodeURIComponent(instance.path)}`"
:to="`/instance/${encodeURIComponent(instance.id)}`"
/>
</template>
<div
@@ -138,7 +138,7 @@ onUnmounted(() => {
>
<Avatar
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
:tint-by="instance.path"
:tint-by="instance.id"
size="48px"
/>
<div class="flex flex-col col-span-2 justify-between h-full">
@@ -206,12 +206,12 @@ onUnmounted(() => {
:options="[
{
id: 'open-instance',
shown: !!instance.path,
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
shown: !!instance.id,
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
},
{
id: 'open-folder',
action: () => showProfileInFolder(instance.path),
action: () => showInstanceInFolder(instance.id),
},
]"
>
@@ -10,13 +10,13 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { trackEvent } from '@/helpers/analytics'
import { process_listener, profile_listener } from '@/helpers/events'
import { instance_listener, process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_all } from '@/helpers/process'
import { kill, run } from '@/helpers/profile'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import {
get_profile_protocol_version,
get_instance_protocol_version,
get_recent_worlds,
getWorldIdentifier,
hasServerQuickPlaySupport,
@@ -27,7 +27,7 @@ import {
type ServerWorld,
start_join_server,
start_join_singleplayer_world,
type WorldWithProfile,
type WorldWithInstance,
} from '@/helpers/worlds.ts'
import { handleSevereError } from '@/store/error'
import { useTheming } from '@/store/theme.ts'
@@ -66,7 +66,7 @@ type InstanceJumpBackInItem = BaseJumpBackInItem & {
type WorldJumpBackInItem = BaseJumpBackInItem & {
type: 'world'
world: WorldWithProfile
world: WorldWithInstance
}
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
@@ -100,7 +100,7 @@ async function populateJumpBackIn() {
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
worlds.forEach((world) => {
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
const instance = props.recentInstances.find((instance) => instance.id === world.instance_id)
if (!instance || !world.last_played) {
return
@@ -115,23 +115,23 @@ async function populateJumpBackIn() {
})
const servers: {
instancePath: string
instanceId: string
address: string
}[] = worldItems
.filter((item) => item.world.type === 'server' && item.instance)
.map((item) => ({
instancePath: item.instance.path,
instanceId: item.instance.id,
address: (item.world as ServerWorld).address,
}))
// fetch protocol versions for all unique MC versions with server worlds
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instanceId))
await Promise.all(
[...uniqueServerInstances].map((path) =>
get_profile_protocol_version(path)
.then((protoVer) => (protocolVersions.value[path] = protoVer))
[...uniqueServerInstances].map((instanceId) =>
get_instance_protocol_version(instanceId)
.then((protoVer) => (protocolVersions.value[instanceId] = protoVer))
.catch(() => {
console.error(`Failed to get profile protocol for: ${path} `)
console.error(`Failed to get instance protocol for: ${instanceId} `)
}),
),
)
@@ -145,14 +145,14 @@ async function populateJumpBackIn() {
}
})
servers.forEach(({ instancePath, address }) =>
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
servers.forEach(({ instanceId, address }) =>
refreshServerData(serverData.value[address], protocolVersions.value[instanceId], address),
)
}
const instanceItems: InstanceJumpBackInItem[] = []
for (const instance of props.recentInstances) {
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
const worldItem = worldItems.find((item) => item.instance.id === instance.id)
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
continue
}
@@ -171,14 +171,14 @@ async function populateJumpBackIn() {
.slice(0, MAX_JUMP_BACK_IN)
}
function refreshServer(address: string, instancePath: string) {
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
function refreshServer(address: string, instanceId: string) {
refreshServerData(serverData.value[address], protocolVersions.value[instanceId], address)
}
async function joinWorld(world: WorldWithProfile, instance?: GameInstance) {
async function joinWorld(world: WorldWithInstance, instance?: GameInstance) {
console.log(`Joining world ${getWorldIdentifier(world)}`)
if (world.type === 'server') {
await start_join_server(world.profile, world.address).catch(handleError)
await start_join_server(world.instance_id, world.address).catch(handleError)
if (instance) {
trackEvent('InstanceStart', {
loader: instance.loader,
@@ -187,13 +187,13 @@ async function joinWorld(world: WorldWithProfile, instance?: GameInstance) {
})
}
} else if (world.type === 'singleplayer') {
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
await start_join_singleplayer_world(world.instance_id, world.path).catch(handleError)
}
}
async function playInstance(instance: GameInstance) {
await run(instance.path)
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
await run(instance.id)
.catch((err) => handleSevereError(err, { instanceId: instance.id }))
.finally(() => {
trackEvent('InstanceStart', {
loader: instance.loader,
@@ -210,14 +210,14 @@ async function stopInstance(path: string) {
})
}
const currentProfile = ref<string>()
const currentInstance = ref<string>()
const currentWorld = ref<string>()
const unlistenProcesses = await process_listener(async () => {
await checkProcesses()
})
const unlistenProfiles = await profile_listener(async () => {
const unlistenInstances = await instance_listener(async () => {
await populateJumpBackIn().catch(() => {
console.error('Failed to populate jump back in')
})
@@ -227,18 +227,18 @@ const runningInstances = ref<string[]>([])
type ProcessMetadata = {
uuid: string
profile_path: string
instance_id: string
start_time: string
}
const checkProcesses = async () => {
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
const runningPaths = runningProcesses.map((x) => x.profile_path)
const runningPaths = runningProcesses.map((x) => x.instance_id)
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
currentProfile.value = undefined
if (currentInstance.value && stoppedInstances.includes(currentInstance.value)) {
currentInstance.value = undefined
currentWorld.value = undefined
}
@@ -252,7 +252,7 @@ onMounted(() => {
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
unlistenInstances()
})
</script>
@@ -278,14 +278,14 @@ onUnmounted(() => {
<div class="grid-when-huge flex flex-col w-full gap-2">
<template
v-for="item in jumpBackInItems"
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
:key="`${item.instance.id}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
>
<WorldItem
v-if="item.type === 'world'"
:world="item.world"
:playing-instance="runningInstances.includes(item.instance.path)"
:playing-instance="runningInstances.includes(item.instance.id)"
:playing-world="
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
currentInstance === item.instance.id && currentWorld === getWorldIdentifier(item.world)
"
:refreshing="
item.world.type === 'server'
@@ -306,34 +306,34 @@ onUnmounted(() => {
:rendered-motd="
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
"
:current-protocol="protocolVersions[item.instance.path]"
:current-protocol="protocolVersions[item.instance.id]"
:game-mode="
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
"
:instance-path="item.instance.path"
:instance-id="item.instance.id"
:instance-name="item.instance.name"
:instance-icon="item.instance.icon_path"
@refresh="
() =>
item.world.type === 'server'
? refreshServer(item.world.address, item.instance.path)
? refreshServer(item.world.address, item.instance.id)
: {}
"
@update="() => populateJumpBackIn()"
@play="
() => {
currentProfile = item.instance.path
currentInstance = item.instance.id
currentWorld = getWorldIdentifier(item.world)
joinWorld(item.world, item.instance)
}
"
@play-instance="
() => {
currentProfile = item.instance.path
currentInstance = item.instance.id
playInstance(item.instance)
}
"
@stop="() => stopInstance(item.instance.path)"
@stop="() => stopInstance(item.instance.id)"
/>
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
</template>
@@ -2,6 +2,7 @@
import {
ClipboardCopyIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
IssuesIcon,
@@ -23,6 +24,7 @@ import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
OverflowMenu,
SmartClickable,
TagItem,
@@ -39,7 +41,7 @@ import type { Component } from 'vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { copyToClipboard } from '@/helpers/utils'
import { copyToClipboard, createInstanceShortcut } from '@/helpers/utils'
import type {
ProtocolVersion,
ServerStatus,
@@ -60,6 +62,7 @@ const formatDateTime = useFormatDateTime({
})
const router = useRouter()
const { addNotification } = injectNotificationManager()
const emit = defineEmits<{
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
@@ -91,9 +94,10 @@ const props = withDefaults(
managed?: boolean
// Instance
instancePath?: string
instanceId?: string
instanceName?: string
instanceIcon?: string
shortcutInstanceId?: string
}>(),
{
playingInstance: false,
@@ -110,9 +114,10 @@ const props = withDefaults(
gameMode: undefined,
managed: false,
instancePath: undefined,
instanceId: undefined,
instanceName: undefined,
instanceIcon: undefined,
shortcutInstanceId: undefined,
},
)
@@ -131,6 +136,33 @@ const serverIncompatible = computed(
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
const managed = computed(() => props.managed)
const shortcutInstanceId = computed(() => props.shortcutInstanceId ?? props.instanceId)
async function createShortcut() {
if (!shortcutInstanceId.value) return
try {
const shortcutPath = await createInstanceShortcut(
props.world.name,
shortcutInstanceId.value,
props.world.type === 'server'
? { server: (props.world as ServerWorld).address }
: { singleplayerWorld: (props.world as SingleplayerWorld).path },
)
if (!shortcutPath) return
addNotification({
type: 'success',
title: 'Shortcut created',
})
} catch (error) {
addNotification({
type: 'error',
title: 'Failed to create shortcut',
text: error instanceof Error ? error.message : '',
})
}
}
const messages = defineMessages({
hardcore: {
@@ -185,6 +217,10 @@ const messages = defineMessages({
id: 'instance.worlds.dont_show_on_home',
defaultMessage: `Don't show on Home`,
},
createShortcut: {
id: 'instance.worlds.create_shortcut',
defaultMessage: 'Create shortcut',
},
linkedServer: {
id: 'instance.worlds.linked_server',
defaultMessage: 'Managed by server project',
@@ -209,10 +245,10 @@ const messages = defineMessages({
</script>
<template>
<SmartClickable>
<template v-if="instancePath" #clickable>
<template v-if="instanceId" #clickable>
<router-link
class="no-click-animation"
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
:to="`/instance/${encodeURIComponent(instanceId)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
/>
</template>
<div
@@ -324,16 +360,16 @@ const messages = defineMessages({
</template>
<template v-else> {{ formatMessage(messages.notPlayedYet) }} </template>
</div>
<template v-if="instancePath">
<template v-if="instanceId">
<router-link
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
:to="`/instance/${instancePath}`"
:to="`/instance/${instanceId}`"
>
<Avatar
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
size="16px"
:tint-by="instancePath"
:tint-by="instanceId"
class="shrink-0"
/>
<span class="truncate">{{ instanceName }}</span>
@@ -420,14 +456,14 @@ const messages = defineMessages({
:options="[
{
id: 'play-instance',
shown: !!instancePath,
shown: !!instanceId,
disabled: playingInstance,
action: () => emit('play-instance'),
},
{
id: 'open-instance',
shown: !!instancePath,
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
shown: !!instanceId,
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
},
{
id: 'refresh',
@@ -442,7 +478,7 @@ const messages = defineMessages({
{
id: 'edit',
action: () => emit('edit'),
shown: !instancePath,
shown: !instanceId,
disabled: locked || managed,
tooltip: locked
? formatMessage(messages.worldInUse)
@@ -457,14 +493,14 @@ const messages = defineMessages({
},
{
divider: true,
shown: !!instancePath,
shown: !!instanceId,
},
{
id: 'dont-show-on-home',
shown: !!instancePath,
shown: !!instanceId,
action: () => {
set_world_display_status(
instancePath,
instanceId,
world.type,
getWorldIdentifier(world),
'hidden',
@@ -473,16 +509,21 @@ const messages = defineMessages({
})
},
},
{
id: 'create-shortcut',
shown: !!shortcutInstanceId,
action: () => createShortcut(),
},
{
divider: true,
shown: !instancePath,
shown: !instanceId,
},
{
id: 'delete',
color: 'red',
hoverFilled: true,
action: () => emit('delete'),
shown: !instancePath,
shown: !instanceId,
disabled: locked || managed,
tooltip: locked
? formatMessage(messages.worldInUse)
@@ -517,6 +558,10 @@ const messages = defineMessages({
<UpdatedIcon aria-hidden="true" />
{{ formatMessage(commonMessages.refreshButton) }}
</template>
<template #create-shortcut>
<ExternalIcon aria-hidden="true" />
{{ formatMessage(messages.createShortcut) }}
</template>
<template #dont-show-on-home>
<XIcon aria-hidden="true" />
{{ formatMessage(messages.dontShowOnHome) }}
@@ -12,7 +12,11 @@ import { ref } from 'vue'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
import {
add_server_to_instance,
type ServerPackStatus,
type ServerWorld,
} from '@/helpers/worlds.ts'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -35,8 +39,8 @@ async function addServer(play: boolean) {
const serverName = name.value ? name.value : address.value
const resourcePackStatus = resourcePack.value
const index =
(await add_server_to_profile(
props.instance.path,
(await add_server_to_instance(
props.instance.id,
serverName,
address.value,
resourcePackStatus,
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { TrashIcon, XIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
defineMessages,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { World } from '@/helpers/worlds.ts'
const { formatMessage } = useVIntl()
const props = defineProps<{
world: World | null
}>()
const emit = defineEmits<{
confirm: [world: World]
}>()
const messages = defineMessages({
removeServerTitle: {
id: 'app.instance.worlds.remove-server-modal.title',
defaultMessage: 'Remove server',
},
deleteWorldTitle: {
id: 'app.instance.worlds.delete-world-modal.title',
defaultMessage: 'Delete world',
},
removeServerWarningHeader: {
id: 'app.instance.worlds.remove-server-modal.warning-header',
defaultMessage: 'Removing {name}',
},
deleteWorldWarningHeader: {
id: 'app.instance.worlds.delete-world-modal.warning-header',
defaultMessage: 'Deleting {name}',
},
removeServerWarningBody: {
id: 'app.instance.worlds.remove-server-modal.warning-body',
defaultMessage:
'This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address.',
},
deleteWorldWarningBody: {
id: 'app.instance.worlds.delete-world-modal.warning-body',
defaultMessage:
'This world will be permanently deleted from this instance. This action cannot be undone.',
},
removeServerButton: {
id: 'app.instance.worlds.remove-server-modal.remove-button',
defaultMessage: 'Remove server',
},
deleteWorldButton: {
id: 'app.instance.worlds.delete-world-modal.delete-button',
defaultMessage: 'Delete world',
},
})
const modal = ref<InstanceType<typeof NewModal>>()
const isServer = computed(() => props.world?.type === 'server')
const isSingleplayer = computed(() => props.world?.type === 'singleplayer')
const titleMessage = computed(() =>
isServer.value ? messages.removeServerTitle : messages.deleteWorldTitle,
)
const actionMessage = computed(() =>
isServer.value ? messages.removeServerButton : messages.deleteWorldButton,
)
const warningHeaderMessage = computed(() =>
isServer.value ? messages.removeServerWarningHeader : messages.deleteWorldWarningHeader,
)
const warningBodyMessage = computed(() =>
isServer.value ? messages.removeServerWarningBody : messages.deleteWorldWarningBody,
)
function show() {
modal.value?.show()
}
function hide() {
modal.value?.hide()
}
function confirm() {
if (!props.world) return
emit('confirm', props.world)
hide()
}
defineExpose({ show, hide })
</script>
<template>
<NewModal ref="modal" :header="formatMessage(titleMessage)" fade="danger" max-width="500px">
<div class="flex flex-col gap-4">
<Admonition
type="critical"
:header="formatMessage(warningHeaderMessage, { name: world?.name })"
>
{{ formatMessage(warningBodyMessage) }}
</Admonition>
</div>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button @click="hide">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button :disabled="!isServer && !isSingleplayer" @click="confirm">
<TrashIcon />
{{ formatMessage(actionMessage) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
@@ -15,7 +15,7 @@ import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import type { GameInstance } from '@/helpers/types'
import {
type DisplayStatus,
edit_server_in_profile,
edit_server_in_instance,
type ServerPackStatus,
type ServerWorld,
set_world_display_status,
@@ -46,8 +46,8 @@ const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal
async function saveServer() {
const serverName = name.value ? name.value : address.value
const resourcePackStatus = resourcePack.value
await edit_server_in_profile(
props.instance.path,
await edit_server_in_instance(
props.instance.id,
index.value,
serverName,
address.value,
@@ -56,7 +56,7 @@ async function saveServer() {
if (newDisplayStatus.value !== displayStatus.value) {
await set_world_display_status(
props.instance.path,
props.instance.id,
'server',
address.value,
newDisplayStatus.value,
@@ -40,14 +40,14 @@ const hideFromHome = ref(false)
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
async function saveWorld() {
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
await rename_world(props.instance.id, path.value, name.value).catch(handleError)
if (removeIcon.value) {
await reset_world_icon(props.instance.path, path.value).catch(handleError)
await reset_world_icon(props.instance.id, path.value).catch(handleError)
}
if (newDisplayStatus.value !== displayStatus.value) {
await set_world_display_status(
props.instance.path,
props.instance.id,
'singleplayer',
path.value,
newDisplayStatus.value,
@@ -0,0 +1,745 @@
import { CheckIcon, CopyIcon, UpdatedIcon } from '@modrinth/assets'
import {
defineMessages,
type PopupNotificationButton,
type PopupNotificationProgressItem,
type PopupNotificationProgressType,
useVIntl,
} from '@modrinth/ui'
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,
install_job_retry,
install_job_support_details,
installJobInstanceId,
type InstallJobSnapshot,
type InstallJobStatus,
type InstallPhaseId,
type InstallProgress,
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
const messages = defineMessages({
installs: {
id: 'app.action-bar.installs',
defaultMessage: 'Installs',
},
retry: {
id: 'app.action-bar.install.retry',
defaultMessage: 'Retry',
},
copyDetails: {
id: 'app.action-bar.install.copy-details',
defaultMessage: 'Copy details',
},
copied: {
id: 'app.action-bar.install.copied-details',
defaultMessage: 'Copied',
},
dismiss: {
id: 'app.action-bar.install.dismiss',
defaultMessage: 'Dismiss',
},
openInstance: {
id: 'app.action-bar.install.open-instance',
defaultMessage: 'Open instance',
},
unknownInstance: {
id: 'app.action-bar.install.unknown-instance',
defaultMessage: 'Unknown instance',
},
})
const phaseMessages = defineMessages({
preparing_instance: {
id: 'app.install.phase.preparing_instance',
defaultMessage: 'Queued to install',
},
resolving_pack: {
id: 'app.install.phase.resolving_pack',
defaultMessage: 'Resolving content',
},
downloading_pack_file: {
id: 'app.install.phase.downloading_pack_file',
defaultMessage: 'Downloading pack file',
},
reading_pack_manifest: {
id: 'app.install.phase.reading_pack_manifest',
defaultMessage: 'Reading pack manifest',
},
downloading_content: {
id: 'app.install.phase.downloading_content',
defaultMessage: 'Downloading content',
},
extracting_overrides: {
id: 'app.install.phase.extracting_overrides',
defaultMessage: 'Extracting overrides',
},
resolving_minecraft: {
id: 'app.install.phase.resolving_minecraft',
defaultMessage: 'Resolving Minecraft',
},
resolving_loader: {
id: 'app.install.phase.resolving_loader',
defaultMessage: 'Resolving loader',
},
preparing_java: {
id: 'app.install.phase.preparing_java',
defaultMessage: 'Preparing Java',
},
downloading_minecraft: {
id: 'app.install.phase.downloading_minecraft',
defaultMessage: 'Downloading Minecraft',
},
running_loader_processors: {
id: 'app.install.phase.running_loader_processors',
defaultMessage: 'Running loader processors',
},
finalizing: {
id: 'app.install.phase.finalizing',
defaultMessage: 'Finalizing',
},
rolling_back: {
id: 'app.install.phase.rolling_back',
defaultMessage: 'Rolling back',
},
})
const javaStepMessages = defineMessages({
resolving: {
id: 'app.install.phase.preparing_java.resolving',
defaultMessage: 'Preparing Java {version}',
},
fetching_metadata: {
id: 'app.install.phase.preparing_java.fetching-metadata',
defaultMessage: 'Fetching Java {version}',
},
downloading: {
id: 'app.install.phase.preparing_java.downloading',
defaultMessage: 'Downloading Java {version}',
},
extracting: {
id: 'app.install.phase.preparing_java.extracting',
defaultMessage: 'Extracting Java {version}',
},
validating: {
id: 'app.install.phase.preparing_java.validating',
defaultMessage: 'Validating Java {version}',
},
})
const failureSummaryMessages = defineMessages({
canceled: {
id: 'app.action-bar.install.summary.canceled',
defaultMessage: 'Canceled',
},
appClosed: {
id: 'app.action-bar.install.summary.app-closing',
defaultMessage: 'Canceled due to app closing',
},
downloadFailed: {
id: 'app.action-bar.install.summary.download-failed',
defaultMessage: "Download couldn't finish",
},
modrinthUnreachable: {
id: 'app.action-bar.install.summary.modrinth-unreachable',
defaultMessage: "Couldn't reach Modrinth",
},
packDownloadFailed: {
id: 'app.action-bar.install.summary.pack-download-failed',
defaultMessage: "Couldn't download pack",
},
badModpackFile: {
id: 'app.action-bar.install.summary.bad-modpack-file',
defaultMessage: "Couldn't read modpack",
},
invalidModpack: {
id: 'app.action-bar.install.summary.invalid-modpack',
defaultMessage: 'Modpack data invalid',
},
contentDownloadFailed: {
id: 'app.action-bar.install.summary.content-download-failed',
defaultMessage: "Couldn't download files",
},
corruptDownload: {
id: 'app.action-bar.install.summary.corrupt-download',
defaultMessage: 'Downloaded file is corrupt',
},
invalidModpackFiles: {
id: 'app.action-bar.install.summary.invalid-modpack-files',
defaultMessage: 'Modpack files have invalid metadata',
},
noWritePermission: {
id: 'app.action-bar.install.summary.no-write-permission',
defaultMessage: 'No permission to write',
},
couldNotSaveFiles: {
id: 'app.action-bar.install.summary.could-not-save-files',
defaultMessage: "Couldn't save files",
},
invalidFilePath: {
id: 'app.action-bar.install.summary.invalid-file-path',
defaultMessage: 'File path is invalid',
},
instanceNotFound: {
id: 'app.action-bar.install.summary.instance-not-found',
defaultMessage: "Instance couldn't be found",
},
cleanupIncomplete: {
id: 'app.action-bar.install.summary.cleanup-incomplete',
defaultMessage: "Cleanup didn't finish",
},
javaSetupFailed: {
id: 'app.action-bar.install.summary.java-setup-failed',
defaultMessage: "Java setup couldn't finish",
},
minecraftSetupFailed: {
id: 'app.action-bar.install.summary.minecraft-setup-failed',
defaultMessage: 'Minecraft setup failed',
},
loaderSetupFailed: {
id: 'app.action-bar.install.summary.loader-setup-failed',
defaultMessage: 'Loader setup failed',
},
localDataError: {
id: 'app.action-bar.install.summary.local-data-error',
defaultMessage: "Couldn't update local data",
},
unexpectedError: {
id: 'app.action-bar.install.summary.unexpected-error',
defaultMessage: 'Something went wrong',
},
})
const visibleJobStatuses = new Set<InstallJobStatus>(['queued', 'running', 'failed', 'interrupted'])
const copyDetailsStallMs = 30_000
interface ProgressSnapshot {
signature: string
changedAt: number
timeout: number | null
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) return null
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) return icon
return convertFileSrc(icon)
}
export async function useInstallJobNotifications(opts: {
router: Router
handleError: (err: unknown) => void
onChange: () => void
}) {
const { formatMessage } = useVIntl()
const jobs = ref<InstallJobSnapshot[]>([])
const iconUrls = ref<Record<string, string | null>>({})
const instanceNames = ref<Record<string, string>>({})
const copiedJobIds = ref<Set<string>>(new Set())
const jobOrder = new Map<string, number>()
let refreshRequest = 0
let metadataRequest = 0
let nextJobOrder = 0
const copiedResetTimeouts = new Map<string, number>()
const progressSnapshots = new Map<string, ProgressSnapshot>()
function getTitle(job: InstallJobSnapshot): string {
if (job.display?.title) return job.display.title
if (job.details.type === 'instance') return job.details.name
if (job.details.type === 'modpack' && job.details.title) return job.details.title
const instanceId = installJobInstanceId(job)
return (
(instanceId ? instanceNames.value[instanceId] : null) ??
formatMessage(messages.unknownInstance)
)
}
function getText(job: InstallJobSnapshot): string {
if (job.status === 'failed' || job.status === 'interrupted') {
return getFailureSummary(job)
}
if (job.phase === 'preparing_java' && job.details.type === 'java') {
return formatMessage(javaStepMessages[job.details.step], {
version: job.details.major_version,
})
}
return formatMessage(phaseMessages[job.phase])
}
function getFailureSummary(job: InstallJobSnapshot): string {
const code = job.error?.code
const phase = job.error?.phase ?? job.phase
if (code === 'app_closed' || (job.status === 'interrupted' && code === 'interrupted')) {
return formatMessage(failureSummaryMessages.appClosed)
}
if (code === 'canceled') {
return formatMessage(failureSummaryMessages.canceled)
}
if (job.rollback_error || code === 'rollback_error') {
return formatMessage(failureSummaryMessages.cleanupIncomplete)
}
if (hasPermissionError(job)) {
return formatMessage(failureSummaryMessages.noWritePermission)
}
switch (code) {
case 'network_error':
return formatMessage(
phase === 'downloading_pack_file'
? failureSummaryMessages.packDownloadFailed
: failureSummaryMessages.downloadFailed,
)
case 'api_error':
return formatMessage(failureSummaryMessages.modrinthUnreachable)
case 'pack_error':
return formatMessage(
phase === 'downloading_pack_file'
? failureSummaryMessages.packDownloadFailed
: failureSummaryMessages.invalidModpack,
)
case 'archive_error':
return formatMessage(failureSummaryMessages.badModpackFile)
case 'parse_error':
return formatMessage(failureSummaryMessages.invalidModpack)
case 'content_error':
return formatMessage(failureSummaryMessages.invalidModpackFiles)
case 'hash_error':
return formatMessage(failureSummaryMessages.corruptDownload)
case 'filesystem_error':
return formatMessage(failureSummaryMessages.couldNotSaveFiles)
case 'path_error':
return formatMessage(failureSummaryMessages.invalidFilePath)
case 'instance_error':
return formatMessage(failureSummaryMessages.instanceNotFound)
case 'java_error':
return formatMessage(failureSummaryMessages.javaSetupFailed)
case 'loader_error':
case 'processor_error':
return formatMessage(failureSummaryMessages.loaderSetupFailed)
case 'database_error':
return formatMessage(failureSummaryMessages.localDataError)
case 'launcher_error':
case 'metadata_error':
return getFailureSummaryForPhase(phase)
default:
return getFailureSummaryForPhase(phase)
}
}
function getFailureSummaryForPhase(phase: InstallPhaseId): string {
switch (phase) {
case 'downloading_pack_file':
return formatMessage(failureSummaryMessages.packDownloadFailed)
case 'resolving_pack':
case 'reading_pack_manifest':
return formatMessage(failureSummaryMessages.invalidModpack)
case 'downloading_content':
return formatMessage(failureSummaryMessages.contentDownloadFailed)
case 'extracting_overrides':
return formatMessage(failureSummaryMessages.couldNotSaveFiles)
case 'resolving_minecraft':
case 'downloading_minecraft':
return formatMessage(failureSummaryMessages.minecraftSetupFailed)
case 'resolving_loader':
case 'running_loader_processors':
return formatMessage(failureSummaryMessages.loaderSetupFailed)
case 'preparing_java':
return formatMessage(failureSummaryMessages.javaSetupFailed)
case 'preparing_instance':
return formatMessage(failureSummaryMessages.instanceNotFound)
case 'rolling_back':
return formatMessage(failureSummaryMessages.cleanupIncomplete)
default:
return formatMessage(failureSummaryMessages.unexpectedError)
}
}
function hasPermissionError(job: InstallJobSnapshot): boolean {
const message = job.error?.message.toLowerCase() ?? ''
return (
message.includes('permission denied') ||
message.includes('access is denied') ||
message.includes('operation not permitted')
)
}
function getProgressType(job: InstallJobSnapshot): PopupNotificationProgressType | undefined {
if (!getEffectiveProgress(job)) return undefined
if (
job.phase === 'preparing_java' &&
job.details.type === 'java' &&
job.details.step === 'downloading'
) {
return 'bytes'
}
if (job.phase === 'downloading_content') {
return job.progress?.secondary ? 'bytes' : 'count'
}
if (
job.phase === 'downloading_pack_file' ||
job.phase === 'extracting_overrides' ||
job.phase === 'downloading_minecraft'
) {
return 'bytes'
}
if (job.phase === 'running_loader_processors') {
return 'count'
}
return 'percentage'
}
function getEffectiveProgress(job: InstallJobSnapshot): InstallProgress | null | undefined {
if (job.phase === 'downloading_content' && job.progress?.secondary) {
return job.progress.secondary
}
return job.progress
}
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))
}
function isTerminalJob(job: InstallJobSnapshot): boolean {
return job.status === 'failed' || job.status === 'interrupted'
}
function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean {
return (
job.status === 'running' &&
job.phase !== 'preparing_instance' &&
job.phase !== 'finalizing' &&
job.phase !== 'rolling_back'
)
}
function getJobSortRank(job: InstallJobSnapshot): number {
if (isTerminalJob(job)) return 0
if (job.status === 'queued' || job.phase === 'preparing_instance') return 2
return 1
}
function progressSignature(job: InstallJobSnapshot): string {
const progress = job.progress
const secondary = progress?.secondary
return [
job.status,
job.phase,
JSON.stringify(job.details),
progress?.current ?? '',
progress?.total ?? '',
secondary?.current ?? '',
secondary?.total ?? '',
].join(':')
}
function clearCopied(jobId: string) {
if (!copiedJobIds.value.has(jobId)) {
return
}
const timeout = copiedResetTimeouts.get(jobId)
if (timeout != null) {
window.clearTimeout(timeout)
copiedResetTimeouts.delete(jobId)
}
const nextCopiedJobIds = new Set(copiedJobIds.value)
nextCopiedJobIds.delete(jobId)
copiedJobIds.value = nextCopiedJobIds
}
function clearProgressSnapshot(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (snapshot?.timeout != null) {
window.clearTimeout(snapshot.timeout)
}
progressSnapshots.delete(jobId)
}
function scheduleStaleProgressRefresh(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = window.setTimeout(() => {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = null
opts.onChange()
}, copyDetailsStallMs)
}
function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) {
const trackedJobIds = new Set<string>()
const now = Date.now()
for (const job of nextJobs) {
if (!canShowStalledProgressDetails(job)) {
continue
}
trackedJobIds.add(job.job_id)
const signature = progressSignature(job)
const snapshot = progressSnapshots.get(job.job_id)
if (snapshot?.signature === signature) {
continue
}
clearProgressSnapshot(job.job_id)
clearCopied(job.job_id)
progressSnapshots.set(job.job_id, {
signature,
changedAt: now,
timeout: null,
})
scheduleStaleProgressRefresh(job.job_id)
}
for (const jobId of progressSnapshots.keys()) {
if (!trackedJobIds.has(jobId)) {
clearProgressSnapshot(jobId)
}
}
}
function hasStalledProgress(job: InstallJobSnapshot): boolean {
const snapshot = progressSnapshots.get(job.job_id)
return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs
}
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job))
}
function isCopied(job: InstallJobSnapshot): boolean {
return copiedJobIds.value.has(job.job_id)
}
function setCopied(job: InstallJobSnapshot) {
copiedJobIds.value = new Set([...copiedJobIds.value, job.job_id])
const existingTimeout = copiedResetTimeouts.get(job.job_id)
if (existingTimeout != null) {
window.clearTimeout(existingTimeout)
}
copiedResetTimeouts.set(
job.job_id,
window.setTimeout(() => {
copiedResetTimeouts.delete(job.job_id)
if (!copiedJobIds.value.has(job.job_id)) {
return
}
const nextCopiedJobIds = new Set(copiedJobIds.value)
nextCopiedJobIds.delete(job.job_id)
copiedJobIds.value = nextCopiedJobIds
opts.onChange()
}, 1_000),
)
opts.onChange()
}
async function copyJobDetails(job: InstallJobSnapshot) {
const details = await install_job_support_details(job.job_id).catch((error) => {
opts.handleError(error)
return null
})
if (!details) {
return
}
try {
await navigator.clipboard.writeText(details)
setCopied(job)
} catch (error) {
opts.handleError(error)
}
}
function getButtons(job: InstallJobSnapshot): PopupNotificationButton[] {
const buttons: PopupNotificationButton[] = []
if (isTerminalJob(job)) {
buttons.push({
label: formatMessage(messages.retry),
icon: UpdatedIcon,
color: 'brand',
keepOpen: true,
action: async () => {
await install_job_retry(job.job_id).catch(opts.handleError)
await refresh()
},
})
}
if (shouldShowCopyDetails(job)) {
const copied = isCopied(job)
buttons.push({
label: formatMessage(copied ? messages.copied : messages.copyDetails),
icon: copied ? CheckIcon : CopyIcon,
color: 'standard',
keepOpen: true,
action: async () => {
await copyJobDetails(job)
},
})
}
return buttons
}
function setJobs(nextJobs: InstallJobSnapshot[]) {
for (const job of nextJobs) {
if (!jobOrder.has(job.job_id)) {
jobOrder.set(job.job_id, nextJobOrder++)
}
}
const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status))
syncProgressSnapshots(visibleJobs)
jobs.value = visibleJobs.sort(
(a, b) =>
getJobSortRank(a) - getJobSortRank(b) ||
a.created.localeCompare(b.created) ||
(jobOrder.get(a.job_id) ?? 0) - (jobOrder.get(b.job_id) ?? 0),
)
}
const progressItems = computed<PopupNotificationProgressItem[]>(() =>
jobs.value.map((job) => {
const progress = getEffectiveProgress(job)
return {
id: job.job_id,
title: getTitle(job),
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,
buttons: getButtons(job),
onDismiss: isTerminalJob(job)
? async () => {
await install_job_dismiss(job.job_id).catch(opts.handleError)
await refresh()
}
: undefined,
}
}),
)
const buttons = computed<PopupNotificationButton[] | undefined>(() => undefined)
async function refreshMetadata(notify = true) {
const request = ++metadataRequest
const sourceJobs = jobs.value
const instanceIds = Array.from(
new Set(
sourceJobs
.map((job) => installJobInstanceId(job))
.filter((instanceId): instanceId is string => !!instanceId),
),
)
const instances = instanceIds.length
? await getInstances(instanceIds).catch((error) => {
opts.handleError(error)
return []
})
: []
if (request !== metadataRequest) {
return
}
const instanceIconUrls = new Map(
instances.map((instance) => [instance.id, getDisplayIconUrl(instance.icon_path)]),
)
instanceNames.value = Object.fromEntries(
instances.map((instance) => [instance.id, instance.name]),
)
iconUrls.value = Object.fromEntries(
sourceJobs.map((job) => [
job.job_id,
getDisplayIconUrl(job.display?.icon) ??
instanceIconUrls.get(installJobInstanceId(job) ?? '') ??
null,
]),
)
if (notify) {
opts.onChange()
}
}
async function refresh(notify = true) {
const request = ++refreshRequest
const nextJobs = await install_job_list(false).catch((error) => {
opts.handleError(error)
return []
})
if (request !== refreshRequest) {
return
}
setJobs(nextJobs)
await refreshMetadata(false)
if (request !== refreshRequest) {
return
}
if (notify) {
opts.onChange()
}
}
function applyJobUpdate(job: InstallJobSnapshot) {
refreshRequest += 1
const existingJob = jobs.value.find((item) => item.job_id === job.job_id)
if (existingJob && existingJob.modified.localeCompare(job.modified) > 0) {
return
}
setJobs([...jobs.value.filter((item) => item.job_id !== job.job_id), job])
opts.onChange()
void refreshMetadata()
}
await refresh(false)
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
return {
active: computed(() => jobs.value.length > 0),
title: computed(() => formatMessage(messages.installs)),
progressItems,
buttons,
refresh,
dispose: () => {
for (const timeout of copiedResetTimeouts.values()) {
window.clearTimeout(timeout)
}
for (const jobId of progressSnapshots.keys()) {
clearProgressSnapshot(jobId)
}
unlisten()
},
}
}
@@ -2,19 +2,24 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
import type { CardAction } from '@modrinth/ui'
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { ComputedRef, Ref } from 'vue'
import { onUnmounted, ref, shallowRef } from 'vue'
import type { Router } from 'vue-router'
import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, list as listInstances } from '@/helpers/profile.js'
import { kill, list as listInstances } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'
import { add_server_to_instance, getServerAddress } from '@/helpers/worlds'
interface BrowseServerInstance {
id: string
name: string
path: string
}
@@ -65,14 +70,13 @@ const messages = defineMessages({
export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
const runningServerProjects = ref<Record<string, string>>({})
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingCacheActive = true
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
@@ -83,13 +87,11 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
})
const newRunning: Record<string, string> = {}
for (const hit of hits) {
const inst = packs.find(
(pack: GameInstance) => pack.linked_data?.project_id === hit.project_id,
)
const inst = packs.find((pack: GameInstance) => pack.link?.project_id === hit.project_id)
if (inst) {
const processes = await get_by_profile_path(inst.path).catch(() => [])
const processes = await get_by_instance_id(inst.id).catch(() => [])
if (Array.isArray(processes) && processes.length > 0) {
newRunning[hit.project_id] = inst.path
newRunning[hit.project_id] = inst.id
}
}
}
@@ -99,9 +101,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
async function handleStopServerProject(projectId: string) {
debugLog('handleStopServerProject', projectId)
const instancePath = runningServerProjects.value[projectId]
if (!instancePath) return
await kill(instancePath).catch(() => {})
const instanceId = runningServerProjects.value[projectId]
if (!instanceId) return
await kill(instanceId).catch(() => {})
const { [projectId]: _, ...rest } = runningServerProjects.value
runningServerProjects.value = rest
}
@@ -118,9 +120,10 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
if (!address) return
if (options.instance.value) {
const instanceId = options.instance.value.id
try {
await add_server_to_profile(
options.instance.value.path,
await add_server_to_instance(
instanceId,
project.name,
address,
'prompt',
@@ -128,6 +131,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
project.minecraft_java_server?.content?.kind,
)
options.newlyInstalled.value.push(project.project_id)
await queryClient.invalidateQueries({ queryKey: ['worlds', instanceId] })
} catch (error) {
options.handleError(error)
}
@@ -145,37 +149,26 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
const cachedStatus = getFreshCachedServerStatus(queryClient, address)
if (cachedStatus) {
nextPings[hit.project_id] = cachedStatus.ping
}
}
serverPings.value = nextPings
await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
if (getFreshCachedServerStatus(queryClient, address)) return
let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((error) => {
console.error(`Failed to ping server ${address}:`, error)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
try {
const status = await fetchCachedServerStatus(queryClient, address)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
} catch (error) {
console.error(`Failed to ping server ${address}:`, error)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: undefined }
}
const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
@@ -289,11 +282,11 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
process_listener((event: { event: string; profile_path_id: string }) => {
process_listener((event: { event: string; instance_id: string }) => {
debugLog('process event', event)
if (event.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
([, path]) => path === event.profile_path_id,
([, path]) => path === event.instance_id,
)?.[0]
if (projectId) {
const { [projectId]: _, ...rest } = runningServerProjects.value
@@ -307,10 +300,8 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
.catch(options.handleError)
onUnmounted(() => {
serverPingCacheActive = false
serverPingsActive = false
unlistenProcesses?.()
serverPingCache.clear()
pendingServerPings.clear()
})
return {
@@ -0,0 +1,50 @@
import type { QueryClient } from '@tanstack/vue-query'
import {
get_server_status,
normalizeServerAddress,
type ProtocolVersion,
type ServerStatus,
} from '@/helpers/worlds'
export const SERVER_STATUS_CACHE_MS = 10 * 60 * 1000
function getProtocolVersionKey(protocolVersion: ProtocolVersion | null) {
if (!protocolVersion) return 'default'
return `${protocolVersion.version}:${protocolVersion.legacy ? 'legacy' : 'modern'}`
}
export function getServerStatusQueryKey(
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return [
'minecraft-server-status',
normalizeServerAddress(address) || address.trim().toLowerCase(),
getProtocolVersionKey(protocolVersion),
] as const
}
export function getFreshCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
const queryKey = getServerStatusQueryKey(address, protocolVersion)
const updatedAt = queryClient.getQueryState(queryKey)?.dataUpdatedAt ?? 0
if (!updatedAt || Date.now() - updatedAt >= SERVER_STATUS_CACHE_MS) return undefined
return queryClient.getQueryData<ServerStatus>(queryKey)
}
export async function fetchCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return await queryClient.fetchQuery({
queryKey: getServerStatusQueryKey(address, protocolVersion),
queryFn: () => get_server_status(address, protocolVersion),
staleTime: SERVER_STATUS_CACHE_MS,
gcTime: SERVER_STATUS_CACHE_MS,
})
}
@@ -22,8 +22,8 @@ interface InstanceConsoleEntry {
const instances = new Map<string, InstanceConsoleEntry>()
function getOrCreate(profilePathId: string): InstanceConsoleEntry {
let entry = instances.get(profilePathId)
function getOrCreate(instanceId: string): InstanceConsoleEntry {
let entry = instances.get(instanceId)
if (entry) return entry
entry = {
@@ -32,25 +32,25 @@ function getOrCreate(profilePathId: string): InstanceConsoleEntry {
historicalCache: new Map(),
logList: null,
}
instances.set(profilePathId, entry)
instances.set(instanceId, entry)
return entry
}
async function hydrate(profilePathId: string): Promise<void> {
const entry = getOrCreate(profilePathId)
async function hydrate(instanceId: string): Promise<void> {
const entry = getOrCreate(instanceId)
if (entry.liveConsole.output.value.length > 0) return
const buffer = await get_live_log_buffer(profilePathId)
const buffer = await get_live_log_buffer(instanceId)
if (buffer) {
entry.liveConsole.addLegacyLog(buffer)
}
}
async function getHistoricalLogs(profilePathId: string, instancePath: string): Promise<LogEntry[]> {
const entry = getOrCreate(profilePathId)
async function getHistoricalLogs(instanceId: string): Promise<LogEntry[]> {
const entry = getOrCreate(instanceId)
if (entry.logList) return entry.logList
const logs: LogEntry[] = await get_logs(instancePath, true)
const logs: LogEntry[] = await get_logs(instanceId, true)
entry.logList = logs
for (const log of logs) {
@@ -62,38 +62,38 @@ async function getHistoricalLogs(profilePathId: string, instancePath: string): P
return logs
}
function getHistoricalContent(profilePathId: string, filename: string): string | undefined {
return instances.get(profilePathId)?.historicalCache.get(filename)
function getHistoricalContent(instanceId: string, filename: string): string | undefined {
return instances.get(instanceId)?.historicalCache.get(filename)
}
function invalidate(profilePathId: string): void {
const entry = instances.get(profilePathId)
function invalidate(instanceId: string): void {
const entry = instances.get(instanceId)
if (!entry) return
entry.historicalCache.clear()
entry.logList = null
}
async function clearLive(profilePathId: string): Promise<void> {
const entry = getOrCreate(profilePathId)
async function clearLive(instanceId: string): Promise<void> {
const entry = getOrCreate(instanceId)
entry.liveConsole.clear()
await clear_log_buffer(profilePathId).catch(() => {})
await clear_log_buffer(instanceId).catch(() => {})
}
async function destroy(profilePathId: string): Promise<void> {
instances.delete(profilePathId)
await clear_log_buffer(profilePathId).catch(() => {})
async function destroy(instanceId: string): Promise<void> {
instances.delete(instanceId)
await clear_log_buffer(instanceId).catch(() => {})
}
export function useInstanceConsole(profilePathId: string) {
const entry = getOrCreate(profilePathId)
export function useInstanceConsole(instanceId: string) {
const entry = getOrCreate(instanceId)
return {
liveConsole: entry.liveConsole,
historicalConsole: entry.historicalConsole,
hydrate: () => hydrate(profilePathId),
getHistoricalLogs: (instancePath: string) => getHistoricalLogs(profilePathId, instancePath),
getHistoricalContent: (filename: string) => getHistoricalContent(profilePathId, filename),
invalidate: () => invalidate(profilePathId),
clearLive: () => clearLive(profilePathId),
destroy: () => destroy(profilePathId),
hydrate: () => hydrate(instanceId),
getHistoricalLogs: () => getHistoricalLogs(instanceId),
getHistoricalContent: (filename: string) => getHistoricalContent(instanceId, filename),
invalidate: () => invalidate(instanceId),
clearLive: () => clearLive(instanceId),
destroy: () => destroy(instanceId),
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
+29 -11
View File
@@ -31,8 +31,8 @@ import { listen } from '@tauri-apps/api/event'
pack_name: name of the pack
pack_id, optional, the id of the modpack
pack_version, optional, the version of the modpack
profile_name: name of the profile
profile_uuid: unique identification of the profile
instance_name: name of the instance
instance_id: unique identification of the instance
}
loader_uuid: unique identification of the loading bar
@@ -57,18 +57,32 @@ export async function process_listener(callback) {
return await listen('process', (event) => callback(event.payload))
}
/// Payload for the 'profile' event
/// Payload for the 'instance' event
/*
ProfilePayload {
uuid: unique identification of the process in the state (currently identified by path, but that will change)
name: name of the profile
profile_path: relative path toprofile_listener profile (used for path identification)
path: path to profile (used for opening the profile in the OS file explorer)
InstancePayload {
instance_id: unique identification of the instance
event: event type ("Created", "Added", "Edited", "Removed")
}
*/
export async function profile_listener(callback) {
return await listen('profile', (event) => callback(event.payload))
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
@@ -98,10 +112,14 @@ 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 {
profile_path_id: string,
instance_id: string,
type: "log4j" | "legacy",
// log4j fields (when type === "log4j"):
timestamp_millis?: number,
+1 -1
View File
@@ -8,7 +8,7 @@ import type { ModrinthCredentials } from '@/helpers/mr_auth'
export type UserStatus = {
user_id: string
profile_name: string | null
instance_name: string | null
last_update: string
}
+3 -14
View File
@@ -1,11 +1,11 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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 { create } from './profile'
import { install_import_instance } from './install'
/*
API for importing instances from other launchers
@@ -32,19 +32,8 @@ export async function get_importable_instances(launcherType, basePath) {
}
/// Import an instance from a launcher type and base path
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
export async function import_instance(launcherType, basePath, instanceFolder) {
// create a basic, empty instance (most properties will be filled in by the import process)
// We do NOT watch the fs for changes to avoid duplicate events during installation
// fs watching will be enabled once the instance is imported
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
return await invoke('plugin:import|import_instance', {
profilePath,
launcherType,
basePath,
instanceFolder,
})
return await install_import_instance(launcherType, basePath, instanceFolder)
}
/// Checks if this instance is valid for importing, given a certain launcher type
+301
View File
@@ -0,0 +1,301 @@
import { invoke } from '@tauri-apps/api/core'
import { install_job_listener } from './events'
import type { InstanceLink, InstanceLoader } from './types'
export interface PackLocationVersionId {
type: 'fromVersionId'
project_id: string
version_id: string
title: string
icon_url?: string | null
}
export interface PackLocationFile {
type: 'fromFile'
path: string
}
export type CreatePackLocation = PackLocationVersionId | PackLocationFile
export interface InstallModpackPreview {
name: string
gameVersion: string
modloader: InstanceLoader
loaderVersion: string | null
icon?: string | null
iconUrl?: string | null
link?: InstanceLink | null
unknownFile: boolean
}
export interface InstallCreateInstanceRequest {
name: string
gameVersion: string
loader: InstanceLoader
loaderVersion: string | null
iconPath: string | null
link?: InstanceLink | null
}
export interface InstallPostInstallEdit {
name?: string | null
iconPath?: string | null
link?: InstanceLink | null
}
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
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'
| '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,
})
}
export async function install_create_instance(request: InstallCreateInstanceRequest) {
return await invoke<InstallJobSnapshot>('plugin:install|install_create_instance', { request })
}
export async function install_create_modpack_instance(
location: CreatePackLocation,
postInstallEdit?: InstallPostInstallEdit | null,
) {
return await invoke<InstallJobSnapshot>('plugin:install|install_create_modpack_instance', {
location,
postInstallEdit,
})
}
export async function install_import_instance(
launcherType: string,
basePath: string,
instanceFolder: string,
) {
return await invoke<InstallJobSnapshot>('plugin:install|install_import_instance', {
launcherType,
basePath,
instanceFolder,
})
}
export async function install_duplicate_instance(sourceInstanceId: string) {
return await invoke<InstallJobSnapshot>('plugin:install|install_duplicate_instance', {
sourceInstanceId,
})
}
export async function install_existing_instance(instanceId: string, force: boolean) {
return await invoke<InstallJobSnapshot>('plugin:install|install_existing_instance', {
instanceId,
force,
})
}
export async function install_pack_to_existing_instance(
instanceId: string,
location: CreatePackLocation,
postInstallEdit?: InstallPostInstallEdit | null,
) {
return await invoke<InstallJobSnapshot>('plugin:install|install_pack_to_existing_instance', {
instanceId,
location,
postInstallEdit,
})
}
export async function install_job_list(includeFinished: boolean) {
return await invoke<InstallJobSnapshot[]>('plugin:install|install_job_list', { includeFinished })
}
export async function install_job_get(jobId: string) {
return await invoke<InstallJobSnapshot>('plugin:install|install_job_get', { jobId })
}
export async function install_job_retry(jobId: string) {
return await invoke<InstallJobSnapshot>('plugin:install|install_job_retry', { jobId })
}
export async function install_job_cancel(jobId: string) {
return await invoke<InstallJobSnapshot>('plugin:install|install_job_cancel', { jobId })
}
export async function install_job_dismiss(jobId: string) {
return await invoke<void>('plugin:install|install_job_dismiss', { jobId })
}
export async function install_job_support_details(jobId: string) {
return await invoke<string>('plugin:install|install_job_support_details', { jobId })
}
export function installJobInstanceId(job: InstallJobSnapshot): string | null {
return job.instance_id ?? job.target.instance_id ?? null
}
export function isInstallJobFinished(status: InstallJobStatus) {
return (
status === 'succeeded' ||
status === 'failed' ||
status === 'interrupted' ||
status === 'canceled'
)
}
function settleInstallJob(job: InstallJobSnapshot) {
if (job.status === 'succeeded') return job
throw new Error(job.error?.message ?? `Install job ${job.job_id} ${job.status}`)
}
export async function wait_for_install_job(jobId: string) {
const current = await install_job_get(jobId)
if (isInstallJobFinished(current.status)) return settleInstallJob(current)
return await new Promise<InstallJobSnapshot>((resolve, reject) => {
let finished = false
let unlisten: (() => void) | null = null
const cleanup = () => {
if (unlisten) {
unlisten()
unlisten = null
}
}
const resolveJob = (job: InstallJobSnapshot) => {
if (finished || job.job_id !== jobId || !isInstallJobFinished(job.status)) return
finished = true
cleanup()
try {
resolve(settleInstallJob(job))
} catch (err) {
reject(err)
}
}
const rejectWait = (err: unknown) => {
if (finished) return
finished = true
cleanup()
reject(err)
}
install_job_listener(resolveJob)
.then((listener) => {
if (finished) {
listener()
return
}
unlisten = listener
install_job_get(jobId).then(resolveJob).catch(rejectWait)
})
.catch(rejectWait)
})
}
@@ -10,7 +10,7 @@ import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/profile'
} from '@/helpers/instance'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
+333
View File
@@ -0,0 +1,333 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import type { InstallJobSnapshot } from './install'
import type {
CacheBehaviour,
ContentFile,
ContentFileProjectType,
GameInstance,
InstanceLoader,
} from './types'
export async function remove(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_remove', { instanceId })
}
export async function get(instanceId: string): Promise<GameInstance | null> {
return await invoke('plugin:instance|instance_get', { instanceId })
}
export async function get_many(instanceIds: string[]): Promise<GameInstance[]> {
return await invoke('plugin:instance|instance_get_many', { instanceIds })
}
export async function get_projects(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<Record<string, ContentFile>> {
return await invoke('plugin:instance|instance_get_projects', { instanceId, cacheBehaviour })
}
export async function get_installed_project_ids(instanceId: string): Promise<string[]> {
return await invoke('plugin:instance|instance_get_installed_project_ids', { instanceId })
}
export type InstanceInstallTarget = {
game_version: string
loader: string
}
export type InstanceInstallCandidate = {
id: string
name: string
icon_path?: string | null
game_version: string
loader: InstanceLoader
installed: boolean
compatible: boolean
}
export async function get_install_candidates(
projectId: string,
projectType: string,
targets: InstanceInstallTarget[],
): Promise<InstanceInstallCandidate[]> {
return await invoke('plugin:instance|instance_get_install_candidates', {
projectId,
projectType,
targets,
})
}
// Get content items with rich metadata for an instance
// Returns content items filtered to exclude modpack files (if linked),
// sorted alphabetically by project name
export async function get_content_items(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_content_items', { instanceId, cacheBehaviour })
}
// Linked modpack info returned from backend
export interface LinkedModpackInfo {
project: Labrinth.Projects.v2.Project
version: Labrinth.Versions.v2.Version
owner: ContentOwner | null
has_update: boolean
update_version_id: string | null
update_version: Labrinth.Versions.v2.Version | null
}
// Get linked modpack info for an instance
// Returns project, version, and owner information for the linked modpack,
// or null if the instance is not linked to a modpack
export async function get_linked_modpack_info(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<LinkedModpackInfo | null> {
return await invoke('plugin:instance|instance_get_linked_modpack_info', {
instanceId,
cacheBehaviour,
})
}
// Get content items that are part of the linked modpack
// Returns the modpack's dependencies as ContentItem list
// Returns empty array if the instance is not linked to a modpack
export async function get_linked_modpack_content(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:instance|instance_get_linked_modpack_content', {
instanceId,
cacheBehaviour,
})
}
// Convert a list of dependencies into ContentItems with rich metadata
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,
})
}
export async function get_full_path(instanceId: string): Promise<string> {
return await invoke('plugin:instance|instance_get_full_path', { instanceId })
}
export async function get_mod_full_path(instanceId: string, projectPath: string): Promise<string> {
return await invoke('plugin:instance|instance_get_mod_full_path', { instanceId, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
export async function get_optimal_jre_key(instanceId: string): Promise<JavaVersion | null> {
return await invoke('plugin:instance|instance_get_optimal_jre_key', { instanceId })
}
export async function list(): Promise<GameInstance[]> {
return await invoke('plugin:instance|instance_list')
}
export async function check_installed(instanceId: string, projectId: string): Promise<boolean> {
return await invoke('plugin:instance|instance_check_installed', { instanceId, projectId })
}
export async function update_all(instanceId: string): Promise<Record<string, string>> {
return await invoke('plugin:instance|instance_update_all', { instanceId })
}
// Updates a specified project
export async function update_project(instanceId: string, projectPath: string): Promise<string> {
return await invoke('plugin:instance|instance_update_project', { instanceId, projectPath })
}
// Add a project to an instance from a version
// Returns a path to the new project file
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
export interface ResolutionPreferences {
game_versions?: string[]
loaders?: string[]
}
export interface ResolveContentRequest {
project_id: string
version_id?: string | null
content_type: Labrinth.Content.v3.ContentType
selected?: ResolutionPreferences
}
export interface ResolvedContent {
project_id: string
version_id: string
dependent_on_version_id?: string | null
}
export interface ResolveContentPlan {
primary: ResolvedContent
dependencies: ResolvedContent[]
skipped: Array<{
project_id: string
version_id?: string | null
dependent_on_version_id?: string | null
reason: string
}>
}
export async function add_project_from_version(
instanceId: string,
versionId: string,
reason: DownloadReason,
dependentOnVersionId?: string,
): Promise<string> {
return await invoke('plugin:instance|instance_add_project_from_version', {
instanceId,
versionId,
reason,
dependentOnVersionId,
})
}
export async function install_project_with_dependencies(
instanceId: string,
request: ResolveContentRequest,
): Promise<ResolveContentPlan> {
return await invoke('plugin:instance|instance_install_project_with_dependencies', {
instanceId,
request,
})
}
export async function switch_project_version_with_dependencies(
instanceId: string,
projectPath: string,
versionId: string,
): Promise<string> {
return await invoke('plugin:instance|instance_switch_project_version_with_dependencies', {
instanceId,
projectPath,
versionId,
})
}
// Add a project to an instance from a path + project_type
// Returns a path to the new project file
export async function add_project_from_path(
instanceId: string,
projectPath: string,
projectType?: ContentFileProjectType,
): Promise<string> {
return await invoke('plugin:instance|instance_add_project_from_path', {
instanceId,
projectPath,
projectType,
})
}
// Toggle disabling a project
export async function toggle_disable_project(
instanceId: string,
projectPath: string,
desiredEnabled?: boolean,
): Promise<string> {
return await invoke('plugin:instance|instance_toggle_disable_project', {
instanceId,
projectPath,
desiredEnabled,
})
}
// Remove a project
export async function remove_project(instanceId: string, projectPath: string): Promise<void> {
return await invoke('plugin:instance|instance_remove_project', { instanceId, projectPath })
}
// Update a managed Modrinth instance to a specific version
export async function update_managed_modrinth_version(
instanceId: string,
versionId: string,
): Promise<InstallJobSnapshot> {
return await invoke('plugin:instance|instance_update_managed_modrinth_version', {
instanceId,
versionId,
})
}
// Repair a managed Modrinth instance
export async function update_repair_modrinth(instanceId: string): Promise<InstallJobSnapshot> {
return await invoke('plugin:instance|instance_repair_managed_modrinth', { instanceId })
}
// Export an instance to .mrpack
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
// Version id is optional (ie: 1.1.5)
export async function export_instance_mrpack(
instanceId: string,
exportLocation: string,
includedOverrides: string[],
versionId?: string,
description?: string,
name?: string,
): Promise<void> {
return await invoke('plugin:instance|instance_export_mrpack', {
instanceId,
exportLocation,
includedOverrides,
versionId,
description,
name,
})
}
// Given a folder path, populate an array of all the subfolders
// Intended to be used for finding potential override folders
// profile
// -- mods
// -- resourcepacks
// -- file1
// => [mods, resourcepacks]
// allows selection for 'included_overrides' in export_instance_mrpack
export async function get_pack_export_candidates(instanceId: string): Promise<string[]> {
return await invoke('plugin:instance|instance_get_pack_export_candidates', { instanceId })
}
// Run Minecraft using an instance
// Returns PID of child
export async function run(
instanceId: string,
serverAddress: string | null = null,
): Promise<unknown> {
return await invoke('plugin:instance|instance_run', { instanceId, serverAddress })
}
export async function kill(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_kill', { instanceId })
}
// Edits an instance
export async function edit(instanceId: string, editInstance: Partial<GameInstance>): Promise<void> {
return await invoke('plugin:instance|instance_edit', { instanceId, editInstance })
}
// Edits an instance's icon
export async function edit_icon(instanceId: string, iconPath: string | null): Promise<void> {
return await invoke('plugin:instance|instance_edit_icon', { instanceId, iconPath })
}
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
+25 -25
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
@@ -14,41 +14,41 @@ pub struct Logs {
}
*/
/// Get all logs that exist for a given profile
/// Get all logs that exist for a given instance
/// This is returned as an array of Log objects, sorted by filename (the folder name, when the log was created)
export async function get_logs(profilePath, clearContents) {
return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
export async function get_logs(instanceId, clearContents) {
return await invoke('plugin:logs|logs_get_logs', { instanceId, clearContents })
}
/// Get a profile's log by filename
export async function get_logs_by_filename(profilePath, logType, filename) {
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
/// Get an instance's log by filename
export async function get_logs_by_filename(instanceId, logType, filename) {
return await invoke('plugin:logs|logs_get_logs_by_filename', { instanceId, logType, filename })
}
/// Get a profile's log text only by filename
export async function get_output_by_filename(profilePath, logType, filename) {
/// Get an instance's log text only by filename
export async function get_output_by_filename(instanceId, logType, filename) {
return await invoke('plugin:logs|logs_get_output_by_filename', {
profilePath,
instanceId,
logType,
filename,
})
}
/// Delete a profile's log by filename
export async function delete_logs_by_filename(profilePath, logType, filename) {
/// Delete an instance's log by filename
export async function delete_logs_by_filename(instanceId, logType, filename) {
return await invoke('plugin:logs|logs_delete_logs_by_filename', {
profilePath,
instanceId,
logType,
filename,
})
}
/// Delete all logs for a given profile
export async function delete_logs(profilePath) {
return await invoke('plugin:logs|logs_delete_logs', { profilePath })
/// Delete all logs for a given instance
export async function delete_logs(instanceId) {
return await invoke('plugin:logs|logs_delete_logs', { instanceId })
}
/// Get the latest log for a given profile and cursor (startpoint to read withi nthe file)
/// Get the latest log for a given instance and cursor (startpoint to read within the file)
/// Returns:
/*
{
@@ -59,16 +59,16 @@ export async function delete_logs(profilePath) {
*/
// From latest.log directly
export async function get_latest_log_cursor(profilePath, cursor) {
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
export async function get_latest_log_cursor(instanceId, cursor) {
return await invoke('plugin:logs|logs_get_latest_log_cursor', { instanceId, cursor })
}
/// Get all buffered live log lines for a profile from the Rust ring buffer
export async function get_live_log_buffer(profilePath) {
return await invoke('plugin:logs|logs_get_live_log_buffer', { profilePath })
/// Get all buffered live log lines for an instance from the Rust ring buffer
export async function get_live_log_buffer(instanceId) {
return await invoke('plugin:logs|logs_get_live_log_buffer', { instanceId })
}
/// Clear the live log buffer for a profile on the Rust side
export async function clear_log_buffer(profilePath) {
return await invoke('plugin:logs|logs_clear_live_log_buffer', { profilePath })
/// Clear the live log buffer for an instance on the Rust side
export async function clear_log_buffer(instanceId) {
return await invoke('plugin:logs|logs_clear_live_log_buffer', { instanceId })
}
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
-105
View File
@@ -1,105 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
import { create } from './profile'
import type { InstanceLoader } from './types'
interface PackProfileCreator {
name: string
gameVersion: string
modloader: InstanceLoader
loaderVersion: string | null
unknownFile: boolean
}
interface PackLocationVersionId {
type: 'fromVersionId'
project_id: string
version_id: string
title: string
icon_url?: string
}
interface PackLocationFile {
type: 'fromFile'
path: string
}
export async function create_profile_and_install(
projectId: string,
versionId: string,
packTitle: string,
iconUrl?: string,
createInstanceCallback: (profile: string) => void = () => {},
): Promise<void> {
const location: PackLocationVersionId = {
type: 'fromVersionId',
project_id: projectId,
version_id: versionId,
title: packTitle,
icon_url: iconUrl,
}
const profile_creator = await invoke<PackProfileCreator>(
'plugin:pack|pack_get_profile_from_pack',
{ location },
)
const profile = await create(
profile_creator.name,
profile_creator.gameVersion,
profile_creator.modloader,
profile_creator.loaderVersion,
null,
true,
)
createInstanceCallback(profile)
return await invoke('plugin:pack|pack_install', { location, profile })
}
export async function install_to_existing_profile(
projectId: string,
versionId: string,
title: string,
profilePath: string,
): Promise<void> {
const location: PackLocationVersionId = {
type: 'fromVersionId',
project_id: projectId,
version_id: versionId,
title,
}
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
}
export async function create_profile_and_install_from_file(
path: string,
showUnknownPackWarningModal?: (createProfile: () => Promise<void>, fileName: string) => void,
): Promise<void> {
const location: PackLocationFile = {
type: 'fromFile',
path,
}
const profile_creator = await invoke<PackProfileCreator>(
'plugin:pack|pack_get_profile_from_pack',
{ location },
)
const createProfile = async () => {
const profile = await create(
profile_creator.name,
profile_creator.gameVersion,
profile_creator.modloader,
profile_creator.loaderVersion,
null,
true,
)
await invoke('plugin:pack|pack_install', { location, profile })
}
if (profile_creator.unknownFile && showUnknownPackWarningModal) {
const splitPath = path.split(/[\\/]/)
const fileName = splitPath ? splitPath[splitPath.length - 1] : path
showUnknownPackWarningModal(createProfile, fileName)
} else {
await createProfile()
}
}
+5 -5
View File
@@ -1,17 +1,17 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
/// Gets all running process IDs with a given profile path
/// Gets all running process IDs with a given instance ID
/// Returns [u32]
export async function get_by_profile_path(path) {
return await invoke('plugin:process|process_get_by_profile_path', { path })
export async function get_by_instance_id(instanceId) {
return await invoke('plugin:process|process_get_by_instance_id', { instanceId })
}
/// Gets all running process IDs with a given profile path
/// Gets all running process IDs
/// Returns [u32]
export async function get_all() {
return await invoke('plugin:process|process_get_all')
-315
View File
@@ -1,315 +0,0 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import { install_to_existing_profile } from '@/helpers/pack'
import type {
CacheBehaviour,
ContentFile,
ContentFileProjectType,
GameInstance,
InstanceLoader,
} from './types'
// Add instance
/*
name: String, // the name of the profile, and relative path to create
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
- ModLoader is an enum, with the following variants: Vanilla, Forge, Fabric, Quilt
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Path, // the icon for the profile
- icon is a path to an image file, which will be copied into the profile directory
*/
export async function create(
name: string,
gameVersion: string,
modloader: InstanceLoader,
loaderVersion: string | null,
icon: string | null,
skipInstall: boolean,
linkedData?: { project_id: string; version_id: string; locked: boolean } | null,
): Promise<string> {
// Trim string name to avoid "Unable to find directory"
name = name.trim()
return await invoke('plugin:profile-create|profile_create', {
name,
gameVersion,
modloader,
loaderVersion,
icon,
skipInstall,
linkedData,
})
}
// duplicate a profile
export async function duplicate(path: string): Promise<string> {
return await invoke('plugin:profile-create|profile_duplicate', { path })
}
// Remove a profile
export async function remove(path: string): Promise<void> {
return await invoke('plugin:profile|profile_remove', { path })
}
// Get a profile by path
// Returns a Profile
export async function get(path: string): Promise<GameInstance | null> {
return await invoke('plugin:profile|profile_get', { path })
}
export async function get_many(paths: string[]): Promise<GameInstance[]> {
return await invoke('plugin:profile|profile_get_many', { paths })
}
// Get a profile's projects
// Returns a map of a path to profile file
export async function get_projects(
path: string,
cacheBehaviour?: CacheBehaviour,
): Promise<Record<string, ContentFile>> {
return await invoke('plugin:profile|profile_get_projects', { path, cacheBehaviour })
}
// Get just the installed project IDs for a profile (lightweight, skips update checks)
export async function get_installed_project_ids(path: string): Promise<string[]> {
return await invoke('plugin:profile|profile_get_installed_project_ids', { path })
}
// Get content items with rich metadata for a profile
// Returns content items filtered to exclude modpack files (if linked),
// sorted alphabetically by project name
export async function get_content_items(
path: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:profile|profile_get_content_items', { path, cacheBehaviour })
}
// Linked modpack info returned from backend
export interface LinkedModpackInfo {
project: Labrinth.Projects.v2.Project
version: Labrinth.Versions.v2.Version
owner: ContentOwner | null
has_update: boolean
update_version_id: string | null
update_version: Labrinth.Versions.v2.Version | null
}
// Get linked modpack info for a profile
// Returns project, version, and owner information for the linked modpack,
// or null if the profile is not linked to a modpack
export async function get_linked_modpack_info(
path: string,
cacheBehaviour?: CacheBehaviour,
): Promise<LinkedModpackInfo | null> {
return await invoke('plugin:profile|profile_get_linked_modpack_info', { path, cacheBehaviour })
}
// Get content items that are part of the linked modpack
// Returns the modpack's dependencies as ContentItem list
// Returns empty array if the profile is not linked to a modpack
export async function get_linked_modpack_content(
path: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:profile|profile_get_linked_modpack_content', { path, cacheBehaviour })
}
// Convert a list of dependencies into ContentItems with rich metadata
export async function get_dependencies_as_content_items(
dependencies: Labrinth.Versions.v3.Dependency[],
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke('plugin:profile|profile_get_dependencies_as_content_items', {
dependencies,
cacheBehaviour,
})
}
// Get a profile's full fs path
// Returns a path
export async function get_full_path(path: string): Promise<string> {
return await invoke('plugin:profile|profile_get_full_path', { path })
}
// Get's a mod's full fs path
// Returns a path
export async function get_mod_full_path(path: string, projectPath: string): Promise<string> {
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
// Get optimal java version from profile
// Returns a java version
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
// Get a copy of the profile set
// Returns hashmap of path -> Profile
export async function list(): Promise<GameInstance[]> {
return await invoke('plugin:profile|profile_list')
}
export async function check_installed(path: string, projectId: string): Promise<boolean> {
return await invoke('plugin:profile|profile_check_installed', { path, projectId })
}
export async function check_installed_batch(projectId: string): Promise<Record<string, boolean>> {
return await invoke('plugin:profile|profile_check_installed_batch', { projectId })
}
// Installs/Repairs a profile
export async function install(path: string, force: boolean): Promise<void> {
return await invoke('plugin:profile|profile_install', { path, force })
}
// Updates all of a profile's projects
export async function update_all(path: string): Promise<Record<string, string>> {
return await invoke('plugin:profile|profile_update_all', { path })
}
// Updates a specified project
export async function update_project(path: string, projectPath: string): Promise<string> {
return await invoke('plugin:profile|profile_update_project', { path, projectPath })
}
// Add a project to a profile from a version
// Returns a path to the new project file
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
export async function add_project_from_version(
path: string,
versionId: string,
reason: DownloadReason,
): Promise<string> {
return await invoke('plugin:profile|profile_add_project_from_version', {
path,
versionId,
reason,
})
}
// Add a project to a profile from a path + project_type
// Returns a path to the new project file
export async function add_project_from_path(
path: string,
projectPath: string,
projectType?: ContentFileProjectType,
): Promise<string> {
return await invoke('plugin:profile|profile_add_project_from_path', {
path,
projectPath,
projectType,
})
}
// Toggle disabling a project
export async function toggle_disable_project(path: string, projectPath: string): Promise<string> {
return await invoke('plugin:profile|profile_toggle_disable_project', { path, projectPath })
}
// Remove a project
export async function remove_project(path: string, projectPath: string): Promise<void> {
return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
}
// Update a managed Modrinth profile to a specific version
export async function update_managed_modrinth_version(
path: string,
versionId: string,
): Promise<void> {
return await invoke('plugin:profile|profile_update_managed_modrinth_version', {
path,
versionId,
})
}
// Repair a managed Modrinth profile
export async function update_repair_modrinth(path: string): Promise<void> {
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
}
// Export a profile to .mrpack
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
// Version id is optional (ie: 1.1.5)
export async function export_profile_mrpack(
path: string,
exportLocation: string,
includedOverrides: string[],
versionId?: string,
description?: string,
name?: string,
): Promise<void> {
return await invoke('plugin:profile|profile_export_mrpack', {
path,
exportLocation,
includedOverrides,
versionId,
description,
name,
})
}
// Given a folder path, populate an array of all the subfolders
// Intended to be used for finding potential override folders
// profile
// -- mods
// -- resourcepacks
// -- file1
// => [mods, resourcepacks]
// allows selection for 'included_overrides' in export_profile_mrpack
export async function get_pack_export_candidates(profilePath: string): Promise<string[]> {
return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath })
}
// Run Minecraft using a pathed profile
// Returns PID of child
export async function run(path: string, serverAddress: string | null = null): Promise<unknown> {
return await invoke('plugin:profile|profile_run', { path, serverAddress })
}
export async function kill(path: string): Promise<void> {
return await invoke('plugin:profile|profile_kill', { path })
}
// Edits a profile
export async function edit(path: string, editProfile: Partial<GameInstance>): Promise<void> {
return await invoke('plugin:profile|profile_edit', { path, editProfile })
}
// Edits a profile's icon
export async function edit_icon(path: string, iconPath: string | null): Promise<void> {
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
}
export async function finish_install(instance: GameInstance): Promise<void> {
if (instance.install_stage !== 'pack_installed') {
const linkedData = instance.linked_data
if (linkedData) {
await install_to_existing_profile(
linkedData.project_id,
linkedData.version_id,
instance.name,
instance.path,
)
}
} else {
await install(instance.path, false)
}
}
@@ -15,12 +15,10 @@ import { skinPreviewStorage } from '../storage/skin-preview-storage'
export interface RenderResult {
forwards: string
backwards: string
}
export interface RawRenderResult {
forwards: Blob
backwards: Blob
}
class BatchSkinRenderer {
@@ -92,12 +90,9 @@ class BatchSkinRenderer {
}
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
const backwards = await this.renderView(backCameraPos, lookAtTarget)
return { forwards, backwards }
return { forwards }
}
private async renderView(
@@ -404,16 +399,15 @@ async function generateSkinPreviewsForGeneration(
const headKey = headKeys[i]
const rawCached = cachedSkinPreviews[skinKey]
if (rawCached) {
if (rawCached && !skinBlobUrlMap.has(skinKey)) {
const cached: RenderResult = {
forwards: URL.createObjectURL(rawCached.forwards),
backwards: URL.createObjectURL(rawCached.backwards),
}
skinBlobUrlMap.set(skinKey, cached)
}
const cachedHead = cachedHeadPreviews[headKey]
if (cachedHead) {
if (cachedHead && !headBlobUrlMap.has(headKey)) {
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
}
}
@@ -427,7 +421,6 @@ async function generateSkinPreviewsForGeneration(
if (DEBUG_MODE) {
const result = skinBlobUrlMap.get(key)!
URL.revokeObjectURL(result.forwards)
URL.revokeObjectURL(result.backwards)
skinBlobUrlMap.delete(key)
} else continue
}
@@ -456,7 +449,6 @@ async function generateSkinPreviewsForGeneration(
const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
}
skinBlobUrlMap.set(key, renderResult)
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
+6
View File
@@ -142,6 +142,12 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
})
}
export async function set_custom_skin_order(textureKeys: string[]): Promise<void> {
await invoke('plugin:minecraft-skins|set_custom_skin_order', {
textureKeys,
})
}
export async function save_custom_skin(
skin: Skin,
textureBlob: Uint8Array,
+4 -2
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
@@ -8,8 +8,10 @@ import { invoke } from '@tauri-apps/api/core'
export interface LoadingBarType {
type?: string
version?: string
profile_path?: string
instance_id?: string
instance_name?: string
pack_name?: string
icon?: string | null
}
export interface LoadingBar {
@@ -2,7 +2,6 @@ import type { RawRenderResult } from '../rendering/batch-skin-renderer'
interface StoredPreview {
forwards: Blob
backwards: Blob
timestamp: number
}
@@ -38,7 +37,6 @@ export class SkinPreviewStorage {
const storedPreview: StoredPreview = {
forwards: result.forwards,
backwards: result.backwards,
timestamp: Date.now(),
}
@@ -67,7 +65,7 @@ export class SkinPreviewStorage {
return
}
resolve({ forwards: result.forwards, backwards: result.backwards })
resolve({ forwards: result.forwards })
}
request.onerror = () => reject(request.error)
})
@@ -95,7 +93,7 @@ export class SkinPreviewStorage {
const result = request.result as StoredPreview | undefined
if (result) {
results[key] = { forwards: result.forwards, backwards: result.backwards }
results[key] = { forwards: result.forwards }
} else {
results[key] = null
}
@@ -173,7 +171,7 @@ export class SkinPreviewStorage {
const key = cursor.primaryKey as string
const value = cursor.value as StoredPreview
const entrySize = value.forwards.size + value.backwards.size
const entrySize = value.forwards.size
totalSize += entrySize
count++
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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'
+62 -6
View File
@@ -1,19 +1,23 @@
import type { ModrinthId } from '@modrinth/utils'
export type GameInstance = {
id: string
path: string
install_stage: InstallStage
launcher_feature_version: string
name: string
icon_path?: string
game_version: string
protocol_version?: number
loader: InstanceLoader
loader_version?: string
groups: string[]
linked_data?: LinkedData
link?: InstanceLink | null
update_channel: ReleaseChannel
created: Date
modified: Date
@@ -39,16 +43,61 @@ type InstallStage =
| 'pack_installing'
| 'not_installed'
type LinkedData = {
project_id: ModrinthId
version_id: ModrinthId
locked: boolean
type InstanceLinkIdentity = {
project_id?: ModrinthId | null
version_id?: ModrinthId | null
server_project_id?: ModrinthId | null
content_project_id?: ModrinthId | null
content_version_id?: ModrinthId | null
}
export type InstanceLink = InstanceLinkIdentity &
(
| {
type: 'modrinth_modpack'
project_id: ModrinthId
version_id: ModrinthId
}
| {
type: 'server_project'
project_id: ModrinthId
}
| {
type: 'server_project_modpack'
server_project_id: ModrinthId
content_project_id?: ModrinthId | null
content_version_id: ModrinthId
project_id?: ModrinthId
version_id?: ModrinthId
}
| {
type: 'imported_modpack'
project_id?: ModrinthId | null
version_id?: ModrinthId | null
name?: string | null
version_number?: string | null
filename?: string | null
}
| {
type: 'modrinth_hosting'
server_id: string
instance_ids: string[]
active_instance_id?: string | null
}
| {
type: 'shared_instance'
shared_instance_id: string
}
)
export type Instance = GameInstance
type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
enabled: boolean
metadata?: {
project_id: string
version_id: string
@@ -84,11 +133,18 @@ type Hooks = {
type Manifest = {
gameVersions: ManifestGameVersion[]
versionGroups?: ManifestVersionGroup[]
}
type ManifestGameVersion = {
id: string
stable: boolean
versionGroup?: string
loaders: ManifestLoaderVersion[]
}
type ManifestVersionGroup = {
id: string
loaders: ManifestLoaderVersion[]
}
@@ -0,0 +1,23 @@
import type { Labrinth } from '@modrinth/api-client'
const MIDAS_BITFLAG = 1 << 0
const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
type Pride26Campaign = Labrinth.Users.v3.Pride26CampaignDonation | null | undefined
export function hasMidasBadge(user?: { badges?: number } | null) {
return !!user?.badges && (user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG
}
export function hasPride26Badge(campaign: Pride26Campaign) {
return campaign?.has_badge === true
}
export function hasActivePride26Midas(campaign: Pride26Campaign, now = Date.now()) {
if (campaign?.has_midas !== true) {
return false
}
const donatedAt = new Date(campaign.last_donated_at).getTime()
return Number.isFinite(donatedAt) && donatedAt + PRIDE_26_MIDAS_DURATION_MS > now
}
+27 -6
View File
@@ -1,6 +1,7 @@
import { invoke } from '@tauri-apps/api/core'
import { save } from '@tauri-apps/plugin-dialog'
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
import { get_full_path, get_mod_full_path } from '@/helpers/instance'
export async function isDev() {
return await invoke('is_dev')
@@ -47,14 +48,34 @@ export async function showLauncherLogsFolder() {
return await invoke('plugin:utils|show_launcher_logs_folder', {})
}
// Opens a profile's folder in the OS file explorer
export async function showProfileInFolder(path) {
const fullPath = await get_full_path(path)
export async function createInstanceShortcut(instanceName, instanceId, options = {}) {
const outputPath = await save({
defaultPath: `Modrinth - ${instanceName}`,
})
if (!outputPath) return null
return await invoke('plugin:shortcuts|create_instance_shortcut', {
instanceName,
instanceId,
outputPath,
server: options.server,
singleplayerWorld: options.singleplayerWorld,
})
}
export async function showAppDbBackupsFolder() {
return await invoke('plugin:utils|show_app_db_backups_folder', {})
}
// Opens an instance's folder in the OS file explorer
export async function showInstanceInFolder(instanceId) {
const fullPath = await get_full_path(instanceId)
return await openPath(fullPath)
}
export async function highlightModInProfile(profilePath, projectPath) {
const fullPath = await get_mod_full_path(profilePath, projectPath)
export async function highlightModInInstance(instanceId, projectPath) {
const fullPath = await get_mod_full_path(instanceId, projectPath)
return await highlightInFolder(fullPath)
}
+94 -57
View File
@@ -3,7 +3,7 @@ import { autoToHTML } from '@sfirew/minecraft-motd-parser'
import { invoke } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import { get_full_path } from '@/helpers/profile'
import { get_full_path } from '@/helpers/instance'
import { openPath } from '@/helpers/utils'
type BaseWorld = {
@@ -36,8 +36,8 @@ export type ServerWorld = BaseWorld & {
export type World = SingleplayerWorld | ServerWorld
export type WorldWithProfile = {
profile: string
export type WorldWithInstance = {
instance_id: string
} & World
export type SingleplayerGameMode = 'survival' | 'creative' | 'adventure' | 'spectator'
@@ -88,12 +88,12 @@ export type ProtocolVersion = {
export async function get_recent_worlds(
limit: number,
displayStatuses?: DisplayStatus[],
): Promise<WorldWithProfile[]> {
): Promise<WorldWithInstance[]> {
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
}
export async function get_profile_worlds(path: string): Promise<World[]> {
return await invoke('plugin:worlds|get_profile_worlds', { path })
export async function get_instance_worlds(instanceId: string): Promise<World[]> {
return await invoke('plugin:worlds|get_instance_worlds', { instanceId })
}
export async function get_singleplayer_world(
@@ -137,16 +137,16 @@ export async function delete_world(instance: string, world: string): Promise<voi
return await invoke('plugin:worlds|delete_world', { instance, world })
}
export async function add_server_to_profile(
path: string,
export async function add_server_to_instance(
instanceId: string,
name: string,
address: string,
packStatus: ServerPackStatus,
projectId?: string,
contentKind?: string,
): Promise<number> {
return await invoke('plugin:worlds|add_server_to_profile', {
path,
return await invoke('plugin:worlds|add_server_to_instance', {
instanceId,
name,
address,
packStatus,
@@ -155,15 +155,15 @@ export async function add_server_to_profile(
})
}
export async function edit_server_in_profile(
path: string,
export async function edit_server_in_instance(
instanceId: string,
index: number,
name: string,
address: string,
packStatus: ServerPackStatus,
): Promise<void> {
return await invoke('plugin:worlds|edit_server_in_profile', {
path,
return await invoke('plugin:worlds|edit_server_in_instance', {
instanceId,
index,
name,
address,
@@ -171,12 +171,17 @@ export async function edit_server_in_profile(
})
}
export async function remove_server_from_profile(path: string, index: number): Promise<void> {
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
export async function remove_server_from_instance(
instanceId: string,
index: number,
): Promise<void> {
return await invoke('plugin:worlds|remove_server_from_instance', { instanceId, index })
}
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
export async function get_instance_protocol_version(
instanceId: string,
): Promise<ProtocolVersion | null> {
return await invoke('plugin:worlds|get_instance_protocol_version', { instanceId })
}
export async function get_server_status(
@@ -186,16 +191,19 @@ export async function get_server_status(
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
}
export async function start_join_singleplayer_world(path: string, world: string): Promise<unknown> {
return await invoke('plugin:worlds|start_join_singleplayer_world', { path, world })
export async function start_join_singleplayer_world(
instanceId: string,
world: string,
): Promise<unknown> {
return await invoke('plugin:worlds|start_join_singleplayer_world', { instanceId, world })
}
export async function start_join_server(path: string, address: string): Promise<unknown> {
return await invoke('plugin:worlds|start_join_server', { path, address })
export async function start_join_server(instanceId: string, address: string): Promise<unknown> {
return await invoke('plugin:worlds|start_join_server', { instanceId, address })
}
export async function showWorldInFolder(instancePath: string, worldPath: string) {
const fullPath = await get_full_path(instancePath)
export async function showWorldInFolder(instanceId: string, worldPath: string) {
const fullPath = await get_full_path(instanceId)
return await openPath(fullPath + '/saves/' + worldPath)
}
@@ -348,6 +356,28 @@ export function resolveManagedServerWorld(
)
}
export function getServerAddress(javaServer?: { address?: string | null } | null) {
if (!javaServer) return null
return javaServer.address ?? null
}
export async function ensureManagedServerWorldExists(
instanceId: string,
serverName: string,
serverAddress: string | null,
) {
if (!instanceId || !serverAddress) return
try {
const worlds = await get_instance_worlds(instanceId)
const managedWorld = resolveManagedServerWorld(worlds, serverName, serverAddress)
if (!managedWorld) {
await add_server_to_instance(instanceId, serverName, serverAddress, 'prompt')
}
} catch (err) {
console.error('Failed to ensure managed server world exists:', err)
}
}
export async function getServerLatency(
address: string,
protocolVersion: ProtocolVersion | null = null,
@@ -374,28 +404,35 @@ export async function refreshServerData(
): Promise<void> {
const refreshTime = Date.now()
serverData.refreshing = true
await get_server_status(address, protocolVersion)
.then((status) => {
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
// Don't update if there was a more recent successful refresh
return
}
serverData.lastSuccessfulRefresh = Date.now()
serverData.status = status
if (status.description) {
serverData.rawMotd = status.description
serverData.renderedMotd = autoToHTML(status.description)
}
})
.finally(() => {
serverData.refreshing = false
})
.catch((err) => {
console.error(`Refreshing addr ${address}`, protocolVersion, err)
if (!protocolVersion?.legacy) {
refreshServerData(serverData, { version: 74, legacy: true }, address)
}
})
try {
const status = await get_server_status(address, protocolVersion)
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
// Don't update if there was a more recent successful refresh
return
}
serverData.lastSuccessfulRefresh = Date.now()
serverData.status = status
if (status.description) {
serverData.rawMotd = status.description
serverData.renderedMotd = autoToHTML(status.description)
} else {
delete serverData.rawMotd
delete serverData.renderedMotd
}
} catch (err) {
console.error(`Refreshing addr ${address}`, protocolVersion, err)
if (!protocolVersion?.legacy) {
await refreshServerData(serverData, { version: 74, legacy: true }, address)
return
}
if (!serverData.lastSuccessfulRefresh || serverData.lastSuccessfulRefresh <= refreshTime) {
delete serverData.status
delete serverData.rawMotd
delete serverData.renderedMotd
}
} finally {
serverData.refreshing = false
}
}
export function refreshServers(
@@ -414,15 +451,15 @@ export function refreshServers(
}
})
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
// noinspection ES6MissingAwait - handled by refreshServerData
Object.keys(serverData).forEach((address) =>
refreshServerData(serverData[address], protocolVersion, address),
)
}
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
export async function refreshWorld(worlds: World[], instanceId: string, worldPath: string) {
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
const newWorld = await get_singleplayer_world(instancePath, worldPath)
const newWorld = await get_singleplayer_world(instanceId, worldPath)
if (index !== -1) {
worlds[index] = newWorld
} else {
@@ -432,13 +469,13 @@ export async function refreshWorld(worlds: World[], instancePath: string, worldP
sortWorlds(worlds)
}
export async function handleDefaultProfileUpdateEvent(
export async function handleDefaultInstanceUpdateEvent(
worlds: World[],
instancePath: string,
e: ProfileEvent,
instanceId: string,
e: InstanceEvent,
) {
if (e.event === 'world_updated') {
await refreshWorld(worlds, instancePath, e.world)
await refreshWorld(worlds, instanceId, e.world)
}
if (e.event === 'server_joined') {
@@ -456,9 +493,9 @@ export async function handleDefaultProfileUpdateEvent(
}
}
export async function refreshWorlds(instancePath: string): Promise<World[]> {
const worlds = await get_profile_worlds(instancePath).catch((err) => {
console.error(`Error refreshing worlds for instance: ${instancePath}`, err)
export async function refreshWorlds(instanceId: string): Promise<World[]> {
const worlds = await get_instance_worlds(instanceId).catch((err) => {
console.error(`Error refreshing worlds for instance: ${instanceId}`, err)
})
if (worlds) {
sortWorlds(worlds)
@@ -489,7 +526,7 @@ export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVer
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
}
export type ProfileEvent = { profile_path_id: string } & (
export type InstanceEvent = { instance_id: string } & (
| {
event: 'servers_updated'
}
+158 -38
View File
@@ -2,18 +2,78 @@
"app.action-bar.downloading-java": {
"message": "جاز تنزيل إصدار جافا {version}"
},
"app.action-bar.downloading-update": {
"message": "تحميل التحديث"
},
"app.action-bar.downloads": {
"message": "التنزيلات"
},
"app.action-bar.hide-more-running-instances": {
"message": "إخفاء المزيد من الحالات قيد التشغيل"
},
"app.action-bar.make-primary-instance": {
"message": "إنشاء نسخة أساسية"
},
"app.action-bar.no-instances-running": {
"message": "لا توجد نسخ قيد التشغيل"
},
"app.action-bar.offline": {
"message": "غير متصل"
},
"app.action-bar.primary-instance": {
"message": "النسخة ألرئيسية"
},
"app.action-bar.reload-to-update": {
"message": "أعد التشغيل للتحديث"
},
"app.action-bar.show-more-running-instances": {
"message": "عرض المزيد من النسخ قيد التشغيل"
},
"app.action-bar.stop-instance": {
"message": "إيقاف النسخة"
},
"app.action-bar.update": {
"message": "تحديث"
},
"app.action-bar.view-active-downloads": {
"message": "عرض التحميلات الفعالة"
},
"app.action-bar.view-instance": {
"message": "عرض النسخة"
},
"app.action-bar.view-logs": {
"message": "عرض التسجيل"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "يتيح العرض المتقدم لتأثيرات مثل التشويش الذي قد تسبب مشكلات في الأداء دون استخدام العرض المسرع بالأجهزة."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "العرض المتقدم"
},
"app.appearance-settings.color-theme.description": {
"message": "اختر لون سمتك المفضل من أجل تطبيق Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "لون السمة"
},
"app.appearance-settings.default-landing-page.description": {
"message": "غير الصفحة التي يفتح عليها المشغل."
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.appearance-settings.default-landing-page.title": {
"message": "الصفحة الرئيسة الافتراضية"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ألغ تفعيل عرض الاسم فوق لاعبك في صفحة المظاهر."
},
"app.appearance-settings.hide-nametag.title": {
"message": "إخفاء عرض الاسم"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
@@ -32,12 +92,18 @@
"app.browse.already-added": {
"message": "مضاف فعلا"
},
"app.browse.back-to-instance": {
"message": "العودة للنموذج"
},
"app.browse.discover-content": {
"message": "استكشف محتوى"
},
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
"app.browse.hide-added-servers": {
"message": "إخفاء الخوادم المضافة مسبقًا"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
},
@@ -75,13 +141,13 @@
"message": "حذف النموذج"
},
"app.instance.modpack-already-installed.body": {
"message": "حُزْمَة التعديل هذه مثبته فعلًا في نموذج <bold>{instanceName}</bold>. هل انت متأكد بإرادة نسخه؟"
"message": "حُزْمَة التعديل مثبته مسبقًا فعلًا في نموذج <bold>{instanceName}</bold>. هل أنت متأكد بإرادة نسخه؟"
},
"app.instance.modpack-already-installed.create": {
"message": "إنشاء"
},
"app.instance.modpack-already-installed.header": {
"message": "حُزْمَة التعديل مثبتة بالفعل"
"message": "حُزْمَة التعديل مثبتة فعلًا"
},
"app.instance.modpack-already-installed.instance": {
"message": "النموذج"
@@ -99,7 +165,7 @@
"message": "تحقق من المشاريع التي أستخدمها في حزمة التعديل الخاص بي!"
},
"app.instance.mods.share-title": {
"message": "مشاركة محتوى حزمة التعديل"
"message": "مشاركة محتوى حُزْمَة التعديل"
},
"app.instance.mods.successfully-uploaded": {
"message": "تم الرفع بنجاح"
@@ -110,12 +176,6 @@
"app.instance.worlds.browse-servers": {
"message": "تصفح الخوادم"
},
"app.instance.worlds.delete-world-description": {
"message": "سيتم حذف '{name}' **نهائيا**, و لن هناك أي طريقة لاسترداده."
},
"app.instance.worlds.delete-world-title": {
"message": "هل أنت متيقِّن من رغبتك بحذف هذا العالم نهائيا؟"
},
"app.instance.worlds.filter-modded": {
"message": "معدّل"
},
@@ -134,21 +194,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "لم تتم إضافة أي من الخوادم أو العوالم"
},
"app.instance.worlds.remove-server-description": {
"message": "ستتم إزالة '{name}' من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "ستتم إزالة '{name}' ({address}) من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
},
"app.instance.worlds.remove-server-title": {
"message": "هل أنت متيقِّن من رغبتك في إزالة {name}؟"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "ابحث بين ال{count} عوالم..."
},
"app.instance.worlds.this-server": {
"message": "هذا الخادم"
},
"app.modal.install-to-play.content-required": {
"message": "المحتوى مطلوب"
},
@@ -162,7 +210,7 @@
"message": "{count, plural, one {# تعديل} other {# تعديلات}}"
},
"app.modal.install-to-play.required-modpack": {
"message": زمة التعديل مطلوبة"
"message": ُزْمَة التعديل مطلوبة"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "يتطلب هذا الخادم تعديلات للعب. انقر فوق \"تثبيت\" لإعداد الملفات المطلوبة من Modrinth، ثم قم بتشغيله مباشرة إلى الخادم."
@@ -183,7 +231,13 @@
"message": "يلزم التحديث"
},
"app.modal.update-to-play.update-required-description": {
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى أحدث اصدار لتشغيل اللعبة."
},
"app.project.install-context.back-to-browse": {
"message": "العودة إلى الاستكشاف"
},
"app.project.install-context.install-content-to-instance": {
"message": "تثبيت المحتويات الى النموذج"
},
"app.settings.developer-mode-enabled": {
"message": "تم تفعيل وضع المطوّر."
@@ -209,8 +263,74 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.update-popup.body": {
"message": "تطبيق Modrinth v{version} جاهز للتثبيت! أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
"app.skins.add-button": {
"message": "إضافة مظهر"
},
"app.skins.add-button.drag-and-drop": {
"message": "أسحب ثم أفلت"
},
"app.skins.apply-button": {
"message": "تطبيق"
},
"app.skins.delete-button": {
"message": "حذف المظهر"
},
"app.skins.delete-modal.description": {
"message": "سيتم حذف المظهر بشكل نهائي، ولايمكن إرجاعه."
},
"app.skins.delete-modal.title": {
"message": "هل أنت متأكد من حذف المظهر هذا؟"
},
"app.skins.edit-button": {
"message": "تعديل المظهر"
},
"app.skins.modal.add-skin-button": {
"message": "إضافة المظهر"
},
"app.skins.modal.add-title": {
"message": "إضافة المظهر"
},
"app.skins.modal.cape-fallback-name": {
"message": "رداء"
},
"app.skins.modal.cape-section": {
"message": "رداء"
},
"app.skins.modal.edit-title": {
"message": "تعديل المظهر"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "عدل المظهر أولا للحفظ !"
},
"app.skins.modal.no-cape-tooltip": {
"message": "بدون رداء"
},
"app.skins.modal.none-cape-option": {
"message": "لا شيء"
},
"app.skins.modal.save-skin-button": {
"message": "حفظ المظهر"
},
"app.skins.modal.saving-tooltip": {
"message": "حفظ..."
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "أرفع المظهر أولا !"
},
"app.skins.preview.edit-button": {
"message": "تعديل المظهر"
},
"app.skins.previewing-badge": {
"message": "أستعراض"
},
"app.skins.rate-limit.title": {
"message": "تمهل !"
},
"app.skins.sign-in.title": {
"message": "الرجاء تسجيل الدخول"
},
"app.skins.title": {
"message": "اختيار المظهر"
},
"app.update-popup.body.download-complete": {
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
@@ -230,9 +350,6 @@
"app.update-popup.download-complete": {
"message": "اكتمل التنزيل"
},
"app.update-popup.reload": {
"message": "إعادة تحميل"
},
"app.update-popup.title": {
"message": "تحديث متاح"
},
@@ -242,17 +359,8 @@
"app.update.complete-toast.title": {
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.update.download-update": {
"message": "تنزيل التحديث"
},
"app.update.downloading-update": {
"message": "جار تنزيل التحديث ({percent}٪)"
},
"app.update.reload-to-update": {
"message": "أعد التحميل لتثبيت التحديث"
},
"app.world.server-modal.placeholder-address": {
"message": "مثال.مودرنث.جج"
"message": "مثال (example.modrinth.gg)"
},
"app.world.server-modal.select-an-option": {
"message": "حدد خيارا"
@@ -503,6 +611,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "الذاكرة المخصَّصة"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "النافذة"
},
@@ -578,9 +689,15 @@
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.label": {
"message": "حساب ماينكرافت"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
"minecraft-account.sign-in": {
"message": "سجل دخولك لـ ماينكرافت"
},
"search.filter.locked.instance": {
"message": "مقدَّم من النسخة"
},
@@ -604,5 +721,8 @@
},
"search.filter.locked.server-loader.title": {
"message": "يتم توفير المحمّل من قبل الخادم"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "لا تعرض هذا التحذير مجددا"
}
}
File diff suppressed because it is too large Load Diff
+280 -13
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Downloader Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Downloader opdatering"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primære instance"
},
"app.action-bar.reload-to-update": {
"message": "Genindlæs for at opdater"
},
"app.action-bar.show-more-running-instances": {
"message": "Vis flere kørende instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.update": {
"message": "Opdater"
},
"app.action-bar.view-active-downloads": {
"message": "Vis aktive downloads"
},
@@ -35,6 +44,9 @@
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avanceret rendering"
},
@@ -77,6 +89,18 @@
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Vis tid spillet"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Aktivere muligheden for at slå sidepanelet til og fra."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Slå sidepanel til og fra"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Hvis du prøver at installere en Modrinth Pack fil (.mrpack) som ikke er hosted på Modrinth, så skal vi nok sikre os du forstår riskoen før du installere den."
},
@@ -89,6 +113,15 @@
"app.auth-servers.unreachable.header": {
"message": "Kan ikke nå autentificeringsservere"
},
"app.browse.add-servers-to-instance": {
"message": "Tilføjet server til instance"
},
"app.browse.add-to-an-instance": {
"message": "Tilføj til en instance"
},
"app.browse.add-to-instance": {
"message": "Tilføj til instance"
},
"app.browse.add-to-instance-name": {
"message": "Tilføjet til {instanceName}"
},
@@ -98,17 +131,29 @@
"app.browse.already-added": {
"message": "Allerede tilføjet"
},
"app.browse.back-to-instance": {
"message": "Tilbage til instance"
},
"app.browse.discover-content": {
"message": "Opdag indhold"
},
"app.browse.discover-servers": {
"message": "Opdag servere"
},
"app.browse.hide-added-servers": {
"message": "Gem allerede tilføjet til servere"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
"app.browse.server-instance-content-warning": {
"message": "Ved at tilføje indhold kan der opstå kompatibilitetsproblemer, når du tilslutter dig serveren. Alt tilføjet indhold går desuden tabt, når du opdaterer indholdet på din server instance."
},
"app.browse.server.installing": {
"message": "Installere"
},
"app.content-install.no-compatible-versions": {
"message": "Ingen tilgængelige versioner passer til {compatibilityLabel}. Vælg en version for at installere. Afhængigheder bliver ikke installeret automatisk."
},
"app.export-modal.description-placeholder": {
"message": "Indtast modpack beskrivelse..."
@@ -149,6 +194,12 @@
"app.instance.modpack-already-installed.create": {
"message": "Opret"
},
"app.instance.modpack-already-installed.header": {
"message": "Modpack allerede installeret"
},
"app.instance.modpack-already-installed.instance": {
"message": "Instance"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
},
@@ -167,6 +218,33 @@
"app.instance.mods.successfully-uploaded": {
"message": "Uploadet"
},
"app.instance.worlds.add-server": {
"message": "Tilføj server"
},
"app.instance.worlds.browse-servers": {
"message": "Søg servere"
},
"app.instance.worlds.filter-modded": {
"message": "Moddet"
},
"app.instance.worlds.filter-offline": {
"message": "Offline"
},
"app.instance.worlds.filter-online": {
"message": "Online"
},
"app.instance.worlds.filter-vanilla": {
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Tilføj en server, eller søg efter en for at komme i gang"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Ingen servere eller verdener tilføjet"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Søg {count} verdener..."
},
"app.modal.install-to-play.content-required": {
"message": "Indhold krævet"
},
@@ -203,6 +281,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
},
"app.project.install-button.already-installed": {
"message": "Dette projekt er allerede installeret"
},
"app.project.install-context.back-to-browse": {
"message": "Tilbage til udforsk"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer indhold til instance"
},
"app.settings.developer-mode-enabled": {
"message": "Udvikler-tilstand aktiveret."
},
@@ -227,8 +314,137 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcestyring"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} er klar til at blive installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
"app.skins.add-button": {
"message": "Tilføj skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Træk og slip"
},
"app.skins.apply-button": {
"message": "Brug"
},
"app.skins.delete-button": {
"message": "Slet skin"
},
"app.skins.delete-modal.description": {
"message": "Dette vil permanent slette det valgte skin. Dette vil ikke kunne blive fortydes."
},
"app.skins.delete-modal.title": {
"message": "Er du sikker på, at du vil slette dette skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Fejl ved læsning af den afleverede fil."
},
"app.skins.dropped-file-error.title": {
"message": "Fejl under behandling af fil"
},
"app.skins.edit-button": {
"message": "Rediger skin"
},
"app.skins.modal.add-skin-button": {
"message": "Tilføj skin"
},
"app.skins.modal.add-title": {
"message": "Tilføjer et skin"
},
"app.skins.modal.arm-style-slim": {
"message": "Slank"
},
"app.skins.modal.arm-style-wide": {
"message": "Bred"
},
"app.skins.modal.cape-fallback-name": {
"message": "Kappe"
},
"app.skins.modal.cape-section": {
"message": "Kappe"
},
"app.skins.modal.edit-title": {
"message": "Rediger skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Rediger skinnet først!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Ingen kappe"
},
"app.skins.modal.none-cape-option": {
"message": "Ingen"
},
"app.skins.modal.save-skin-button": {
"message": "Gem skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Gemmer..."
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Upload et skin først!"
},
"app.skins.preview.edit-button": {
"message": "Rediger skin"
},
"app.skins.previewing-badge": {
"message": "Forhåndsvisning"
},
"app.skins.rate-limit.text": {
"message": "Du skifter dit skin for ofte. Mojang's servere har midlertidigt blokeret fremtidige anmodninger. Vent venligst et øjeblik for du prøver igen."
},
"app.skins.rate-limit.title": {
"message": "Ro på!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du modtog disse skins for at donere til en Modrinth Pride indsamling imens der er Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gemte skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Log Ind"
},
"app.skins.sign-in.description": {
"message": "Venligst log ind til din Minecraft konto for at kunne funktionerne til at administrere dit skin i Modrinth appen."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Overrasket Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Venligst log ind"
},
"app.skins.title": {
"message": "Skin vælger"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
@@ -248,9 +464,6 @@
"app.update-popup.download-complete": {
"message": "Download færdiggjort"
},
"app.update-popup.reload": {
"message": "Geninlæs"
},
"app.update-popup.title": {
"message": "Opdatering tilgængelig"
},
@@ -260,14 +473,23 @@
"app.update.complete-toast.title": {
"message": "Version {version} var installeret med succes!"
},
"app.update.download-update": {
"message": "Download opdatering"
"app.world.server-modal.placeholder-address": {
"message": "eksemple.modrinth.gg"
},
"app.update.downloading-update": {
"message": "Downloader opdatering ({percent}%)"
"app.world.server-modal.select-an-option": {
"message": "Vælg en mulighed"
},
"app.update.reload-to-update": {
"message": "Genindlæs for at installere opdatering"
"app.world.world-item.incompatible-version": {
"message": "Ikke kompatibel version {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ikke spillet endnu"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
@@ -368,6 +590,12 @@
"instance.edit-world.title": {
"message": "Rediger verden"
},
"instance.files.adding-files": {
"message": "Tilføjer filer ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Gem som..."
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -482,6 +710,9 @@
"instance.settings.tabs.java": {
"message": "Java og hukommelse"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Indskriv Java argumenter..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Omgivelses variabler"
},
@@ -497,6 +728,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Allokeret hukommelse"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/vej/til/java"
},
"instance.settings.tabs.window": {
"message": "Vindue"
},
@@ -569,6 +803,24 @@
"instance.worlds.world_in_use": {
"message": "Verden er i brug"
},
"minecraft-account.add-account": {
"message": "Tilføj konto"
},
"minecraft-account.label": {
"message": "Minecraft konto"
},
"minecraft-account.not-signed-in": {
"message": "Ikke logged ind"
},
"minecraft-account.remove-account": {
"message": "Fjern konto"
},
"minecraft-account.select-account": {
"message": "Vælg konto"
},
"minecraft-account.sign-in": {
"message": "Log ind på Minecraft"
},
"search.filter.locked.instance": {
"message": "Givet af instanceen"
},
@@ -592,5 +844,20 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader er givet af serveren"
},
"unknown-pack-warning-modal.body": {
"message": "En fil behandles kun, hvis den uploades til Modrinth, uanset filformat (herunder .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Vis ikke denne advarsel igen"
},
"unknown-pack-warning-modal.header": {
"message": "Bekræft installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installer alligevel"
},
"unknown-pack-warning-modal.warning.title": {
"message": "Ukendt fil advarsel"
}
}
+319 -31
View File
@@ -2,12 +2,30 @@
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update wird heruntergeladen"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Weitere laufende Instanzen ausblenden"
},
"app.action-bar.install.dismiss": {
"message": "Ignorieren"
},
"app.action-bar.install.open-instance": {
"message": "Instanz öffnen"
},
"app.action-bar.install.retry": {
"message": "Wiederholen"
},
"app.action-bar.install.unknown-instance": {
"message": "Unbekannte Instanz"
},
"app.action-bar.installs": {
"message": "Installationen"
},
"app.action-bar.make-primary-instance": {
"message": "Zur primären Instanz machen"
},
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Primäre Instanz"
},
"app.action-bar.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"app.action-bar.view-active-downloads": {
"message": "Aktive Downloads anzeigen"
},
@@ -86,6 +110,18 @@
"app.appearance-settings.select-option": {
"message": "Option wählen"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
},
"app.appearance-settings.show-play-time.title": {
"message": "Spielzeit anzeigen"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Überspringt automatisch Bestätigungen mit geringem Risiko, wie z. B. die doppelte Installation von Modpacks, das Löschen normaler Inhalte, Massenaktualisierungen, das Trennen von Modpacks und Reparaturen. Warnungen bei gefährlichen Aktionen werden weiterhin angezeigt."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Nicht essenzielle Warnungen überspringen"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Ermöglicht das Umschalten der Seitenleiste."
},
@@ -143,11 +179,8 @@
"app.browse.server.installing": {
"message": "Wird installiert"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modpack wird installiert..."
"app.content-install.no-compatible-versions": {
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
},
"app.export-modal.description-placeholder": {
"message": "Modpaketbeschreibung eingeben..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschliessen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpaketname"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Lade Inhalte herunter"
},
"app.install.phase.downloading_minecraft": {
"message": "Lade Minecraft herunter"
},
"app.install.phase.downloading_pack_file": {
"message": "Lade Packdatei herunter"
},
"app.install.phase.extracting_overrides": {
"message": "Überschreibungen extrahieren"
},
"app.install.phase.finalizing": {
"message": "Abschliessen"
},
"app.install.phase.preparing_instance": {
"message": "Instanz präparieren"
},
"app.install.phase.preparing_java": {
"message": "Java präparieren"
},
"app.install.phase.preparing_java.downloading": {
"message": "Java {version} herunterladen"
},
"app.install.phase.preparing_java.extracting": {
"message": "Java {version} extrahieren"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Java {version} wird heruntergeladen"
},
"app.install.phase.preparing_java.resolving": {
"message": "Java {version} präparieren"
},
"app.install.phase.preparing_java.validating": {
"message": "Java {version} validieren"
},
"app.install.phase.reading_pack_manifest": {
"message": "Paketmanifest wird gelesen"
},
"app.install.phase.resolving_loader": {
"message": "Loader wird ermittelt"
},
"app.install.phase.resolving_minecraft": {
"message": "Minecraft wird ermittelt"
},
"app.install.phase.resolving_pack": {
"message": "Inhalte werden ermittelt"
},
"app.install.phase.rolling_back": {
"message": "Wird rückgängig gemacht"
},
"app.install.phase.running_loader_processors": {
"message": "Loader-Prozesse werden ausgeführt"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, inlusive deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -200,6 +281,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instanz"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "Lade {current, number}/{total, number} Projekte..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Update abschliessen..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Versionen werden ermittelt..."
},
"app.instance.mods.content-type-project": {
"message": "Projekt"
},
@@ -224,8 +314,17 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Welt löschen"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Welt löschen"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Diese Welt wird permanent gelöscht von dieser Instanz, dies kann nicht rückgängig gemacht werden."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Lösche {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Modifiziert"
@@ -245,15 +344,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Server entfernen"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Server entfernen"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Dieser Server wird aus deiner Serverliste und aus der Serverliste im Spiel entfernt. Du kannst ihn später wieder hinzufügen, wenn du die Adresse kennst."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Entferne {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Durchsuche {count} Welten..."
},
"app.instance.worlds.this-server": {
"message": "dieser Server"
},
"app.modal.install-to-play.content-required": {
"message": "Inhalte benötigt"
},
@@ -294,7 +399,7 @@
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
"message": "Zurück zu Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
@@ -323,8 +428,152 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
"app.skins.add-button": {
"message": "Skin hinzufügen"
},
"app.skins.add-button.drag-and-drop": {
"message": "Drag-and-Drop"
},
"app.skins.apply-button": {
"message": "Anwenden"
},
"app.skins.delete-button": {
"message": "Skin löschen"
},
"app.skins.delete-modal.description": {
"message": "Dies wird den ausgewählten Skin unwiderruflich löschen. Diese Aktion kann nicht rückgängig gemacht werden."
},
"app.skins.delete-modal.title": {
"message": "Bist du dir sicher, dass du diesen Skin löschen willst?"
},
"app.skins.dropped-file-error.text": {
"message": "Fehler beim Lesen der abgelegten Datei."
},
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Dateiverarbeitung"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.modal.add-skin-button": {
"message": "Skin hinzufügen"
},
"app.skins.modal.add-title": {
"message": "Skin wird hinzugefügt"
},
"app.skins.modal.arm-style-section": {
"message": "Arm Stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Schlank"
},
"app.skins.modal.arm-style-wide": {
"message": "Breit"
},
"app.skins.modal.cape-fallback-name": {
"message": "Umhang"
},
"app.skins.modal.cape-section": {
"message": "Umhang"
},
"app.skins.modal.edit-title": {
"message": "Skin bearbeiten"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nimm zuerst eine Änderung am Skin vor!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Kein Umhang"
},
"app.skins.modal.none-cape-option": {
"message": "Keines"
},
"app.skins.modal.replace-texture-button": {
"message": "Textur ersetzen"
},
"app.skins.modal.save-skin-button": {
"message": "Skin speichern"
},
"app.skins.modal.saving-tooltip": {
"message": "Speichern..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lade zuerst einen Skin hoch!"
},
"app.skins.preview.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.previewing-badge": {
"message": "Vorschau"
},
"app.skins.rate-limit.text": {
"message": "Du änderst deinen Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
},
"app.skins.rate-limit.title": {
"message": "Nicht so schnell!"
},
"app.skins.reorder-error.text": {
"message": "Deine Skin-Reihenfolge konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard-Skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du hast diese Skins als Dankeschön für eine Spende bei der Modrinth Pride Spendenaktion während des Pride Month erhalten."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gespeicherte Skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Anmelden"
},
"app.skins.sign-in.description": {
"message": "Bitte melde dich in deinem Minecraft-Konto an, um die Skin-Verwaltungsfunktionen der Modrinth App zu nutzen."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Begeisterter Modrinth-Bot"
},
"app.skins.sign-in.title": {
"message": "Bitte anmelden"
},
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
@@ -345,7 +594,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Neu Laden"
"message": "Zum Aktualisieren neu laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -356,15 +605,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Aktualisierung herunterladen"
},
"app.update.downloading-update": {
"message": "Lade Aktualisierung herunter ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Lade neu um Aktualisierung zu installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -554,6 +794,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Update-Kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release-, Beta- und Alpha-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release- und Beta-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Release-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Update-Kanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Start Hooks"
},
@@ -602,6 +866,24 @@
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Benutzerdefinierte Umgebungsvariablen"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Benutzerdefinierte Java-Argumente"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Benutzerdefinierte Java-Installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Benutzerdefinierte Speicherzuweisung"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Umgebungsvariablen eingeben..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java-Argumente eingeben..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Umgebungsvariablen"
},
@@ -617,6 +899,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.window": {
"message": "Spielfenster"
},
@@ -656,6 +941,9 @@
"instance.worlds.copy_address": {
"message": "Adresse kopieren"
},
"instance.worlds.create_shortcut": {
"message": "Shortcut erstellen"
},
"instance.worlds.dont_show_on_home": {
"message": "Nicht auf Startseite anzeigen"
},
+321 -33
View File
@@ -2,17 +2,35 @@
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update wird heruntergeladen"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Weitere laufende Instanzen ausblenden"
},
"app.action-bar.install.dismiss": {
"message": "Ausblenden"
},
"app.action-bar.install.open-instance": {
"message": "Instanz öffnen"
},
"app.action-bar.install.retry": {
"message": "Erneut versuchen"
},
"app.action-bar.install.unknown-instance": {
"message": "Unbekannte Instanz"
},
"app.action-bar.installs": {
"message": "Installationen"
},
"app.action-bar.make-primary-instance": {
"message": "Zur primären Instanz machen"
},
"app.action-bar.no-instances-running": {
"message": "Keine aktiven Instanzen"
"message": "Keine laufenden Instanzen"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Primäre Instanz"
},
"app.action-bar.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"app.action-bar.view-active-downloads": {
"message": "Aktive Downloads anzeigen"
},
@@ -33,7 +57,7 @@
"message": "Instanz anzeigen"
},
"app.action-bar.view-logs": {
"message": "Protokolle anzeigen"
"message": "Logs anzeigen"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfe-Effekte, welche ohne Hardware-beschleunigtes Rendering zu Leistungsproblemen führen können."
@@ -48,7 +72,7 @@
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Kategorie, die beim Öffnen des Launchers angezeigt wird."
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
@@ -86,6 +110,18 @@
"app.appearance-settings.select-option": {
"message": "Wähle eine Option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
},
"app.appearance-settings.show-play-time.title": {
"message": "Spielzeit anzeigen"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Überspringt automatisch Bestätigungen mit geringem Risiko, wie z. B. die doppelte Installation von Modpacks, das Löschen normaler Inhalte, Massenaktualisierungen, das Trennen von Modpacks und Reparaturen. Warnungen bei gefährlichen Aktionen werden weiterhin angezeigt."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Unwichtige Warnungen überspringen"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Ermöglicht das Umschalten der Seitenleiste."
},
@@ -143,11 +179,8 @@
"app.browse.server.installing": {
"message": "Wird installiert"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modpack wird installiert..."
"app.content-install.no-compatible-versions": {
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
},
"app.export-modal.description-placeholder": {
"message": "Beschreibung des Modpacks eingeben..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschließen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpackname"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpackname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Inhalte werden heruntergeladen"
},
"app.install.phase.downloading_minecraft": {
"message": "Minecraft wird heruntergeladen"
},
"app.install.phase.downloading_pack_file": {
"message": "Paketdatei wird herunterladen"
},
"app.install.phase.extracting_overrides": {
"message": "Überschreibungen werden extrahiert"
},
"app.install.phase.finalizing": {
"message": "Wird finalisiert"
},
"app.install.phase.preparing_instance": {
"message": "Instanz wird vorbereitet"
},
"app.install.phase.preparing_java": {
"message": "Java wird vorbereitet"
},
"app.install.phase.preparing_java.downloading": {
"message": "Java {version} wird heruntergeladen"
},
"app.install.phase.preparing_java.extracting": {
"message": "Java {version} wird extrahiert"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Java {version} wird heruntergeladen"
},
"app.install.phase.preparing_java.resolving": {
"message": "Java {version} wird vorbereitet"
},
"app.install.phase.preparing_java.validating": {
"message": "Java {version} wird validiert"
},
"app.install.phase.reading_pack_manifest": {
"message": "Paketmanifest wird gelesen"
},
"app.install.phase.resolving_loader": {
"message": "Loader wird ermittelt"
},
"app.install.phase.resolving_minecraft": {
"message": "Minecraft wird ermittelt"
},
"app.install.phase.resolving_pack": {
"message": "Inhalte werden ermittelt"
},
"app.install.phase.rolling_back": {
"message": "Wird rückgängig gemacht"
},
"app.install.phase.running_loader_processors": {
"message": "Loader-Prozesse werden ausgeführt"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -200,6 +281,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instanz"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "{current, number}/{total, number} Projekte werden heruntergelanden..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Update wird fertiggestellt..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Versionen werden ermittelt..."
},
"app.instance.mods.content-type-project": {
"message": "Projekt"
},
@@ -224,8 +314,17 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Welt löschen"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Welt löschen"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Die Welt wird permanent von der Instanz gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "{name} wird gelöscht"
},
"app.instance.worlds.filter-modded": {
"message": "Modifiziert"
@@ -245,15 +344,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Server entfernen"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Server entfernen"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Dieser Server wird aus deiner Serverliste und aus der Serverliste im Spiel entfernt. Du kannst ihn später wieder hinzufügen, wenn du die Adresse kennst."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "{name} wird entfernt"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Durchsuche {count} Welten..."
},
"app.instance.worlds.this-server": {
"message": "dieser Server"
},
"app.modal.install-to-play.content-required": {
"message": "Inhalte benötigt"
},
@@ -323,8 +428,152 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
"app.skins.add-button": {
"message": "Skin hinzufügen"
},
"app.skins.add-button.drag-and-drop": {
"message": "Ziehen und Ablegen"
},
"app.skins.apply-button": {
"message": "Anwenden"
},
"app.skins.delete-button": {
"message": "Skin löschen"
},
"app.skins.delete-modal.description": {
"message": "Dadurch wird der ausgewählte Skin permanent gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden."
},
"app.skins.delete-modal.title": {
"message": "Möchtest du diesen Skin wirklich löschen?"
},
"app.skins.dropped-file-error.text": {
"message": "Die abgelegte Datei konnte nicht gelesen werden."
},
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Verarbeitung der Datei"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.modal.add-skin-button": {
"message": "Skin hinzufügen"
},
"app.skins.modal.add-title": {
"message": "Einen Skin hinzufügen"
},
"app.skins.modal.arm-style-section": {
"message": "Arm-Stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Schlank"
},
"app.skins.modal.arm-style-wide": {
"message": "Breit"
},
"app.skins.modal.cape-fallback-name": {
"message": "Umhang"
},
"app.skins.modal.cape-section": {
"message": "Umhang"
},
"app.skins.modal.edit-title": {
"message": "Skin bearbeiten"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nimm zuerst eine Änderung am Skin vor!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Kein Umhang"
},
"app.skins.modal.none-cape-option": {
"message": "Keinen"
},
"app.skins.modal.replace-texture-button": {
"message": "Textur ersetzen"
},
"app.skins.modal.save-skin-button": {
"message": "Skin speichern"
},
"app.skins.modal.saving-tooltip": {
"message": "Wird gespeichert..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lade zuerst ein Skin hoch!"
},
"app.skins.preview.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.previewing-badge": {
"message": "Vorschau"
},
"app.skins.rate-limit.text": {
"message": "Du wechselst deinen Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
},
"app.skins.rate-limit.title": {
"message": "Langsamer!"
},
"app.skins.reorder-error.text": {
"message": "Die Anordnung deiner Skins konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard-Skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du hast diese Skins erhalten, da du während des Pride-Monats für eine Modrinth-Pride-Spendenaktion gespendet hast."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gespeicherte Skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Anmelden"
},
"app.skins.sign-in.description": {
"message": "Bitte melde dich bei deinem Minecraft-Konto an, um die Funktionen zur Skin-Verwaltung der Modrinth-App nutzen zu können."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Begeisterter Modrinth-Bot"
},
"app.skins.sign-in.title": {
"message": "Bitte melde dich an"
},
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch aktualisieren, wenn du die Modrinth App schließt."
@@ -345,7 +594,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Neu laden"
"message": "Zum Aktualisieren neu laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -356,15 +605,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Update herunterladen"
},
"app.update.downloading-update": {
"message": "Update wird heruntergeladen ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Neu laden, um das Update zu installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -554,6 +794,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Aktualisierungskanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Voll, Beta- und Alpha-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Voll- und Beta-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Vollversion"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Vollversionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Aktualisierungskanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Startargumente"
},
@@ -602,6 +866,24 @@
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Benutzerdefinierte Umgebungsvariablen"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Benutzerdefinierte Java-Argumente"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Benutzerdefinierte Java-Installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Benutzerdefinierte Speicherzuweisung"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Umgebungsvariablen eingeben..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java-Argumente eingeben..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Umgebungsvariablen"
},
@@ -617,6 +899,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.window": {
"message": "Fenster"
},
@@ -656,6 +941,9 @@
"instance.worlds.copy_address": {
"message": "Adresse kopieren"
},
"instance.worlds.create_shortcut": {
"message": "Verknüpfung erstellen"
},
"instance.worlds.dont_show_on_home": {
"message": "Nicht auf der Startseite anzeigen"
},
+249 -39
View File
@@ -2,12 +2,96 @@
"app.action-bar.downloading-java": {
"message": "Downloading Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Downloading update"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Hide more running instances"
},
"app.action-bar.install.copied-details": {
"message": "Copied"
},
"app.action-bar.install.copy-details": {
"message": "Copy details"
},
"app.action-bar.install.dismiss": {
"message": "Dismiss"
},
"app.action-bar.install.open-instance": {
"message": "Open instance"
},
"app.action-bar.install.retry": {
"message": "Retry"
},
"app.action-bar.install.summary.app-closing": {
"message": "Canceled due to app closing"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Couldn't read modpack"
},
"app.action-bar.install.summary.canceled": {
"message": "Canceled"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Cleanup didn't finish"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Couldn't download files"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "Downloaded file is corrupt"
},
"app.action-bar.install.summary.could-not-save-files": {
"message": "Couldn't save files"
},
"app.action-bar.install.summary.download-failed": {
"message": "Download couldn't finish"
},
"app.action-bar.install.summary.instance-not-found": {
"message": "Instance couldn't be found"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "File path is invalid"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "Modpack data invalid"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "Modpack files have invalid metadata"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "Java setup couldn't finish"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "Loader setup failed"
},
"app.action-bar.install.summary.local-data-error": {
"message": "Couldn't update local data"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "Minecraft setup failed"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Couldn't reach Modrinth"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "No permission to write"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "Couldn't download pack"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Something went wrong"
},
"app.action-bar.install.unknown-instance": {
"message": "Unknown instance"
},
"app.action-bar.installs": {
"message": "Installs"
},
"app.action-bar.make-primary-instance": {
"message": "Make primary instance"
},
@@ -20,12 +104,18 @@
"app.action-bar.primary-instance": {
"message": "Primary instance"
},
"app.action-bar.reload-to-update": {
"message": "Reload to update"
},
"app.action-bar.show-more-running-instances": {
"message": "Show more running instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.update": {
"message": "Update"
},
"app.action-bar.view-active-downloads": {
"message": "View active downloads"
},
@@ -86,6 +176,18 @@
"app.appearance-settings.select-option": {
"message": "Select an option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Displays how much time you've spent playing an instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Show play time"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Skip non-essential warnings"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Enables the ability to toggle the sidebar."
},
@@ -132,7 +234,7 @@
"message": "Discover servers"
},
"app.browse.hide-added-servers": {
"message": "Hide already added servers"
"message": "Hide servers already added"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
@@ -143,11 +245,8 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installing modpack..."
"app.content-install.no-compatible-versions": {
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
},
"app.export-modal.description-placeholder": {
"message": "Enter modpack description..."
@@ -158,24 +257,72 @@
"app.export-modal.header": {
"message": "Export modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Include \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpack Name"
"message": "Modpack name"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpack name"
},
"app.export-modal.select-files-label": {
"message": "Configure which files are included in this export"
},
"app.export-modal.version-number-label": {
"message": "Version number"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Downloading content"
},
"app.install.phase.downloading_minecraft": {
"message": "Downloading Minecraft"
},
"app.install.phase.downloading_pack_file": {
"message": "Downloading pack file"
},
"app.install.phase.extracting_overrides": {
"message": "Extracting overrides"
},
"app.install.phase.finalizing": {
"message": "Finalizing"
},
"app.install.phase.preparing_instance": {
"message": "Queued to install"
},
"app.install.phase.preparing_java": {
"message": "Preparing Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Downloading Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Extracting Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Fetching Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Preparing Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Validating Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Reading pack manifest"
},
"app.install.phase.resolving_loader": {
"message": "Resolving loader"
},
"app.install.phase.resolving_minecraft": {
"message": "Resolving Minecraft"
},
"app.install.phase.resolving_pack": {
"message": "Resolving content"
},
"app.install.phase.rolling_back": {
"message": "Rolling back"
},
"app.install.phase.running_loader_processors": {
"message": "Running loader processors"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data for your instance will be permanently deleted, including your worlds, configs, and all installed content."
},
@@ -200,6 +347,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instance"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "Downloading {current, number}/{total, number} projects..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Finishing update..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Resolving versions..."
},
"app.instance.mods.content-type-project": {
"message": "project"
},
@@ -224,11 +380,17 @@
"app.instance.worlds.browse-servers": {
"message": "Browse servers"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' will be **permanently deleted**, and there will be no way to recover it."
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Delete world"
},
"app.instance.worlds.delete-world-title": {
"message": "Are you sure you want to permanently delete this world?"
"app.instance.worlds.delete-world-modal.title": {
"message": "Delete world"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "This world will be permanently deleted from this instance. This action cannot be undone."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Deleting {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Modded"
@@ -248,21 +410,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No servers or worlds added"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' will be removed from your list, including in-game, and there will be no way to recover it."
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Remove server"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) will be removed from your list, including in-game, and there will be no way to recover it."
"app.instance.worlds.remove-server-modal.title": {
"message": "Remove server"
},
"app.instance.worlds.remove-server-title": {
"message": "Are you sure you want to remove {name}?"
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Removing {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Search {count} worlds..."
},
"app.instance.worlds.this-server": {
"message": "this server"
},
"app.modal.install-to-play.content-required": {
"message": "Content required"
},
@@ -302,12 +464,27 @@
"app.project.install-button.already-installed": {
"message": "This project is already installed"
},
"app.project.install-button.switch-version": {
"message": "Switch version"
},
"app.project.install-context.back-to-browse": {
"message": "Back to discover"
},
"app.project.install-context.install-content-to-instance": {
"message": "Install content to instance"
},
"app.project.version.all-versions": {
"message": "All versions"
},
"app.project.version.download-in-browser": {
"message": "Download in browser"
},
"app.project.version.installing": {
"message": "Installing"
},
"app.project.versions.already-installed": {
"message": "Already installed"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
@@ -419,9 +596,18 @@
"app.skins.rate-limit.title": {
"message": "Slow down!"
},
"app.skins.reorder-error.text": {
"message": "Your skin order could not be saved."
},
"app.skins.reorder-error.title": {
"message": "Failed to reorder skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chaos-cubed": {
"message": "Chaos Cubed"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
@@ -431,6 +617,15 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "You received these skins for donating to a Modrinth Pride fundraiser during Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
@@ -464,9 +659,6 @@
"app.skins.title": {
"message": "Skin selector"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App."
},
@@ -486,7 +678,7 @@
"message": "Download complete"
},
"app.update-popup.reload": {
"message": "Reload"
"message": "Reload to update"
},
"app.update-popup.title": {
"message": "Update available"
@@ -497,15 +689,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} was successfully installed!"
},
"app.update.download-update": {
"message": "Download update"
},
"app.update.downloading-update": {
"message": "Downloading update ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Reload to install update"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -695,6 +878,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Update channel"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release, beta, and alpha versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release and beta versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Only release versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Select update channel"
},
"instance.settings.tabs.hooks": {
"message": "Launch hooks"
},
@@ -818,6 +1025,9 @@
"instance.worlds.copy_address": {
"message": "Copy address"
},
"instance.worlds.create_shortcut": {
"message": "Create shortcut"
},
"instance.worlds.dont_show_on_home": {
"message": "Don't show on Home"
},
+300 -33
View File
@@ -2,12 +2,30 @@
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"app.action-bar.downloads": {
"message": "Descargas"
},
"app.action-bar.hide-more-running-instances": {
"message": "Ocultar más instancias en ejecución"
},
"app.action-bar.install.dismiss": {
"message": "Descartar"
},
"app.action-bar.install.open-instance": {
"message": "Abrir instancia"
},
"app.action-bar.install.retry": {
"message": "Reintentar"
},
"app.action-bar.install.unknown-instance": {
"message": "Instancia desconocida"
},
"app.action-bar.installs": {
"message": "Instalaciones"
},
"app.action-bar.make-primary-instance": {
"message": "Hacer instancia principal"
},
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.reload-to-update": {
"message": "Reinicia para actualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Detener instancia"
},
"app.action-bar.update": {
"message": "Actualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Ver descargas en curso"
},
@@ -86,6 +110,18 @@
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostrar tiempo de juego"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Omite automáticamente las confirmaciones de bajo riesgo, como las instalaciones duplicadas de paquetes de mods, la eliminación de contenido normal, las actualizaciones masivas, la desvinculación de paquetes de mods y las solicitudes de reparación. Las advertencias peligrosas seguirán mostrándose."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Omitir advertencias que no sean esenciales"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permite mostrar u ocultar la barra lateral."
},
@@ -143,11 +179,8 @@
"app.browse.server.installing": {
"message": "Instalando"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando modpack..."
"app.content-install.no-compatible-versions": {
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
},
"app.export-modal.description-placeholder": {
"message": "Introduce la descripción del modpack..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Exportar modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "¿Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nombre del modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos incluir en esta exportación"
},
"app.export-modal.version-number-label": {
"message": "Número de la versión"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Descargando contenido"
},
"app.install.phase.downloading_minecraft": {
"message": "Descargando Minecraft"
},
"app.install.phase.downloading_pack_file": {
"message": "Descargando el archivo del paquete"
},
"app.install.phase.extracting_overrides": {
"message": "Extracción de anulaciones"
},
"app.install.phase.finalizing": {
"message": "Finalizando"
},
"app.install.phase.preparing_instance": {
"message": "Preparando la instancia"
},
"app.install.phase.preparing_java": {
"message": "Preparando Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Descargando Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Descomprimiendo Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Obteniendo Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Preparando Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Validación de Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Leer el manifiesto del paquete"
},
"app.install.phase.resolving_loader": {
"message": "Resolviendo el cargador"
},
"app.install.phase.resolving_minecraft": {
"message": "Resolviendo Minecraft"
},
"app.install.phase.resolving_pack": {
"message": "Resolviendo el contenido"
},
"app.install.phase.rolling_back": {
"message": "Revertir"
},
"app.install.phase.running_loader_processors": {
"message": "Ejecutando los procesadores del cargador"
},
"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."
},
@@ -200,6 +281,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instancia"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "Descargando {current, number}/{total, number} proyectos..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Finalizando la actualización..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Resolviendo versiones..."
},
"app.instance.mods.content-type-project": {
"message": "proyecto"
},
@@ -224,8 +314,17 @@
"app.instance.worlds.browse-servers": {
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-title": {
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Eliminar mundo"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Eliminar mundo"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Este mundo se eliminará de forma permanente de esta instancia. Esta acción no se puede deshacer."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Eliminando {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Con mods"
@@ -245,15 +344,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores ni mundos añadidos"
},
"app.instance.worlds.remove-server-title": {
"message": "¿Estás seguro de que quieres eliminar {name}?"
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Eliminar servidor"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Eliminar servidor"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Este servidor se eliminará de tu lista de servidores y de la lista de servidores del juego. Puedes volver a agregarlo más adelante si conoces la dirección."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Se está eliminando {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Buscar en {count} mundos..."
},
"app.instance.worlds.this-server": {
"message": "este servidor"
},
"app.modal.install-to-play.content-required": {
"message": "Contenido requerido"
},
@@ -291,7 +396,7 @@
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
},
"app.project.install-button.already-installed": {
"message": "Este proyecto está actualmente instalado"
"message": "Este proyecto ya está instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver al explorador"
@@ -323,8 +428,152 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"app.update-popup.body": {
"message": "¡Modrinth App v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la Modrinth App."
"app.skins.add-button": {
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastra y suelta"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.skins.delete-button": {
"message": "Eliminar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto eliminará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro de que quieres eliminar esta skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Error al leer el archivo arrastrado."
},
"app.skins.dropped-file-error.title": {
"message": "Error al procesar el archivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
},
"app.skins.modal.add-skin-button": {
"message": "Añadir skin"
},
"app.skins.modal.add-title": {
"message": "Añadiendo una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo de brazo"
},
"app.skins.modal.arm-style-slim": {
"message": "Delgado"
},
"app.skins.modal.arm-style-wide": {
"message": "Ancho"
},
"app.skins.modal.cape-fallback-name": {
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "¡Primero edita el skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sin capa"
},
"app.skins.modal.none-cape-option": {
"message": "Ninguna"
},
"app.skins.modal.replace-texture-button": {
"message": "Remplazar textura"
},
"app.skins.modal.save-skin-button": {
"message": "Guardar skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Guardando..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "¡Primero carga una skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Previsualizando"
},
"app.skins.rate-limit.text": {
"message": "Estás cambiando tu skin muy frecuentemente. Los servidores de Mojang han bloqueado temporalmente las solicitdues futuras. Por favor, espera un momento antes de volver a intentarlo."
},
"app.skins.rate-limit.title": {
"message": "¡Más despacio!"
},
"app.skins.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Constructores y Biomas"
},
"app.skins.section.chase-the-skies": {
"message": "Persigue los Cielos"
},
"app.skins.section.default-skins": {
"message": "Skins predeterminadas"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgullo Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Monturas del Caos"
},
"app.skins.section.saved-skins": {
"message": "Skins guardadas"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "La Era del Cobre"
},
"app.skins.section.the-garden-awakens": {
"message": "El Jardín Despierta"
},
"app.skins.section.tiny-takeover": {
"message": "Bebés al Poder"
},
"app.skins.sign-in.button": {
"message": "Iniciar sesión"
},
"app.skins.sign-in.description": {
"message": "Por favor, inicia sesión en tu cuenta de Minecraft para poder utilizar las funciones de gestión de skins de la Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emocionado"
},
"app.skins.sign-in.title": {
"message": "Por favor, inicia sesión"
},
"app.skins.title": {
"message": "Selector de skin"
},
"app.update-popup.body.download-complete": {
"message": "La descarga de la Modrinth App v{version} ha finalizado. Actualiza ahora o automáticamente al cerrar la Modrinth App."
@@ -345,7 +594,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Recargar"
"message": "Reinicia para actualizar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -356,15 +605,6 @@
"app.update.complete-toast.title": {
"message": "¡La versión {version} se ha instalado correctamente!"
},
"app.update.download-update": {
"message": "Descargar actualización"
},
"app.update.downloading-update": {
"message": "Descargando actualización ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"app.world.server-modal.placeholder-address": {
"message": "ejemplo.modrinth.gg"
},
@@ -554,6 +794,30 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Las versiones estables, beta y alpha se mostrarán como actualizaciones disponibles. "
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Las versiones estables y beta serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Sólo las versiones estables serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Selecciona el canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de inicio"
},
@@ -636,7 +900,7 @@
"message": "Memoria asignada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/a/java"
"message": "/ruta/de/java"
},
"instance.settings.tabs.window": {
"message": "Ventana"
@@ -677,6 +941,9 @@
"instance.worlds.copy_address": {
"message": "Copiar dirección"
},
"instance.worlds.create_shortcut": {
"message": "Crear un acceso directo"
},
"instance.worlds.dont_show_on_home": {
"message": "No mostrar en la página de inicio"
},
@@ -744,7 +1011,7 @@
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se puede añadir mods que sean del lado del cliente a la instancia del servidor"
"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"
+210 -60
View File
@@ -2,14 +2,20 @@
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"app.action-bar.downloads": {
"message": "Descargas"
},
"app.action-bar.hide-more-running-instances": {
"message": "Ocultar más instancias en ejecución"
},
"app.action-bar.install.dismiss": {
"message": "Descartar"
},
"app.action-bar.make-primary-instance": {
"message": "Hacer instancia principal"
"message": "Establecer como instancia principal"
},
"app.action-bar.no-instances-running": {
"message": "Ninguna instancia en ejecución"
@@ -20,20 +26,26 @@
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.reload-to-update": {
"message": "Recargar para actualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
"message": "Detener instancia"
},
"app.action-bar.update": {
"message": "Actualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Mostrar descargas activas"
},
"app.action-bar.view-instance": {
"message": "Mostrar instancia"
"message": "Ver instancia"
},
"app.action-bar.view-logs": {
"message": "Mostrar registros"
"message": "Ver registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
@@ -42,43 +54,43 @@
"message": "Renderizado avanzado"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
"message": "Selecciona tu tema de color preferido para la Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el lanzador inicia."
"message": "Cambia la página en la que el launcher inicia."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Librería"
"message": "Biblioteca"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página predeterminada"
"message": "Página de inicio predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar etiqueta"
"message": "Ocultar etiqueta de nombre"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Vuelve a jugar tus mundos"
"message": "Volver a jugar en tus mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el lanzador cuando un proceso de Minecraft empieza."
"message": "Minimiza el lanzador cuando se inicia un proceso de Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el lanzador"
"message": "Minimizar lanzador"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de marco de Windows (se requiere resetear la aplicación)."
"message": "Usa el marco de ventana del sistema (requiere reiniciar la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -86,6 +98,12 @@
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostrar tiempo de juego"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Activa la posibilidad de alternar la barra lateral."
},
@@ -96,7 +114,7 @@
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, debes comprender los riesgos que esto conlleva antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advertir antes de instalarme paquetes de mods desconocidos"
"message": "Adviérteme antes de instalarme modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -105,7 +123,7 @@
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a instancia"
"message": "Añadiendo servidor a la instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
@@ -135,7 +153,7 @@
"message": "Ocultar servidores ya añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Paquetes de Mods"
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede comprometer la compatibilidad a la hora de unirse a un servidor. Cualquier contenido añadido se perderá cuando actualices la instancia del servidor."
@@ -143,11 +161,8 @@
"app.browse.server.installing": {
"message": "Instalando"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando el modpack..."
"app.content-install.no-compatible-versions": {
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
},
"app.export-modal.description-placeholder": {
"message": "Escribe la descripción del modpack..."
@@ -158,18 +173,12 @@
"app.export-modal.header": {
"message": "Exportar modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "¿Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nombre del modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos se incluirán en esta instancia"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
},
@@ -224,12 +233,6 @@
"app.instance.worlds.browse-servers": {
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' será **permanentemente eliminado**, y habrá forma de recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "¿Estás seguro de qué quieres eliminar permanentemente este mundo?"
},
"app.instance.worlds.filter-modded": {
"message": "Modeado"
},
@@ -248,21 +251,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores o mundos añadidos"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "¿Estás seguro de qué quieres eliminar {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Buscar {count} mundos..."
},
"app.instance.worlds.this-server": {
"message": "este servidor"
},
"app.modal.install-to-play.content-required": {
"message": "Contenido obligatorio"
},
@@ -332,8 +323,152 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"app.update-popup.body": {
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
"app.skins.add-button": {
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastrar y soltar"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.skins.delete-button": {
"message": "Borrar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto borrará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro/a de que quieres eliminar está skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Error al leer el archivo arrastrado."
},
"app.skins.dropped-file-error.title": {
"message": "Error al procesar el archivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
},
"app.skins.modal.add-skin-button": {
"message": "Añadir skin"
},
"app.skins.modal.add-title": {
"message": "Añadiendo una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo de brazo"
},
"app.skins.modal.arm-style-slim": {
"message": "Delgado"
},
"app.skins.modal.arm-style-wide": {
"message": "Ancho"
},
"app.skins.modal.cape-fallback-name": {
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "¡Modifica la skin primero!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sin capa"
},
"app.skins.modal.none-cape-option": {
"message": "Nada"
},
"app.skins.modal.replace-texture-button": {
"message": "Reemplazar textura"
},
"app.skins.modal.save-skin-button": {
"message": "Guardar skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Guardando..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "¡Primero sube una skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Previsualizando"
},
"app.skins.rate-limit.text": {
"message": "Estuviste cambiando tu skin demasiado rápido. Los servidores de Mojang han bloqueado temporalmente tus futuras solicitaciones. Por favor, espere un momento antes de intentar de nuevo."
},
"app.skins.rate-limit.title": {
"message": "¡Cálmate!"
},
"app.skins.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders y Biomas"
},
"app.skins.section.chase-the-skies": {
"message": "Persigue los cielos"
},
"app.skins.section.default-skins": {
"message": "Skins por defecto"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgullo Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Monturas del Caos"
},
"app.skins.section.saved-skins": {
"message": "Skins guardadas"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "La Edad de Cobre"
},
"app.skins.section.the-garden-awakens": {
"message": "El Jardín Despierta"
},
"app.skins.section.tiny-takeover": {
"message": "Pequeña Invasión"
},
"app.skins.sign-in.button": {
"message": "Iniciar Sesión"
},
"app.skins.sign-in.description": {
"message": "Por favor, inicia sesión con tu cuenta de Minecraft para usar las funciones del administrador de Skins de la app de Modrinth."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emocionado"
},
"app.skins.sign-in.title": {
"message": "Por favor, inicia sesión"
},
"app.skins.title": {
"message": "Selector de Skin"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} se ha descargado. Actualiza la página ahora o espera a que se actualice automáticamente al cerrar Modrinth App."
@@ -354,7 +489,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Recargar"
"message": "Recargar para actualizar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -365,15 +500,6 @@
"app.update.complete-toast.title": {
"message": "¡La versión {version} se ha instalado correctamente!"
},
"app.update.download-update": {
"message": "Descarga actualización"
},
"app.update.downloading-update": {
"message": "Descargando actualización ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"app.world.server-modal.placeholder-address": {
"message": "ejemplo.modrinth.gg"
},
@@ -563,6 +689,30 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Se mostrarán las versiones estables, beta y alfa como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Se mostrarán las versiones estables y beta como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo se mostrarán las versiones estables como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleccionar canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de lanzamiento"
},
@@ -744,7 +894,7 @@
"message": "Versión del juego proporciona por la instancia"
},
"search.filter.locked.instance-loader.title": {
"message": "Cargador proporcionado por la instancia"
"message": "Loader proporcionado por la instancia"
},
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
@@ -759,7 +909,7 @@
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "El cargador lo proporciona el servidor"
"message": "Loader proporcionado por el servidor"
},
"unknown-pack-warning-modal.body": {
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
+294 -36
View File
@@ -2,12 +2,30 @@
"app.action-bar.downloading-java": {
"message": "Ladataan Java-versiota {version}"
},
"app.action-bar.downloading-update": {
"message": "Ladataan päivitystä"
},
"app.action-bar.downloads": {
"message": "Lataukset"
},
"app.action-bar.hide-more-running-instances": {
"message": "Piilota enemmän käynnissä olevia instansseja"
},
"app.action-bar.install.dismiss": {
"message": "Hylkää"
},
"app.action-bar.install.open-instance": {
"message": "Avaa instanssi"
},
"app.action-bar.install.retry": {
"message": "Yritä uudelleen"
},
"app.action-bar.install.unknown-instance": {
"message": "Tuntematon instanssi"
},
"app.action-bar.installs": {
"message": "Asennukset"
},
"app.action-bar.make-primary-instance": {
"message": "Aseta ensisijaiseksi instanssiksi"
},
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Ensisijainen instanssi"
},
"app.action-bar.reload-to-update": {
"message": "Lataa uudelleen päivittääksesi"
},
"app.action-bar.show-more-running-instances": {
"message": "Näytä enemmän käynnissä olevia instansseja"
},
"app.action-bar.stop-instance": {
"message": "Pysäytä instanssi"
},
"app.action-bar.update": {
"message": "Päivitä"
},
"app.action-bar.view-active-downloads": {
"message": "Näytä aktiiviset lataukset"
},
@@ -86,6 +110,18 @@
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
},
"app.appearance-settings.show-play-time.title": {
"message": "Näyttää peliaika"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Ohittaa automaattisesti vähäriskiset vahvistukset, kuten modpackien kaksoisasennukset, tavallisen sisällön poistamisen, massapäivitykset, modpackien linkityksen poistamisen ja korjauskehotteet. Vaaralliset varoitukset näytetään silti."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Ohita ei olennaiset varoitukset"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Laittaa päälle mahdollisuuden kytkeä sivupalkkia."
},
@@ -143,11 +179,8 @@
"app.browse.server.installing": {
"message": "Asennetaan"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Asennetaan modipakettia..."
"app.content-install.no-compatible-versions": {
"message": "Yhtään versiota ei ole saatavilla yhteensopivuudella {compatibilityLabel}. Valitse asennettava versio siitä huolimatta. Riippuvuuksia ei asenneta automaattisesti."
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Vie modipaketti"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sisällytä \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modipaketin nimi"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modipaketin nimi"
},
"app.export-modal.select-files-label": {
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Ladataan sisältöä"
},
"app.install.phase.downloading_minecraft": {
"message": "Ladataan Minecraftia"
},
"app.install.phase.downloading_pack_file": {
"message": "Ladataan paketti tiedostoja"
},
"app.install.phase.extracting_overrides": {
"message": "Puretaan ohituksia"
},
"app.install.phase.finalizing": {
"message": "Viimeistellään"
},
"app.install.phase.preparing_instance": {
"message": "Valmistellaan instanssia"
},
"app.install.phase.preparing_java": {
"message": "Valmistellaan Javaa"
},
"app.install.phase.preparing_java.downloading": {
"message": "Ladataan Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Puretaan Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Haetaan Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Valmistellaan Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Tarkistetaan Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Luetaan paketin manifestia"
},
"app.install.phase.resolving_loader": {
"message": "Ratkaistaan lataajaa"
},
"app.install.phase.resolving_minecraft": {
"message": "Ratkaistaan Minecraftia"
},
"app.install.phase.resolving_pack": {
"message": "Ratkaistaan sisältöä"
},
"app.install.phase.rolling_back": {
"message": "Palautetaan muutoksia"
},
"app.install.phase.running_loader_processors": {
"message": "Suoritetaan lataajan käsittelijöitä"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Kaikki sinun instanssisi data poistetaan pysyvästi, sisältäen maailmasi, konfiguraatiosi ja asennetun sisällön."
},
@@ -200,6 +281,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instanssi"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "Ladataan {current, number}/{total, number} projekteja..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Viimeistellään päivitystä..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Selvitetään versioita..."
},
"app.instance.mods.content-type-project": {
"message": "projekti"
},
@@ -224,11 +314,17 @@
"app.instance.worlds.browse-servers": {
"message": "Selaa palvelimia"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' tullaan **poistamaan pysyvästi**, eikä tule olemaan mitään tapaa palauttaa sitä."
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Poista maailma"
},
"app.instance.worlds.delete-world-title": {
"message": "Oletko varma että haluat pysyvästi poistaa tämän maailman?"
"app.instance.worlds.delete-world-modal.title": {
"message": "Poista maailma"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Tämä maailma poistetaan pysyvästi tästä instanssista. Tätä toimintoa ei voi kumota."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Poistetaan {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Modattu"
@@ -248,21 +344,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Ei palvelimia tai maailmoja lisättynä"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Poista palvelin"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
"app.instance.worlds.remove-server-modal.title": {
"message": "Poista palvelin"
},
"app.instance.worlds.remove-server-title": {
"message": "Oletko varma ethaluat poistaa {name}?"
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Tämä palvelin poistetaan palvelinluettelostasi ja pelin sisäisespalvelinluettelosta. Voit lisätä sen myöhemmin uudelleen, jos tiedät osoitteen."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Poistetaan {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Hae {count} maailmasta..."
},
"app.instance.worlds.this-server": {
"message": "tämän palvelimen"
},
"app.modal.install-to-play.content-required": {
"message": "Sisältö vaaditaan"
},
@@ -332,8 +428,152 @@
"app.settings.tabs.resource-management": {
"message": "Resurssien hallinta"
},
"app.update-popup.body": {
"message": "Modrinth App versio {version} on valmis asennettavaksi. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
"app.skins.add-button": {
"message": "Lisää ulkoasu"
},
"app.skins.add-button.drag-and-drop": {
"message": "Raahaa ja pudota"
},
"app.skins.apply-button": {
"message": "Käytä"
},
"app.skins.delete-button": {
"message": "Poista ulkoasu"
},
"app.skins.delete-modal.description": {
"message": "Tämä poistaa valitun skinin pysyvästi. Tätä toimintoa ei voi kumota."
},
"app.skins.delete-modal.title": {
"message": "Haluatko varmasti poistaa ulkoasun?"
},
"app.skins.dropped-file-error.text": {
"message": "Tiedoston lukeminen epäonnistui."
},
"app.skins.dropped-file-error.title": {
"message": "Virhe prosessoidessa tiedostoa"
},
"app.skins.edit-button": {
"message": "Muokkaa ulkoasua"
},
"app.skins.modal.add-skin-button": {
"message": "Lisää ulkoasu"
},
"app.skins.modal.add-title": {
"message": "Lisätään ulkoasua"
},
"app.skins.modal.arm-style-section": {
"message": "Käsien tyyli"
},
"app.skins.modal.arm-style-slim": {
"message": "Ohut"
},
"app.skins.modal.arm-style-wide": {
"message": "Paksu"
},
"app.skins.modal.cape-fallback-name": {
"message": "Viitta"
},
"app.skins.modal.cape-section": {
"message": "Viitta"
},
"app.skins.modal.edit-title": {
"message": "Muokataan ulkoasua"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Tee muokkaus ulkoasuun ensin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Ei viittaa"
},
"app.skins.modal.none-cape-option": {
"message": "Ei mitään"
},
"app.skins.modal.replace-texture-button": {
"message": "Korvaa tekstuuri"
},
"app.skins.modal.save-skin-button": {
"message": "Tallenna ulkoasu"
},
"app.skins.modal.saving-tooltip": {
"message": "Tallennetaan..."
},
"app.skins.modal.texture-section": {
"message": "Tekstuuri"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lataa ulkoasu ensin!"
},
"app.skins.preview.edit-button": {
"message": "Muokkaa ulkoasua"
},
"app.skins.previewing-badge": {
"message": "Esikatsellaan"
},
"app.skins.rate-limit.text": {
"message": "Vaihdat ulkoasua liian usein. Mojangin palvelimet ovat estäneet uudet pyynnöt väliaikaisesti. Yritä hetken kuluttua uudelleen."
},
"app.skins.rate-limit.title": {
"message": "Hidasta vähän!"
},
"app.skins.reorder-error.text": {
"message": "Ulkoasujen järjestystä ei voitu tallentaa."
},
"app.skins.reorder-error.title": {
"message": "Skinien järjestyksen muuttaminen epäonnistui"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Oletus ulkoasut"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Sait nämä skinit lahjoittamalla Modrinthin Pride-keräykseen Pride-kuukauden aikana."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Sekasorron ratsut"
},
"app.skins.section.saved-skins": {
"message": "Tallennetut ulkoasut"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Pienten vallankumous"
},
"app.skins.sign-in.button": {
"message": "Kirjaudu sisään"
},
"app.skins.sign-in.description": {
"message": "Kirjaudu Minecraft-tilillesi käyttääksesi Modrinth-sovelluksen ulkoasujen hallintaominaisuuksia."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Innostunut Modrinth Botti"
},
"app.skins.sign-in.title": {
"message": "Kirjaudu sisään"
},
"app.skins.title": {
"message": "Skinivalinta"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App versio {version} on ladattu. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
@@ -354,7 +594,7 @@
"message": "Lataus valmis"
},
"app.update-popup.reload": {
"message": "Lataa uudelleen"
"message": "Lataa uudelleen päivittääksesi"
},
"app.update-popup.title": {
"message": "Päivitys saatavilla"
@@ -365,15 +605,6 @@
"app.update.complete-toast.title": {
"message": "Versio {version} asennettiin onnistuneesti!"
},
"app.update.download-update": {
"message": "Lataa päivitys"
},
"app.update.downloading-update": {
"message": "Ladataan päivitystä ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Lataa uudelleen asentaaksesi päivityksen"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -563,6 +794,30 @@
"instance.settings.tabs.general.name": {
"message": "Nimi"
},
"instance.settings.tabs.general.update-channel": {
"message": "Päivitys kanava"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release-, beta- ja alpha-versiot näytetään saatavilla olevina päivityksinä."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release- ja beta-versiot näytetään saatavilla olevina päivityksinä."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Julkaisu"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Vain release-versiot näytetään saatavilla olevina päivityksinä."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Valitse päivitys kanava"
},
"instance.settings.tabs.hooks": {
"message": "Aloitustoiminnot"
},
@@ -686,6 +941,9 @@
"instance.worlds.copy_address": {
"message": "Kopioi osoite"
},
"instance.worlds.create_shortcut": {
"message": "Luo pikakuvake"
},
"instance.worlds.dont_show_on_home": {
"message": "Älä näytä koti näkymässä"
},
+65 -44
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Dina-download ang Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Dina-download ang update"
},
"app.action-bar.downloads": {
"message": "Mga downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Pangunahing instansiya"
},
"app.action-bar.reload-to-update": {
"message": "Mag-reload upang ma-update"
},
"app.action-bar.show-more-running-instances": {
"message": "Ipakita ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.stop-instance": {
"message": "Itigil ang instansiya"
},
"app.action-bar.update": {
"message": "Mag-update"
},
"app.action-bar.view-active-downloads": {
"message": "Tignan ang mga active downloads"
},
@@ -86,6 +95,9 @@
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Pagpapagana na mata-toggle ang sidebar."
},
@@ -104,6 +116,12 @@
"app.auth-servers.unreachable.header": {
"message": "Hindi maabot ang mga authentication server"
},
"app.browse.add-servers-to-instance": {
"message": "Idinaragdag ang server sa instansiya"
},
"app.browse.add-to-an-instance": {
"message": "Idagdag sa instansiya"
},
"app.browse.add-to-instance": {
"message": "Idagdag sa instansiya"
},
@@ -116,6 +134,9 @@
"app.browse.already-added": {
"message": "Nadagdag na"
},
"app.browse.back-to-instance": {
"message": "Bumalik sa instansiya"
},
"app.browse.discover-content": {
"message": "Tumuklas ng kontento"
},
@@ -128,15 +149,12 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Maaaring makasisira sa pagkakatugma sa pagsasali ng server ang pagdaragdag ng kontento. Ang anumang kontento ay mawawala sa pag-update ng kontento ng instansiyang pang-server."
},
"app.browse.server.installing": {
"message": "Ini-install"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Ini-install ang modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Ilagay ang paglalarawan ng modpack..."
},
@@ -146,18 +164,12 @@
"app.export-modal.header": {
"message": "Iluwas ang modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Salihin ang \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Pangalan ng Modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Pangalan ng modpack"
},
"app.export-modal.select-files-label": {
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
},
"app.export-modal.version-number-label": {
"message": "Numero ng bersiyon"
},
@@ -212,12 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Mag-browse ng servers"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' ay **permanenteng mabubura**, at walang paraan upang maibalik muli."
},
"app.instance.worlds.delete-world-title": {
"message": "Sigurado ka bang gusto mong permanenteng burahin ang world na ito?"
},
"app.instance.worlds.filter-modded": {
"message": "Modded"
},
@@ -236,21 +242,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Walang servers o worlds ang nalalagay"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-title": {
"message": "Sigurado ka bang gusto mong tanggalin ang {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Hanapin sa {count} mundo..."
},
"app.instance.worlds.this-server": {
"message": "server na ito"
},
"app.modal.install-to-play.content-required": {
"message": "Nangangailangan ng kontento"
},
@@ -287,6 +281,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "Kailangang mag-update upang malaro ang {name}. Mangyaring mag-update sa pinakabagong bersiyon upang ma-launch ang laro."
},
"app.project.install-button.already-installed": {
"message": "Naka-install na ang proyektong ito"
},
"app.project.install-context.back-to-browse": {
"message": "Bumalik sa pagtutuklas"
},
"app.settings.developer-mode-enabled": {
"message": "Binuksan ang moda ng nagdi-develop."
},
@@ -311,8 +311,35 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng paglalaan"
},
"app.update-popup.body": {
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
"app.skins.apply-button": {
"message": "Ilapat"
},
"app.skins.modal.arm-style-slim": {
"message": "Payat"
},
"app.skins.modal.arm-style-wide": {
"message": "Malapad"
},
"app.skins.modal.cape-fallback-name": {
"message": "Kapa"
},
"app.skins.modal.cape-section": {
"message": "Kapa"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Walang kapa"
},
"app.skins.modal.none-cape-option": {
"message": "Wala"
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.update-popup.body.download-complete": {
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
@@ -332,9 +359,6 @@
"app.update-popup.download-complete": {
"message": "Nakumpleto ang pagdownload"
},
"app.update-popup.reload": {
"message": "Mag-reload"
},
"app.update-popup.title": {
"message": "May bagong update"
},
@@ -344,15 +368,6 @@
"app.update.complete-toast.title": {
"message": "Tagumpay na na-install ang bersiyong {version}!"
},
"app.update.download-update": {
"message": "I-download ang update"
},
"app.update.downloading-update": {
"message": "Nagdadownload ng update ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Handang ma-install ang update"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -542,6 +557,12 @@
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.hooks": {
"message": "Mga launch hook"
},
+306 -36
View File
@@ -2,12 +2,30 @@
"app.action-bar.downloading-java": {
"message": "Téléchargement de Java {version} en cours"
},
"app.action-bar.downloading-update": {
"message": "Téléchargement de la mise à jour"
},
"app.action-bar.downloads": {
"message": "Téléchargements"
},
"app.action-bar.hide-more-running-instances": {
"message": "Masquer plus d'instances en exécution"
},
"app.action-bar.install.dismiss": {
"message": "Rejeter"
},
"app.action-bar.install.open-instance": {
"message": "Ouvrir l'instance"
},
"app.action-bar.install.retry": {
"message": "Réessayer"
},
"app.action-bar.install.unknown-instance": {
"message": "Instance inconnue"
},
"app.action-bar.installs": {
"message": "Installations"
},
"app.action-bar.make-primary-instance": {
"message": "Définir en tante qu'instance première"
},
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Instance première"
},
"app.action-bar.reload-to-update": {
"message": "Recharger pour mettre à jour"
},
"app.action-bar.show-more-running-instances": {
"message": "Montrer plus d'instances en exécution"
},
"app.action-bar.stop-instance": {
"message": "Arrêter l'instance"
},
"app.action-bar.update": {
"message": "Mettre à jour"
},
"app.action-bar.view-active-downloads": {
"message": "Voir les téléchargements actifs"
},
@@ -45,13 +69,13 @@
"message": "Sélectionnez votre couleur préférée pour le thème de Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Thème couleur"
"message": "Couleur du thème"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Modifier la page sur laquelle le lancheur s'ouvre."
"message": "Modifier la page sur laquelle le lanceur s'ouvre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Acceuil"
"message": "Accueil"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliothèque"
@@ -72,10 +96,10 @@
"message": "Mondes sur partie rapide"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiser le launcher quand Minecraft démarre."
"message": "Minimiser le lanceur quand Minecraft démarre."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiser le launcher"
"message": "Minimiser le lanceur"
},
"app.appearance-settings.native-decorations.description": {
"message": "Utiliser le cadre fenêtre du système (redémarrage de l'application requis)."
@@ -86,8 +110,20 @@
"app.appearance-settings.select-option": {
"message": "Sélectionnez une option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Afficher le temps de jeu"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Ignore automatiquement les confirmations à faible risque, comme l'installation de modpacks en double, la suppression de contenu standard, les mises à jour groupées, la dissociation des modpacks et les demandes de réparation. Les avertissements concernant les actions dangereuses continueront d'être affichés."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Cacher les alertes non essentielles"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permet d'ouvrir de de fermer la barre latérale."
"message": "Permet d'ouvrir ou de fermer la barre latérale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Tiroir latéral"
@@ -143,11 +179,8 @@
"app.browse.server.installing": {
"message": "Installation en cours"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installation du modpack..."
"app.content-install.no-compatible-versions": {
"message": "Aucune version disponible ne correspond à {compatibilityLabel}. Sélectionnez une version pour l'installer malgré tout. Les dépendances ne seront pas installées automatiquement."
},
"app.export-modal.description-placeholder": {
"message": "Saisir la description du modpack..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Exporter le modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Inclure « {file} » ?"
},
"app.export-modal.modpack-name-label": {
"message": "Nom du modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nom du modpack"
},
"app.export-modal.select-files-label": {
"message": "Configurez quels fichiers sont inclus dans cette exportation"
},
"app.export-modal.version-number-label": {
"message": "Numéro de version"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Téléchargement du contenu"
},
"app.install.phase.downloading_minecraft": {
"message": "Téléchargement de Minecraft"
},
"app.install.phase.downloading_pack_file": {
"message": "Téléchargement du fichier de pack"
},
"app.install.phase.extracting_overrides": {
"message": "Extraction des fichiers personnalisés"
},
"app.install.phase.finalizing": {
"message": "Finalisation"
},
"app.install.phase.preparing_instance": {
"message": "Préparation de l'instance"
},
"app.install.phase.preparing_java": {
"message": "Préparation de Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Téléchargement de Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Extraction de Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Récupération de Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Préparation de Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Validation de Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Lecture du manifeste du pack"
},
"app.install.phase.resolving_loader": {
"message": "Résolution du loader"
},
"app.install.phase.resolving_minecraft": {
"message": "Résolution de Minecraft"
},
"app.install.phase.resolving_pack": {
"message": "Résolution du contenu"
},
"app.install.phase.rolling_back": {
"message": "Annulation"
},
"app.install.phase.running_loader_processors": {
"message": "Exécution des processeurs du chargeur"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Toutes les données pour votre instance seront supprimées à jamais, y compris vos mondes, vos configurations, et le contenu installé."
},
@@ -200,6 +281,15 @@
"app.instance.modpack-already-installed.instance": {
"message": "Instance"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "Téléchargement de {current, number}/{total, number} projets..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Finalisation de la mise à jour..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Résolution des versions..."
},
"app.instance.mods.content-type-project": {
"message": "projet"
},
@@ -224,8 +314,17 @@
"app.instance.worlds.browse-servers": {
"message": "Parcourir des serveurs"
},
"app.instance.worlds.delete-world-title": {
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Supprimer le monde"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Supprimer le monde"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Ce monde sera définitivement supprimé de cette instance. Cette action est irréversible."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Suppression de {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Moddé"
@@ -245,15 +344,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Aucun serveur ou monde ajoutés"
},
"app.instance.worlds.remove-server-title": {
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Enlever le serveur"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Enlever le serveur"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Ce serveur sera enlevé de votre liste de serveur et de la liste de serveur en jeu. Vous pourrez le rajouter plus tard si vous connaissez son adresse."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Retrait de {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Rechercher {count} mondes..."
},
"app.instance.worlds.this-server": {
"message": "ce serveur"
},
"app.modal.install-to-play.content-required": {
"message": "Contenu requis"
},
@@ -323,8 +428,152 @@
"app.settings.tabs.resource-management": {
"message": "Gestion des ressources"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} est prête à être installée ! Relancez l'application pour faire la mise à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
"app.skins.add-button": {
"message": "Ajouter une skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Glisser et déposer"
},
"app.skins.apply-button": {
"message": "Appliquer"
},
"app.skins.delete-button": {
"message": "Supprimer le skin"
},
"app.skins.delete-modal.description": {
"message": "Cela va supprimer de manière permanente ce skin. Cette action est irréversible."
},
"app.skins.delete-modal.title": {
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
},
"app.skins.dropped-file-error.text": {
"message": "Impossible de lire le fichier déposé."
},
"app.skins.dropped-file-error.title": {
"message": "Erreur lors du traitement du fichier"
},
"app.skins.edit-button": {
"message": "Modifier le skin"
},
"app.skins.modal.add-skin-button": {
"message": "Ajouter une skin"
},
"app.skins.modal.add-title": {
"message": "Ajouter une skin"
},
"app.skins.modal.arm-style-section": {
"message": "Bras"
},
"app.skins.modal.arm-style-slim": {
"message": "Mince"
},
"app.skins.modal.arm-style-wide": {
"message": "Large"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Modification du skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Faites d'abord une modification à la skin !"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Aucune cape"
},
"app.skins.modal.none-cape-option": {
"message": "Aucune"
},
"app.skins.modal.replace-texture-button": {
"message": "Remplacer la texture"
},
"app.skins.modal.save-skin-button": {
"message": "Sauvegarder le skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Sauvegarde..."
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Importez d'abord une skin !"
},
"app.skins.preview.edit-button": {
"message": "Modifier le skin"
},
"app.skins.previewing-badge": {
"message": "Aperçu"
},
"app.skins.rate-limit.text": {
"message": "Vous changez de skin trop vite. Les serveurs de Mojang ont temporairement bloqué certaines requêtes. Veuillez patienter un moment puis réessayez."
},
"app.skins.rate-limit.title": {
"message": "Du calme !"
},
"app.skins.reorder-error.text": {
"message": "L'ordre des skins n'a pas pu être sauvegardé."
},
"app.skins.reorder-error.title": {
"message": "Impossible de réorganiser les skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chassez les cieux"
},
"app.skins.section.default-skins": {
"message": "Skins par défaut"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Vous avez reçu ces skins pour avoir fait un don à une collecte de fonds Modrinth Pride pendant le mois de la fierté."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Montures en pagaille"
},
"app.skins.section.saved-skins": {
"message": "Skins sauvegardées"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "L'éveil du jardin"
},
"app.skins.section.tiny-takeover": {
"message": "Horde miniature"
},
"app.skins.sign-in.button": {
"message": "Se connecter"
},
"app.skins.sign-in.description": {
"message": "Veuillez vous connecter à votre compte Minecraft pour utiliser les fonctionnalités de gestion de skins de Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth enthousiaste"
},
"app.skins.sign-in.title": {
"message": "Veuillez vous connecter"
},
"app.skins.title": {
"message": "Sélecteur de skin"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} a finie d'être téléchargée. Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
@@ -345,7 +594,7 @@
"message": "Téléchargement terminé"
},
"app.update-popup.reload": {
"message": "Recharger"
"message": "Recharger pour mettre à jour"
},
"app.update-popup.title": {
"message": "Mise à jour disponible"
@@ -356,15 +605,6 @@
"app.update.complete-toast.title": {
"message": "La version {version} a été téléchargée avec succès !"
},
"app.update.download-update": {
"message": "Télécharger la mise à jour"
},
"app.update.downloading-update": {
"message": "Téléchargement de la mise à jour ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Relancez l'application pour installer la mise à jour"
},
"app.world.server-modal.placeholder-address": {
"message": "exemple.modrinth.gg"
},
@@ -551,6 +791,30 @@
"instance.settings.tabs.general.name": {
"message": "Nom"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de mise à jour"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Les versions stables, bêta et alpha seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Bêta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Les versions stables et bêta seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Seules les versions stables seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Sélectionner le canal de mise à jour"
},
"instance.settings.tabs.hooks": {
"message": "Crochets de lancement"
},
@@ -632,6 +896,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Mémoire allouée"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/chemin/vers/java"
},
"instance.settings.tabs.window": {
"message": "Fenêtre"
},
@@ -671,6 +938,9 @@
"instance.worlds.copy_address": {
"message": "Copier l'adresse"
},
"instance.worlds.create_shortcut": {
"message": "Créer un raccourci"
},
"instance.worlds.dont_show_on_home": {
"message": "Ne pas montrer dans l'Accueil"
},
+30 -33
View File
@@ -1,4 +1,31 @@
{
"app.action-bar.downloading-java": {
"message": "מוריד את Java {version}"
},
"app.action-bar.downloading-update": {
"message": "מוריד עדכון"
},
"app.action-bar.downloads": {
"message": "הורדות"
},
"app.action-bar.hide-more-running-instances": {
"message": "החבא יותר התקנים רצים"
},
"app.action-bar.make-primary-instance": {
"message": "הפוך למקרה ראשי"
},
"app.action-bar.offline": {
"message": "לא מקוון"
},
"app.action-bar.stop-instance": {
"message": ""
},
"app.appearance-settings.advanced-rendering.description": {
"message": "מאפשר עיבוד מתקדם כגון אפקטים טשטוש העלולים לגרום לבעיות ביצועים ללא עיבוד מואץ לחומרה."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "חזרה לעולמות"
},
"app.auth-servers.unreachable.body": {
"message": "ייתכן ששרתי האימות של Minecraft מושבתים כרגע. יש לבדוק את חיבור האינטרנט שלך ולנסות שוב מאוחר יותר."
},
@@ -92,12 +119,6 @@
"app.instance.worlds.browse-servers": {
"message": "עיון בשרתים"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' יימחק לצמיתות, ולא תהיה דרך לשחזר אותו."
},
"app.instance.worlds.delete-world-title": {
"message": "האם אתה בטוח שברצונך למחוק לצמיתות את העולם הזה?"
},
"app.instance.worlds.filter-modded": {
"message": "עם מודים"
},
@@ -116,21 +137,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "אין שרתים אן עולמות שנוספו"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
},
"app.instance.worlds.remove-server-title": {
"message": "אתה בטוח שאתה רוצה להסיר {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "חיפוש ב-{count} עולמות..."
},
"app.instance.worlds.this-server": {
"message": "השרת הזה"
},
"app.modal.install-to-play.content-required": {
"message": "תוכן נדרש"
},
@@ -191,9 +200,6 @@
"app.settings.tabs.resource-management": {
"message": "ניהול משאבים"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} מוכנה להורדה!\nיש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} סיימה את תהליך ההורדה. יש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
@@ -212,9 +218,6 @@
"app.update-popup.download-complete": {
"message": "הורדה הושלמה"
},
"app.update-popup.reload": {
"message": "רענן"
},
"app.update-popup.title": {
"message": "עדכון זמין"
},
@@ -224,15 +227,6 @@
"app.update.complete-toast.title": {
"message": "גרסה {version} הותקנה בהצלחה!"
},
"app.update.download-update": {
"message": "הורדת עדכון"
},
"app.update.downloading-update": {
"message": "מוריד עדכון ({percent}%)"
},
"app.update.reload-to-update": {
"message": "צריך לרענן כדי להתקין את העדכון"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -254,6 +248,9 @@
"friends.action.add-friend": {
"message": "הוספת חבר"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {בקשת חברות אחת} other {# בקשות חברות}}"
},
"friends.add-friend.submit": {
"message": "שליחת בקשת חברות"
},
+237 -60
View File
@@ -2,35 +2,50 @@
"app.action-bar.downloading-java": {
"message": "A Java {version} letöltése"
},
"app.action-bar.downloading-update": {
"message": "Frissítés letöltése"
},
"app.action-bar.downloads": {
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó profilok elrejtése"
"message": "További futó példányok elrejtése"
},
"app.action-bar.install.retry": {
"message": "Újra"
},
"app.action-bar.install.unknown-instance": {
"message": "Ismeretlen profil"
},
"app.action-bar.make-primary-instance": {
"message": "Beállítás elsődleges profilként"
"message": "Beállítás elsődleges példányként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó profilok"
"message": "Nincsenek futó példányok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges profil"
"message": "Elsődleges példány"
},
"app.action-bar.reload-to-update": {
"message": "Töltsd újra a frissítéshez"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó profilok megjelenítése"
"message": "További futó pédányok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Profil megállítása"
"message": "Példány leállítása"
},
"app.action-bar.update": {
"message": "Frissítés"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Profil megtekintése"
"message": "Példányok megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
@@ -60,7 +75,7 @@
"message": "Alapértelmezett nyitóoldal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Letiltja a játékosod felett megjelenő névcímkét a Skinek oldalon."
"message": "Letiltja a játékosod felett megjelenő névcímkét a Kinézetek oldalon."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Névcímke eltüntetése"
@@ -69,7 +84,7 @@
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "„Folytasd itt” a világokat is"
"message": "Visszaugrás a világokba"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
@@ -86,6 +101,12 @@
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél játszva egy profillal."
},
"app.appearance-settings.show-play-time.title": {
"message": "Játékidő megjelenítése"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
@@ -105,13 +126,13 @@
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása a profilhoz"
"message": "Szerver hozzáadása a példányhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy profilhoz"
"message": "Hozzáadás egy példányhoz"
},
"app.browse.add-to-instance": {
"message": "Hozzáadás a profilhoz"
"message": "Példányhoz adás"
},
"app.browse.add-to-instance-name": {
"message": "Hozzáadás ehhez: {instanceName}"
@@ -123,7 +144,7 @@
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a profilhoz"
"message": "Vissza a példányhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
@@ -143,11 +164,8 @@
"app.browse.server.installing": {
"message": "Letöltés..."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modcsomag telepítése..."
"app.content-install.no-compatible-versions": {
"message": "Nincs a(z) {compatibilityLabel} verziónak megfelelő elérhető verzió. Válassz ki egy verziót, ha mégis telepíteni szeretnéd. A függőségek nem települnek automatikusan."
},
"app.export-modal.description-placeholder": {
"message": "Írd be a modcsomag leírását..."
@@ -158,24 +176,21 @@
"app.export-modal.header": {
"message": "Modcsomag exportálása"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Tartalmaz „{file}” fájlokat?"
},
"app.export-modal.modpack-name-label": {
"message": "A modcsomag neve"
},
"app.export-modal.modpack-name-placeholder": {
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.resolving_loader": {
"message": ""
},
"app.instance.confirm-delete.admonition-body": {
"message": "Az profilodhoz tartozó összes adat véglegesen törlődik, beleértve a világokat, a beállításokat és az összes telepített tartalmat."
},
@@ -198,7 +213,7 @@
"message": "A modcsomag már telepítve van"
},
"app.instance.modpack-already-installed.instance": {
"message": "Profil"
"message": "Példány"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
@@ -224,12 +239,6 @@
"app.instance.worlds.browse-servers": {
"message": "Szerverek böngészése"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' **véglegesen törlődik**, és azt semmilyen módon nem lehet majd helyreállítani."
},
"app.instance.worlds.delete-world-title": {
"message": "Biztosan véglegesen törölni szeretnéd ezt a világot?"
},
"app.instance.worlds.filter-modded": {
"message": "Modolt"
},
@@ -248,21 +257,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Nincs szerver vagy világ"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' eltávolításra kerül a listádról, beleértve a játékon belülieket is. Ezt a lépést nem tudod visszavonni."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) eltávolításra kerül a listádról, beleértve a játékon belülieket is. Ezt a lépést nem tudod visszavonni."
},
"app.instance.worlds.remove-server-title": {
"message": "Biztosan el akarod távolítani ezt: {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Keresés {count} világ között..."
},
"app.instance.worlds.this-server": {
"message": "ez a szerver"
},
"app.modal.install-to-play.content-required": {
"message": "Szükséges tartalom"
},
@@ -272,6 +269,9 @@
"app.modal.install-to-play.install-button": {
"message": "Telepítés"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Szükséges modcsomag"
},
@@ -329,8 +329,146 @@
"app.settings.tabs.resource-management": {
"message": "Erőforráskezelés"
},
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
"app.skins.add-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.add-button.drag-and-drop": {
"message": "Húzd ide a fájlt"
},
"app.skins.apply-button": {
"message": "Alkalmazás"
},
"app.skins.delete-button": {
"message": "Kinézet törlése"
},
"app.skins.delete-modal.description": {
"message": "Ezzel a kiválasztott kinézet véglegesen törlődik. Ez a művelet visszafordíthatatlan."
},
"app.skins.delete-modal.title": {
"message": "Biztos, hogy le akarod törölni ezt a kinézetet?"
},
"app.skins.dropped-file-error.text": {
"message": "Nem sikerült beolvasni a feltöltött fájlt."
},
"app.skins.dropped-file-error.title": {
"message": "Hiba a fájl feldolgozása közben"
},
"app.skins.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.add-skin-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.add-title": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.arm-style-section": {
"message": "Karstílus"
},
"app.skins.modal.arm-style-slim": {
"message": "Vékony"
},
"app.skins.modal.arm-style-wide": {
"message": "Széles"
},
"app.skins.modal.cape-fallback-name": {
"message": "Köpeny"
},
"app.skins.modal.cape-section": {
"message": "Köpeny"
},
"app.skins.modal.edit-title": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Előbb módosíts valamit!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nincs köpeny"
},
"app.skins.modal.none-cape-option": {
"message": "Nincs"
},
"app.skins.modal.replace-texture-button": {
"message": "Textúra cseréje"
},
"app.skins.modal.save-skin-button": {
"message": "Kinézet mentése"
},
"app.skins.modal.saving-tooltip": {
"message": "Mentés..."
},
"app.skins.modal.texture-section": {
"message": "Textúra"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Először tölts fel egy kinézetet!"
},
"app.skins.preview.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.previewing-badge": {
"message": "Előnézet"
},
"app.skins.rate-limit.text": {
"message": "Túl gyakran cseréled a kinézetedet. A Mojang szerverei ideiglenesen letiltották a további cseréket. Kérlek várj egy pillanatot, majd próbáld meg újra."
},
"app.skins.rate-limit.title": {
"message": "Lassíts!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Alap kinézetek"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Ezeket a kinézeteket azért kaptad, mert a Pride-hónap során adományoztál a Modrinth Pride jótékonysági gyűjtéséhez."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Mentett kinézetek"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Bejelentkezés"
},
"app.skins.sign-in.description": {
"message": "Kérlek jelentkezz be a Minecraft-fiókodba, hogy használni tudd a Modrinth App kinézetkezelési funkcióit."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Izgatott Modrinth-bot"
},
"app.skins.sign-in.title": {
"message": "Kérlek jelentkezz be"
},
"app.skins.title": {
"message": "Kinézetváltó"
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
@@ -351,7 +489,7 @@
"message": "Sikeres letöltés"
},
"app.update-popup.reload": {
"message": "Frissítés"
"message": "Töltsd újra a frissítéshez"
},
"app.update-popup.title": {
"message": "Frissítés elérhető"
@@ -362,15 +500,6 @@
"app.update.complete-toast.title": {
"message": "Verzió {version} sikeresen telepítve!"
},
"app.update.download-update": {
"message": "Frissítés letöltése"
},
"app.update.downloading-update": {
"message": "Frissítés letöltése ({percent}%)"
},
"app.update.reload-to-update": {
"message": "A telepítéshez újraindítás szükséges"
},
"app.world.server-modal.placeholder-address": {
"message": "pelda.modrinth.gg"
},
@@ -392,6 +521,9 @@
"friends.action.add-friend": {
"message": "Barát hozzáadása"
},
"friends.action.view-friend-requests": {
"message": "{count} barát {count, plural, other {kérelem}}"
},
"friends.add-friend.submit": {
"message": "Barátkérelem elküldése"
},
@@ -557,6 +689,30 @@
"instance.settings.tabs.general.name": {
"message": "Név"
},
"instance.settings.tabs.general.update-channel": {
"message": "Frissítési csatorna"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "A kiadás, béta és az alfa verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Béta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "A kiadás és a béta verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Kiadás"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Csak a release verziók fognak megjelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Válaszd ki a frissítési csatornát"
},
"instance.settings.tabs.hooks": {
"message": "Indítási horgok"
},
@@ -605,6 +761,24 @@
"instance.settings.tabs.java": {
"message": "Java és memória"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Egyéni környezeti változók"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Egyéni Java argumentumok"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Egyedi Java telepítés"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Egyéni memóriakiosztás"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Írja be a környezeti változókat..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java argumentumok megadása..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Környezeti változók"
},
@@ -620,6 +794,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Hozzárendelt memória"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Ablak"
},
@@ -663,7 +840,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már el van indítva"
"message": "A példány már el van indítva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -687,7 +864,7 @@
"message": "Játék a profillal"
},
"instance.worlds.view_instance": {
"message": "Profil megtekintése"
"message": "Példány megtekintése"
},
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
@@ -711,13 +888,13 @@
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "A profil által van megadva"
"message": "Pédány által megadva"
},
"search.filter.locked.instance-game-version.title": {
"message": "A játékverzió a profil által van megadva"
"message": "A játékverziót az adott példány biztosítja"
},
"search.filter.locked.instance-loader.title": {
"message": "A betöltő a profil által van megadva"
"message": "A betöltőt az adott példány biztosítja"
},
"search.filter.locked.instance.sync": {
"message": "Profil szinkronizálása"
+257 -41
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Mengunduh pembaruan"
},
"app.action-bar.downloads": {
"message": "Unduhan"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instans utama"
},
"app.action-bar.reload-to-update": {
"message": "Muat ulang untuk memperbarui"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan instans"
},
"app.action-bar.update": {
"message": "Perbarui"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat pengunduhan berlangsung"
},
@@ -56,21 +65,51 @@
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Halaman arahan bawaan"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan label nama"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Menyertakan dunia terkini dalam bagian \"Masuk kembali\" pada Beranda."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Masuk kembali ke dunia"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Kecilkan pluncur ketika proses Minecraft berjalan."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gunakan bingkai jendela sistem (wajib menjalankan ulang aplikasi)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
"app.appearance-settings.show-play-time.title": {
"message": "Tampilkan waktu bermain"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Menghidupkan kemampuan untuk menghidupkan dan mematikan bilah sisi."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Hidup/matikan bilah sisi"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Bila Anda mencoba memasangkan berkas Modrinth Pack (.mrpack) yang tidak dihoskan pada Modrinth, kami akan memastikan Anda untuk memahami risikonya sebelum memasangkannya."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Peringatkan saya sebelum memasang paket mod tidak dikenal"
},
@@ -113,14 +152,14 @@
"app.browse.project-type.modpacks": {
"message": "Paket Mod"
},
"app.browse.server-instance-content-warning": {
"message": "Menambah konten dapat merusak kompatibilitas saat bergabung ke server. Konten tambahan apa pun akan hilang ketika Anda memperbarui konten instans server."
},
"app.browse.server.installing": {
"message": "Memasang"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Memasang paket mod..."
"app.content-install.no-compatible-versions": {
"message": "Tidak ada versi tersedia yang cocok untuk {compatibilityLabel}. Anda tetap dapat memilih versi yang akan dipasang. Dependensi tidak akan dipasang secara otomatis."
},
"app.export-modal.description-placeholder": {
"message": "Masukkan deskripsi paket mod..."
@@ -131,18 +170,12 @@
"app.export-modal.header": {
"message": "Ekspor paket mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Paket Mod"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nama paket mod"
},
"app.export-modal.select-files-label": {
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
},
"app.export-modal.version-number-label": {
"message": "Nomor versi"
},
@@ -197,12 +230,6 @@
"app.instance.worlds.browse-servers": {
"message": "Telusuri server"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' akan **dihapus secara permanen**, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.delete-world-title": {
"message": "Apakah Anda yakin ingin menghapus dunia ini secara permanen?"
},
"app.instance.worlds.filter-modded": {
"message": "Dimodifikasi"
},
@@ -221,21 +248,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Tidak ada server atau dunia yang ditambahkan"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.remove-server-title": {
"message": "Apakah Anda yakin ingin menghapus {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Cari {count} dunia..."
},
"app.instance.worlds.this-server": {
"message": "server ini"
},
"app.modal.install-to-play.content-required": {
"message": "Konten diperlukan"
},
@@ -245,6 +260,9 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Paket mod yang diperlukan"
},
@@ -269,6 +287,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} perlu diperbarui sebelum dimainkan. Mohon perbarui ke versi terkini untuk meluncurkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Proyek ini sudah terpasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali ke halaman pencarian"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
@@ -296,8 +320,152 @@
"app.settings.tabs.resource-management": {
"message": "Manajemen sumber"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
"app.skins.add-button": {
"message": "Tambah rupa"
},
"app.skins.add-button.drag-and-drop": {
"message": "Seret dan lepas"
},
"app.skins.apply-button": {
"message": "Terapkan"
},
"app.skins.delete-button": {
"message": "Hapus rupa"
},
"app.skins.delete-modal.description": {
"message": "Ini akan menghapus rupa terpilih secara permanen. Tindakan ini tidak dapat dibatalkan."
},
"app.skins.delete-modal.title": {
"message": "Apakah Anda yakin ingin menghapus rupa ini?"
},
"app.skins.dropped-file-error.text": {
"message": "Gagal membaca berkas yang diletakkan."
},
"app.skins.dropped-file-error.title": {
"message": "Kesalahan dalam memroses berkas"
},
"app.skins.edit-button": {
"message": "Sunting rupa"
},
"app.skins.modal.add-skin-button": {
"message": "Tambah rupa"
},
"app.skins.modal.add-title": {
"message": "Menambah rupa"
},
"app.skins.modal.arm-style-section": {
"message": "Gaya lengan"
},
"app.skins.modal.arm-style-slim": {
"message": "Ramping"
},
"app.skins.modal.arm-style-wide": {
"message": "Lebar"
},
"app.skins.modal.cape-fallback-name": {
"message": "Jubah"
},
"app.skins.modal.cape-section": {
"message": "Jubah"
},
"app.skins.modal.edit-title": {
"message": "Menyunting rupa"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Sunting rupa terlebih dahulu!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Tanpa jubah"
},
"app.skins.modal.none-cape-option": {
"message": "Tidak ada"
},
"app.skins.modal.replace-texture-button": {
"message": "Ganti tekstur"
},
"app.skins.modal.save-skin-button": {
"message": "Simpan rupa"
},
"app.skins.modal.saving-tooltip": {
"message": "Menyimpan..."
},
"app.skins.modal.texture-section": {
"message": "Tekstur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Unggah rupa terlebih dahulu!"
},
"app.skins.preview.edit-button": {
"message": "Sunting rupa"
},
"app.skins.previewing-badge": {
"message": "Mempratinjau"
},
"app.skins.rate-limit.text": {
"message": "Anda terlalu sering mengganti rupa Anda. Server Mojang telah memblokir permintaan lebih lanjut untuk sementara. Mohon tunggu sebentar sebelum mencoba lagi."
},
"app.skins.rate-limit.title": {
"message": "Pelan-pelan!"
},
"app.skins.reorder-error.text": {
"message": "Urutan rupa Anda tidak dapat disimpan."
},
"app.skins.reorder-error.title": {
"message": "Gagal mengurut ulang rupa"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Rupa bawaan"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Anda mendapat rupa ini karena telah berdonasi ke penggalangan dana Modrinth Pride saat Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Rupa tersimpan"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Masuk"
},
"app.skins.sign-in.description": {
"message": "Mohon masuk ke akun Minecraft Anda untuk menggunakan fitur pengelolaan rupa dari Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth penuh antusias"
},
"app.skins.sign-in.title": {
"message": "Silakan masuk"
},
"app.skins.title": {
"message": "Pemilih rupa"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
@@ -318,7 +486,7 @@
"message": "Selesai mengunduh"
},
"app.update-popup.reload": {
"message": "Muat ulang"
"message": "Muat ulang untuk memperbarui"
},
"app.update-popup.title": {
"message": "Pembaruan tersedia"
@@ -329,15 +497,6 @@
"app.update.complete-toast.title": {
"message": "Versi {version} berhasil dipasang!"
},
"app.update.download-update": {
"message": "Unduh pembaruan"
},
"app.update.downloading-update": {
"message": "Mengunduh pembaruan ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Muat ulang untuk memasang pembaruan"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -359,6 +518,9 @@
"friends.action.add-friend": {
"message": "Tambah teman"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} teman"
},
"friends.add-friend.submit": {
"message": "Kirim permintaan teman"
},
@@ -524,6 +686,30 @@
"instance.settings.tabs.general.name": {
"message": "Nama"
},
"instance.settings.tabs.general.update-channel": {
"message": "Perbarui kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Versi rilisan, beta, dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Versi beta dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Rilisan"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Hanya versi rilisan yang akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Pilih kanal pembaruan"
},
"instance.settings.tabs.hooks": {
"message": "Luncurkan kait"
},
@@ -572,6 +758,24 @@
"instance.settings.tabs.java": {
"message": "Java dan memori"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabel lingkungan tersuai"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumen Java tersuai"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Pemasangan Java tersuai"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Alokasi memori tersuai"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Masukkan variabel lingkungan..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Masukkan argumen Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabel lingkungan"
},
@@ -587,6 +791,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memori yang teralokasikan"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/jalur/ke/java"
},
"instance.settings.tabs.window": {
"message": "Jendela"
},
@@ -674,6 +881,9 @@
"minecraft-account.select-account": {
"message": "Pilih akun"
},
"minecraft-account.sign-in": {
"message": "Masuk ke Minecraft"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh instans"
},
@@ -698,6 +908,9 @@
"search.filter.locked.server-loader.title": {
"message": "Pemuat disediakan oleh server"
},
"unknown-pack-warning-modal.body": {
"message": "Berkas hanya akan ditinjau bila ia diunggah ke Modrinth, tak peduli format berkasnya (termasuk .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tampilkan peringatan ini lagi"
},
@@ -707,6 +920,9 @@
"unknown-pack-warning-modal.install-anyway": {
"message": "Tetap pasang"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Program jahat (malware) dibagikan melalui berkas paket mod dengan membagikannya melalui platform seperti Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemukan berkas ini di Modrinth. Kami sangat menyarankan Anda untuk hanya memasang berkas dari sumber-sumber terpercaya."
},
+307 -40
View File
@@ -2,12 +2,30 @@
"app.action-bar.downloading-java": {
"message": "Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Scaricando l'aggiornamento"
},
"app.action-bar.downloads": {
"message": "Download"
},
"app.action-bar.hide-more-running-instances": {
"message": "Mostra meno istanze in esecuzione"
},
"app.action-bar.install.dismiss": {
"message": "Ignora"
},
"app.action-bar.install.open-instance": {
"message": "Apri istanza"
},
"app.action-bar.install.retry": {
"message": "Riprova"
},
"app.action-bar.install.unknown-instance": {
"message": "Istanza sconosciuta"
},
"app.action-bar.installs": {
"message": "Installazioni"
},
"app.action-bar.make-primary-instance": {
"message": "Rendi primaria"
},
@@ -20,12 +38,18 @@
"app.action-bar.primary-instance": {
"message": "Primaria"
},
"app.action-bar.reload-to-update": {
"message": "Ricarica per aggiornare"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.update": {
"message": "Aggiorna"
},
"app.action-bar.view-active-downloads": {
"message": "Mostra i download attivi"
},
@@ -86,14 +110,26 @@
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostra le ore di gioco"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Salta automaticamente le conferme a basso rischio, come l'installazione di pacchetti di mod duplicati, la normale eliminazione di contenuti, gli aggiornamenti simultanei, lo scollegare i pacchetti di mod e le richieste di riparazione. Le avvertenze verranno comunque mostrate."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Salta le conferme poco importanti"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
"message": "Mostra o nascondi la barra laterale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Visti i rischi, apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth."
"message": "Mostrerà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, a causa dei possibili rischi."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
@@ -138,16 +174,13 @@
"message": "Pacchetti di mod"
},
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe portare problemi entrando nel server. Qualsiasi contenuto aggiunto sarà anche perso aggiornando i contenuti dell'istanza del server."
"message": "Aggiungere dei contenuti potrebbe causare problemi entrando nel server. Inoltre, qualsiasi contenuto aggiunto sarà perso quando aggiorni i contenuti dell'istanza."
},
"app.browse.server.installing": {
"message": "Installazione"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installando il pacchetto..."
"app.content-install.no-compatible-versions": {
"message": "Nessuna versione disponibile soddisfa {compatibilityLabel}. Puoi selezionare una versione da installare comunque. Le dipendenze non verranno scaricate automaticamente."
},
"app.export-modal.description-placeholder": {
"message": "Inserisci descrizione del pacchetto..."
@@ -158,24 +191,72 @@
"app.export-modal.header": {
"message": "Esporta pacchetto"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Includere \"{file}\" nell'esportazione?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome del pacchetto"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nome del pacchetto"
},
"app.export-modal.select-files-label": {
"message": "Seleziona i file da includere in questa esportazione"
},
"app.export-modal.version-number-label": {
"message": "Numero di versione"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Download dei contenuti"
},
"app.install.phase.downloading_minecraft": {
"message": "Download di Minecraft"
},
"app.install.phase.downloading_pack_file": {
"message": "Download del pacchetto"
},
"app.install.phase.extracting_overrides": {
"message": "Estrazione degli override"
},
"app.install.phase.finalizing": {
"message": "Finalizzazione"
},
"app.install.phase.preparing_instance": {
"message": "Preparazione dell'istanza"
},
"app.install.phase.preparing_java": {
"message": "Preparazione di Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Download di Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "Estrazione di Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Recupero di Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Preparazione di Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Validazione di Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Lettura del manifesto"
},
"app.install.phase.resolving_loader": {
"message": "Risoluzione del loader"
},
"app.install.phase.resolving_minecraft": {
"message": "Risoluzione di Minecraft"
},
"app.install.phase.resolving_pack": {
"message": "Risoluzione dei contenuti"
},
"app.install.phase.rolling_back": {
"message": "Tornando indietro"
},
"app.install.phase.running_loader_processors": {
"message": "Esecuzione dei processori del loader"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Tutti i dati della tua istanza verranno eliminati permanentemente, inclusi i tuoi mondi, configurazioni e tutti i contenuti installati."
},
@@ -200,15 +281,21 @@
"app.instance.modpack-already-installed.instance": {
"message": "Istanza"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "{current, number}/{total, number} progetti scaricati..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Finalizzando l'aggiornamento..."
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "Risolvendo le versioni..."
},
"app.instance.mods.content-type-project": {
"message": "progetto"
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" è stato aggiunto"
},
"app.instance.mods.projects-were-added": {
"message": "{count} progetti aggiunti"
},
"app.instance.mods.share-text": {
"message": "Guarda le mod che uso nel mio pacchetto!"
},
@@ -224,8 +311,17 @@
"app.instance.worlds.browse-servers": {
"message": "Esplora i server"
},
"app.instance.worlds.delete-world-title": {
"message": "Vuoi davvero eliminare questo mondo per sempre?"
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Elimina mondo"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Eliminare il mondo"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Questo mondo verrà eliminato permanentemente da questa istanza. Questa azione non può essere annullata."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Eliminare {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Moddato"
@@ -245,15 +341,21 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Nessun server o mondo aggiunto"
},
"app.instance.worlds.remove-server-title": {
"message": "Vuoi davvero rimuovere {name}?"
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Rimuovi server"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Rimuovere il server"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Questo server verrà rimosso dalla tua lista di server nell'app e nel gioco. Potrai aggiungerlo nuovamente se conosci l'indirizzo."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Rimuovere {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Cerca tra {count} mondi..."
},
"app.instance.worlds.this-server": {
"message": "questo server"
},
"app.modal.install-to-play.content-required": {
"message": "Contenuto richiesto"
},
@@ -263,6 +365,9 @@
"app.modal.install-to-play.install-button": {
"message": "Installa"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacchetto di mod richiesto"
},
@@ -320,8 +425,152 @@
"app.settings.tabs.resource-management": {
"message": "Gestione delle risorse"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
"app.skins.add-button": {
"message": "Nuova skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Trascina qui"
},
"app.skins.apply-button": {
"message": "Applica"
},
"app.skins.delete-button": {
"message": "Elimina"
},
"app.skins.delete-modal.description": {
"message": "Questa skin sarà rimossa per sempre. Quest'azione non può essere annullata."
},
"app.skins.delete-modal.title": {
"message": "Vuoi davvero eliminare questa skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Non è stato possibile leggere il file trascinato."
},
"app.skins.dropped-file-error.title": {
"message": "Impossibile processare il file"
},
"app.skins.edit-button": {
"message": "Modifica"
},
"app.skins.modal.add-skin-button": {
"message": "Aggiungi"
},
"app.skins.modal.add-title": {
"message": "Aggiungi una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Modello"
},
"app.skins.modal.arm-style-slim": {
"message": "Snello"
},
"app.skins.modal.arm-style-wide": {
"message": "Largo"
},
"app.skins.modal.cape-fallback-name": {
"message": "Mantello"
},
"app.skins.modal.cape-section": {
"message": "Mantello"
},
"app.skins.modal.edit-title": {
"message": "Modifica la skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Modifica prima la skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nessun mantello"
},
"app.skins.modal.none-cape-option": {
"message": "Nessuno"
},
"app.skins.modal.replace-texture-button": {
"message": "Cambia"
},
"app.skins.modal.save-skin-button": {
"message": "Salva"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvando..."
},
"app.skins.modal.texture-section": {
"message": "Aspetto"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Carica prima una skin!"
},
"app.skins.preview.edit-button": {
"message": "Modifica"
},
"app.skins.previewing-badge": {
"message": "Anteprima"
},
"app.skins.rate-limit.text": {
"message": "Stai cambiando skin troppo spesso. I server di Mojang hanno limitato le nostre richieste. Aspetta un momento prima di riprovare."
},
"app.skins.rate-limit.title": {
"message": "Rallenta!"
},
"app.skins.reorder-error.text": {
"message": "L'ordine delle skin non è stato salvato."
},
"app.skins.reorder-error.title": {
"message": "Errore nel riordine delle skin"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Skin predefinite"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Pride Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Hai ottenuto queste skin per aver donato alla raccolta fondi per il Pride durante il Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Skin salvate"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Accedi"
},
"app.skins.sign-in.description": {
"message": "Accedi col tuo account Minecraft per poter gestire le skin qui."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emozionato"
},
"app.skins.sign-in.title": {
"message": "Effettua l'accesso"
},
"app.skins.title": {
"message": "Seleziona una skin"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
@@ -353,15 +602,6 @@
"app.update.complete-toast.title": {
"message": "La versione {version} è stata installata con successo!"
},
"app.update.download-update": {
"message": "Scarica aggiornamento"
},
"app.update.downloading-update": {
"message": "Scaricando l'aggiornamento ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ricarica per installare l'aggiornamento"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -384,7 +624,7 @@
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {richiesta} other {richieste}} d''amicizia"
"message": "{count, plural, one {# richiesta} other {# richieste}} d''amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
@@ -551,6 +791,30 @@
"instance.settings.tabs.general.name": {
"message": "Nome"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canale di aggiornamento"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Le versioni stabili, beta e alpha saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Le versioni stabili e beta saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stabile"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo le versioni stabili saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleziona il canale di aggiornamento"
},
"instance.settings.tabs.hooks": {
"message": "Hook di avvio"
},
@@ -600,16 +864,16 @@
"message": "Java e memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabili d'ambiente"
"message": "Personalizzate"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argomenti JVM"
"message": "Personalizzati"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Eseguibile di Java"
"message": "Personalizzata"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Memoria allocata"
"message": "Personalizzata"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Inserisci le variabili d'ambiente..."
@@ -674,6 +938,9 @@
"instance.worlds.copy_address": {
"message": "Copia indirizzo"
},
"instance.worlds.create_shortcut": {
"message": "Crea collegamento"
},
"instance.worlds.dont_show_on_home": {
"message": "Non mostrare nella home"
},
+86 -47
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} をダウンロード中"
},
"app.action-bar.downloading-update": {
"message": "アップデートをダウンロード中"
},
"app.action-bar.downloads": {
"message": "ダウンロード"
},
@@ -42,7 +45,7 @@
"message": "高度なレンダリング"
},
"app.appearance-settings.color-theme.description": {
"message": "Modrinth Appでのお好みのテーマを選択してください"
"message": "Modrinth Appでのお好みのテーマを選択してください"
},
"app.appearance-settings.color-theme.title": {
"message": "テーマ"
@@ -60,25 +63,25 @@
"message": "デフォルトの起動ページ"
},
"app.appearance-settings.hide-nametag.description": {
"message": "スキンページでプレイヤー名の上にある名前タグを無効にします。"
"message": "スキンページでプレイヤー上部のネームタグを非表示にします。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "名札を非表示にする"
"message": "ネームタグを非表示にする"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "ホームページの「ジャンプバック」セクションには、最近追加されたワールドが含まれています。"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "再び世界へ飛び込もう"
"message": "最近のワールドを表示"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minecraftが起動した時、ランチャーを最小化します"
"message": "Minecraftが起動した時、ランチャーを最小化します"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "ランチャーを最小化"
},
"app.appearance-settings.native-decorations.description": {
"message": "OSデフォルトのウィンドウUIを使用します(アプリの再起動が必要です)"
"message": "OSデフォルトのウィンドウUIを使用します。(アプリの再起動が必要です"
},
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
@@ -86,17 +89,23 @@
"app.appearance-settings.select-option": {
"message": "オプションを選択してください"
},
"app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。"
},
"app.appearance-settings.show-play-time.title": {
"message": "プレイ時間を表示"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "サイドバーの表示/非表示を切り替える機能を有効にします。"
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "サイドバーの表示/非表示を切り替える"
"message": "サイドバーの表示切替を有効化"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Modrinth上にホストされていないModrinth Packファイル.mrpack)をインストールしようとした場合、インストール前にそのリスクを理解していただけるよう確認します。"
"message": "Modrinthで配布されていないModrinth Pack.mrpack)をインストールする場合、事前にリスクについて警告します。"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "未知のMODパックインストールする前に警告してください"
"message": "不明なModパックインストール前に警告する"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraftの認証サーバーは現在停止している可能性があります。インターネット接続を確認し、しばらくしてからもう一度お試しください。"
@@ -131,12 +140,6 @@
"app.browse.server.installing": {
"message": "インストール中"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modパックをインストール中..."
},
"app.export-modal.description-placeholder": {
"message": "Modパックの説明を入力…"
},
@@ -206,12 +209,6 @@
"app.instance.worlds.browse-servers": {
"message": "サーバーを探す"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' は **完全に削除**され、元に戻すことはできません。"
},
"app.instance.worlds.delete-world-title": {
"message": "本当にこのワールドを完全に削除しますか?"
},
"app.instance.worlds.filter-modded": {
"message": "Mod導入済み"
},
@@ -230,21 +227,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "サーバー、ワールドは追加されていません"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' はゲーム内を含むサーバーリストから削除され、元に戻すことはできません。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) はゲーム内を含むサーバーリストから削除され、元に戻すことはできません。"
},
"app.instance.worlds.remove-server-title": {
"message": "本当に {name} を削除しますか?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "{count} 個のワールドを検索…"
},
"app.instance.worlds.this-server": {
"message": "このサーバー"
},
"app.modal.install-to-play.content-required": {
"message": "必須コンテンツ"
},
@@ -254,6 +239,9 @@
"app.modal.install-to-play.install-button": {
"message": "インストール"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "必須のModパック"
},
@@ -302,8 +290,65 @@
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} のインストール準備が整いました!今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
"app.skins.modal.arm-style-slim": {
"message": "スリム"
},
"app.skins.modal.arm-style-wide": {
"message": "ワイド"
},
"app.skins.modal.cape-fallback-name": {
"message": "マント"
},
"app.skins.modal.cape-section": {
"message": "マント"
},
"app.skins.modal.no-cape-tooltip": {
"message": "マントなし"
},
"app.skins.modal.replace-texture-button": {
"message": "アップロード"
},
"app.skins.modal.texture-section": {
"message": "テクスチャ"
},
"app.skins.preview.edit-button": {
"message": "編集"
},
"app.skins.section.builders-and-biomes": {
"message": "ビルダーズ&バイオーム"
},
"app.skins.section.chase-the-skies": {
"message": "チェイス・ザ・スカイ"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth プライド"
},
"app.skins.section.mounts-of-mayhem": {
"message": "マウント・オブ・メイヘム"
},
"app.skins.section.saved-skins": {
"message": "保存済みのスキン"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "銅の時代"
},
"app.skins.section.the-garden-awakens": {
"message": "ガーデン・アウェイケンズ"
},
"app.skins.section.tiny-takeover": {
"message": "タイニー・テイクオーバー"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "ワクワクしている Modrinth ボット"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} のダウンロードが完了しました。今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
@@ -323,9 +368,6 @@
"app.update-popup.download-complete": {
"message": "ダウンロード完了"
},
"app.update-popup.reload": {
"message": "再読み込み"
},
"app.update-popup.title": {
"message": "アップデートが利用可能です"
},
@@ -335,15 +377,6 @@
"app.update.complete-toast.title": {
"message": "バージョン {version} が正常にインストールされました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
},
"app.update.downloading-update": {
"message": "アップデートをダウンロード中 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "再起動して今すぐ更新"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -365,6 +398,9 @@
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
@@ -593,6 +629,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "メモリ割り当て"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "ウィンドウ"
},

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