83 Commits

Author SHA1 Message Date
c60cf79a72 Update README.md 2025-12-28 17:46:36 +00:00
ddf51c9596 Bump application version
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 42m38s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-11-29 00:19:47 +03:00
a63b6b27d5 Fix launcher GUI not loaded 2025-11-29 00:19:19 +03:00
cac3b46652 Merge tag 'v0.10.21' into beta 2025-11-28 22:23:28 +03:00
aecsocket
fe684ab903 Change auth servers reachable check URL (#4830) 2025-11-27 19:38:26 +00:00
Calum H.
8592761493 fix: stale modpack permissions data on re-review (#4822) 2025-11-27 17:15:10 +00:00
aecsocket
dfe087df20 Enforce 2dp on payout withdrawals (#4829)
* fix mural withdraw amount

* Enforce 2dp on all payout logic
2025-11-27 10:03:34 +00:00
82119a9fc9 Bump application version
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 37m53s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-11-27 05:09:45 +03:00
b9ec1b42dc Merge tag 'v0.10.20' into beta 2025-11-27 05:08:45 +03:00
7345fa401b Add patch file 2025-11-27 04:51:21 +03:00
aecsocket
be3208c5a1 Fix Docker Compose to match staging/prod environments (#4828) 2025-11-26 23:54:03 +00:00
Calum H.
b56f39ce07 fix: edit server icon issues (#4821) 2025-11-26 23:29:29 +00:00
aecsocket
0178fddc38 Install mod update dependencies automatically (#4800)
* Redownload version dependencies when updating a mod

* Fix update all button as well
2025-11-24 13:35:14 +00:00
aecsocket
31417a2aa1 more logging on sync payouts task (#4814) 2025-11-23 21:48:15 +01:00
Truman Gao
f333a75221 fix empty state for projects in "All" tab (#4801)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-23 02:38:13 +00:00
aecsocket
bcf14a4c51 Fix downloading libraries for Forge 1.7.2 (#4808)
* wip: fix Forge 1.7.2 downloads

* Bump recursion limit
2025-11-21 22:45:34 +00:00
aecsocket
130c2863ab Fix exposing Docker Compose ports to broadcast addr (#4805) 2025-11-21 11:13:11 +00:00
Prospector
e59664426b changelog 2025-11-19 15:17:01 -08:00
aecsocket
2f0ef07944 Add logging and change limit of Mural payouts task (#4798) 2025-11-19 12:38:30 +00:00
Truman Gao
9af19d01e5 Fix modrinth+ firing ad requests on load (#4792) 2025-11-18 18:05:24 +00:00
François-Xavier Talbot
e837d9fa30 Add route to reprocess a refund charge's tax record (#4791) 2025-11-18 11:36:55 +00:00
aecsocket
93b79759c7 Add auth servers unreachable warning to app (#4774)
* Add auth servers unreachable warning to app

* Check auth status every 5 minutes

* Use admonition in auth server warning

* feat: tanstack

* Fix auth server reachability query

* Format

* intl extract

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2025-11-17 18:41:52 +00:00
Modrinth Bot
4becb2a822 New translations from Crowdin (main) (#4787) 2025-11-17 07:45:40 +00:00
Calum H.
134a621d0d fix: use ref rather than direct val (#4785) 2025-11-16 22:36:11 +00:00
aecsocket
089cca60ce Fix PayPal SSO OAuth callback (#4758)
* Maybe fix PayPal SSO

* cargo sqlx prepare

* maybe works

* Attempt 2 of fixing

* Fix vue

* Try adding more logging to flow
2025-11-16 21:49:48 +00:00
Prospector
20484ed7aa fix timezone 2025-11-14 12:10:58 -08:00
Prospector
763a38812f changelog 2025-11-14 11:58:48 -08:00
Calum H.
7ccc32675b feat: start of cross platform page system (#4731)
* feat: abstract api-client DI into ui package

* feat: cross platform page system

* feat: tanstack as cross platform useAsyncData

* feat: archon servers routes + labrinth billing routes

* fix: dont use partial

* feat: migrate server list page to tanstack + api-client + re-enabled broken features!

* feat: migrate servers manage page to api-client before page system

* feat: migrate manage page to page system

* fix: type issues

* fix: upgrade wrapper bugs

* refactor: move state types into api-client

* feat: disable financial stuff on app frontend

* feat: finalize cross platform page system for now

* fix: lint

* fix: build issues

* feat: remove papaparse

* fix: lint

* fix: interface error

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-14 17:15:09 +00:00
Truman Gao
26feaf753a Fixes #4348 (#4773)
* also fixes spacing issue in collections card small width

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-13 23:51:08 +00:00
Prospector
94c0003c19 Fix a number of light mode issues and get rid of scrollbar jumping on menus (#4760)
* Fix DEV-466, Fixes #4692 as well as a bunch of other poor contrast and inconsistency issues in light mode. Adds shadows to buttons and makes scrollbar gutter stable.

* lintttt & only do scrollbar gutter on website

* try to fix following hydration issue

* try another clientonly approach

* fix home page link animation

* lint

* remove dropdown style from checkbox & improve shadow consistency

* liiiint
2025-11-13 23:21:43 +00:00
aecsocket
c27f787c91 Task to retroactively update Mural statuses (#4769)
* Task to retroactively update Mural statuses

* cargo sqlx prepare

* wip: add tests

* Prepare

* Fix up test

* start on muralpay mock

* Move mocking to muralpay crate
2025-11-13 18:16:41 +00:00
Calum H.
70e2138248 feat: base api-client impl (#4694)
* feat: base api-client impl

* fix: doc

* feat: start work on module stuff

* feat: migrate v2/v3 projects into module system

* fix: lint & README.md contributing

* refactor: remove utils old api client prototype

* fix: lint

* fix: api url issues

* fix: baseurl in error.vue

* fix: readme

* fix typo in readme

* Update apps/frontend/src/providers/api-client.ts

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

* Update packages/api-client/src/features/verbose-logging.ts

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

* Update packages/api-client/src/features/retry.ts

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

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 20:29:12 +00:00
Calum H.
590ba3ce55 fix: original startup settings values not being updated with new state we send to server (#4761) 2025-11-12 19:45:51 +00:00
Ksawier Wilczynski
29671347a0 fix: correct parameter name for create function in profile helper (#4744)
Co-authored-by: Panyu <48863527+PanyuDev@users.noreply.github.com>
2025-11-12 05:43:10 +00:00
Calum H.
386e6e50da refactor: move batch credit modal into default layout (#4767)
* refactor: move batch credit modal into default layout

* fix: spacing + spans rather than p
2025-11-12 04:14:11 +00:00
Calum H.
880ed21bcd fix: incorrect autocomplete for pardon_ip and ban_ip (#4763) 2025-11-12 04:01:25 +00:00
Calum H.
bbc31ef077 fix: z index issues for mobile nav (#4766)
* fix: z index issues for mobile nav

Closes: #4722

* fix: below modals

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-12 04:00:46 +00:00
Calum H.
9a13e977a0 fix: crash on charges.vue (#4765) 2025-11-12 04:00:39 +00:00
Modrinth Bot
a5602ff18c New translations from Crowdin (main) (#4749) 2025-11-11 23:11:36 +00:00
Prospector
5901c5a535 update changelog 2025-11-11 12:28:24 -08:00
Jonathan Romano
cca1dd7e37 Update openapi spec to fix invalid json (#4756)
Signed-off-by: Jonathan Romano <jonathan@luxaritas.com>
2025-11-11 02:00:26 +00:00
Prospector
127e01cc96 changelog 2025-11-10 11:58:39 -08:00
aecsocket
1dcb38cb57 Fix dependency installation not respecting mod loader (#4751)
* Fix dependency installation not respecting mod loader

* fix
2025-11-10 16:48:11 +00:00
aecsocket
98b4970680 Store method ID for payouts (#4752)
* Store method ID for payouts

* Fixes
2025-11-10 16:41:06 +00:00
aecsocket
9706f1597b Supporting documents for Mural payouts (#4721)
* wip: gotenberg

* Generate and provide supporting docs for Mural payouts

* Correct docs

* shear

* update cargo lock because r-a complains otherwise

* Remove local Gotenberg queue and use Redis instead

* Store platform_id in database correctly

* Address PR comments

* Fix up CI

* fix rebase

* Add timeout to default env vars
2025-11-08 23:27:31 +00:00
aecsocket
f8a5a77daa Expose test utils to Labrinth dependents (#4703)
* Expose test utils to Labrinth dependents

* Feature gate `labrinth::test`

* Unify db migrators

* Expose `NotificationBuilder::insert_many_deliveries`

* Add logging utils to common crate

* Remove unused console-subscriber layer

* fix CI
2025-11-08 20:26:24 +00:00
Prospector
1efdceacfd changelog 2025-11-07 21:06:54 -08:00
Calum H.
b998c71337 feat: add skript + mcfunction highlightjs support (#4739)
* feat: add skript + mcfunction highlightjs support

* fix: lint

* fix: dep

* lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-08 02:24:08 +00:00
Prospector
6a6adb3480 Updated changelog 2025-11-07 15:38:23 -08:00
Prospector
a694aeed32 Add frontend support for Geyser Extension plugin loader (#4735) 2025-11-07 19:55:16 +00:00
Prospector
8182b795de Fix resource pack projects with multiple loaders not finding a release in Download modal (#4734) 2025-11-07 19:54:59 +00:00
aecsocket
608ab988f0 Fetch more data for moderation endpoints (#4727)
* Moderation endpoints fetch ownership data

* fix up endpoint configs

* add some docs
2025-11-07 18:50:29 +00:00
Prospector
a261598e89 changelog 2025-11-07 09:26:51 -08:00
Prospector
11a1918a2e fix advanced rendering toggle on web with NewModal Closes #2284 (#4733) 2025-11-07 16:47:54 +00:00
Airyzz
67fb825937 Make major box shadows toggleable with Advanced rendering setting (#4712)
* Update App.vue

* Update App.vue

* tone down light mode shadows, disable with advanced rendering disabled

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-07 16:35:55 +00:00
Prospector
4289f8b52d changelog 2025-11-07 08:01:12 -08:00
Prospector
fb1ba51a2b Revert "Reapply "fix: firefox backup download issues (#4679)" (#4683) (#4704)"
This reverts commit b11934054d.
2025-11-07 07:45:12 -08:00
aecsocket
cb47bc97c7 Logging hotfix for canceling Mural payout requests (#4730)
* Logging hotfix for canceling payout requests

* Remove Tombi CI step for now
2025-11-07 12:07:10 +01:00
Modrinth Bot
06e1bc9dd6 New translations from Crowdin (main) (#4548) 2025-11-07 10:36:06 +00:00
Truman Gao
af39a1769c Fixes on small frontend bugs (#4719)
* Account list is not scrollable
Fixes #4688

* Selecting Glitch in the log Screen
Fixes #4687 by explicitly defining the buffer

* When sorting or grouping your instance, the option you choose does not get saved
Fixes #4647

* use label prop to specify specific local storage for grid display state

* Implement persistent filters on mods page
Fixes #4517

* fix lint errors

* update schemastore links

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-11-07 07:56:00 +00:00
Calum H.
60ffa75653 feat: 2nd batch of withdraw QA changes (#4724)
* polish: increase gap between svg and text in empty state

* fix: use ts & change cancel btn style

* fix: btn style

* polish: new transaction page design

* fix: navstack match nested + csv download

* fix: lint & i18n

* Add tooltip to CSV download button + standard btn style

Signed-off-by: Calum H. <contact@cal.engineer>

* fix: lint & i18n

---------

Signed-off-by: Calum H. <contact@cal.engineer>
2025-11-06 21:55:07 +00:00
Jerozgen
7674433f88 Improve nags translation strings (#4693) 2025-11-06 10:37:37 +00:00
François-Xavier Talbot
7437a833ef Fix payout notifications (#4707)
* Add limit to payouts_values_notifications synchronizer

* Set payout notification threshold to $1

* Fix formatting

* Query cache
2025-11-05 19:43:59 +00:00
Prospector
1bad1a57b0 changelog 2025-11-04 18:29:18 -08:00
Prospector
3437387885 Fix fee amount for Tremendous PayPal (#4720) 2025-11-04 17:51:07 -08:00
aecsocket
23d098eee5 Fix error chain logging and withdrawal fees (#4718)
* Log Labrinth errors properly

* Tweak how we do Tremendous fees

* Fix maths for Tremendous fees
2025-11-04 17:50:54 -08:00
Prospector
4636372ff4 changelog 2025-11-04 16:13:51 -08:00
Prospector
4592786de8 Update withdraw blog post now that it's live. (#4715) 2025-11-04 16:12:11 -08:00
Calum H.
f054f39c5d polish: withdraw flow fixes (#4713)
* fix: negative value stuff

* fix: mobile responsiveness for modal min-w

* feat: better error handling on withdraw

* fix: empty state positioning + svg sizing

* fix: title case -> sentence case

* fix: re-add virtual visa under gift cards

* fix: hide <1% segments
2025-11-04 21:29:47 +00:00
aecsocket
6e47de06bb Address withdrawal QA changes (#4711)
* Add Mural to balance monitoring

* Add back Visa prepaid Tremendous cards

* cargo sqlx prepare
2025-11-04 16:40:15 +00:00
aecsocket
c38751a38a cargo sqlx prepare (#4710) 2025-11-04 01:34:39 +01:00
aecsocket
2d218d79c6 Mural fixes (#4709) 2025-11-04 01:12:30 +01:00
Prospector
5a41a35716 fix affiliate link mistake 2025-11-03 15:45:49 -08:00
Prospector
644554f1e9 changelog 2025-11-03 15:15:53 -08:00
Calum H.
3765a6ded8 feat: creator revenue page overhaul (#4204)
* feat: start on tax compliance

* feat: avarala1099 composable

* fix: shouldShow should be managed on the page itself

* refactor: move show logic to revenue page

* feat: security practices rather than info

* feat: withdraw page lock

* fix: empty modal bug & lint issues

* feat: hide behind feature flag

* Use standard admonition components, make casing consistent

* modal title

* lint

* feat: withdrawal check

* feat: tax cap on withdrawals warning

* feat: start on revenue page overhaul

* feat: segment generation for bar

* feat: tooltips and links

* fix: tooltip border

* feat: finish initial layout, start on withdraw modal

* feat: start on withdrawal limit stage

* feat: shade support for primary colors

* feat: start on withdraw details stage

* fix: convert swatches to hex

* feat: payout method/region dropdown temporarily using multiselect

* feat: fix modal open issues and use teleport dropdowns

* feat: hide transactions section if there are no transactions

* refactor: NavStack surfaces

* feat: new dropdown component

* feat: remove teleport dropdown modal in favour of new combobox component

* fix: lint

* refactor: dashboard sidebar layout

* feat: cleanup

* fix: niche bugs

* fix: ComboBox styling

* feat: first part of qa

* feat: animate flash rather than tooltip

* fix: lint

* feat: qa border gradient

* fix: seg hover flashes

* feat: i18n

* feat: i18n and final QA

* fix: lint

* feat: QA

* fix: lint

* fix: merge conflicts

* fix: intl

* fix: blue hover

* fix: transfers page

* feat: surface variables & gradients

* feat: text vars

* fix: lint

* fix: intl

* feat: stages

* fix: lint

* feat: region selection

* feat: method selection btns

* fix: flex col on transactions

* feat: hook up method selection to ctx

* feat: muralpay kyc stage info

* wip: muralpay integration

* Basic Mural Pay API bindings

* Fix clippy

* use dotenvy in muralpay example

* Refactor payout creation code

* wip: muralpay payout requests

* Mural Pay payouts work

* Fix clippy

* feat: progress

* fix: broken tax form stage logic

* polish: tax form stage and method selection stage layout

* add mural pay fees API

* Work on payout fee API

* Fees API for more payment methods

* Fix CI

* polish: muralpay qa

* refactor: clean up combobox component

* polish: change from critical -> warning admonition in MuralpayDetailsStage

* Temporarily disable Venmo and PayPal methods from frontend

* polish: clean up transaction component & page

* polish: navbar qa, text color-contrast in chips type buttonstyled, mb on rev/index.vue page

* fix: incorrectly using available balance as tax form withdraw limit after tax forms submitted

* wip: counterparties

* Start on counterparties and payment methods API

* polish: combobox component

* polish: fix broken scroll logic using a composable & web:fix

* fix: lint

* polish: various QA fixes

* feat: hook up with backend (wip)

* feat: draft muralpay rails dynamic logic

* polish: modify rails to support backend changes

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* feat: fees & methods endpoint hookup

* chore: remove duplicates fix

* polish: qa changes + figma match

* Add countries to muralpay fiat methods

* Compile fix

* Add exchange rate info to fees endpoint

* Add fees to premium Tremendous options

* polish: i18n and better document type dropdown -> id input labels

* feat: tremendous

* fix: lint & i18n

* feat: reintroduce tin mismatch logic to index.vue

* polish: qa

* fix: i18n

* feat: remove teleport dropdown menu - combobox should be used

* fix: lint

* fix: jsdoc

* feat: checkbox for reward program terms

* Add delivery email field to Tremendous payouts

* Add Tremendous product category to payout methods

* Add bank details API to muralpay

* Fix CI

* Fix CI

* polish: qa changes

* feat: i18n pass

* feat: deduplicate methods endpoint & fix i18n issues

* chore: deduplicate i18n strings into common-messages.ts

* fix: lint

* fix: i18n

* feat: estimates

* polish: more QA

* Remove prepaid visa, compute fees properly for Tremendous methods

* Add more details to Tremendous errors

* feat: withdraw endpoint impl & internals refactor

* Add more details to Tremendous errors

* feat: completion stage

* Add fees to Mural

* feat: transactions page match figma

* fix: i18n

* polish: QA changes

* polish: qa

* Payout history route and bank details

* polish: autofill and requirements checks

* fix: i18n + lint

* fix: fiat rail fees

* polish: move scroll fade stuff into NewModal rather than just CreatorWithdrawModal

* feat: simplify action btn logic & tax form error

* fix: tax -> Tax form

* Re-add legacy PayPal/Venmo options for US

* feat: mobile responsiveness fixes for modal

* fix: responsiveness issues

* feat: navstack responsiveness

* fix: responsiveness

* move the mural bank details route

* fix: generated state cleanup & bank details input

* fix: lint & i18n

* Add utoipa support to payout endpoints

* address some PR comments

* polish: qa

* add CORS to new utoipa routes

* feat: legacy paypal/venmo stage

* polish: reset amount on back qa

* revert: navstack mr changes

* polish: loading indicator on method selection stage

* fix: paypal modal doesnt reopen after auth

* fix: lint & i18n

* fix: paypal flow

* polish: qa changes

* fix: gitignore

* polish: qa fixes

* fix: payouts_available in payouts.rs

* fix: bug when limit is zero

* polish: qa changes

* fix: qa stuff & muralpay sub-division fix

* Immediately approve mural payouts

* Add currency support to Tremendous payouts

* Currency forex

* add forex to tremendous fee request

* polish: qa & currency support for paypal tremendous

* polish: fx qa

* feat: demo mode flag

* fix: i18n & padding issues

* polish: qa changes

* fix: ml

* Add Mural balance to bank balance info

* polish: show warning for paypal international USD withdrawals + more currencies

* Add more Tremendous currencies support

* fix: colors on balance bars

* fix: empty states

* fix: pl-8 mobile issue

* fix: hide see all

* Transaction payouts available use the correct date

* Address my own review comment

* Address PR comments

* Change Mural withdrawal limit to 3k

* fix: empty state + paypal warning

* maybe fix tremendous gift cards

* Change how Mural minimum withdrawals are calculated

* Tweak min/max withdrawal values

* fix: segment brightness

* fix: min & max for muralpay & legacy paypal

* Fix some icon issues

* more issues

* fix user menu

* fix: remove + network

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: aecsocket <aecsocket@tutanota.com>
Co-authored-by: Alejandro González <me@alegon.dev>
2025-11-03 15:15:25 -08:00
François-Xavier Talbot
92698e4bb5 Update tax change notification timings (#4706) 2025-11-03 22:15:16 +00:00
aecsocket
17f395ee55 Mural Pay integration (#4520)
* wip: muralpay integration

* Basic Mural Pay API bindings

* Fix clippy

* use dotenvy in muralpay example

* Refactor payout creation code

* wip: muralpay payout requests

* Mural Pay payouts work

* Fix clippy

* add mural pay fees API

* Work on payout fee API

* Fees API for more payment methods

* Fix CI

* Temporarily disable Venmo and PayPal methods from frontend

* wip: counterparties

* Start on counterparties and payment methods API

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* Add countries to muralpay fiat methods

* Compile fix

* Add exchange rate info to fees endpoint

* Add fees to premium Tremendous options

* Add delivery email field to Tremendous payouts

* Add Tremendous product category to payout methods

* Add bank details API to muralpay

* Fix CI

* Fix CI

* Remove prepaid visa, compute fees properly for Tremendous methods

* Add more details to Tremendous errors

* Add fees to Mural

* Payout history route and bank details

* Re-add legacy PayPal/Venmo options for US

* move the mural bank details route

* Add utoipa support to payout endpoints

* address some PR comments

* add CORS to new utoipa routes

* Immediately approve mural payouts

* Add currency support to Tremendous payouts

* Currency forex

* add forex to tremendous fee request

* Add Mural balance to bank balance info

* Add more Tremendous currencies support

* Transaction payouts available use the correct date

* Address my own review comment

* Address PR comments

* Change Mural withdrawal limit to 3k

* maybe fix tremendous gift cards

* Change how Mural minimum withdrawals are calculated

* Tweak min/max withdrawal values

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
2025-11-03 14:19:46 -08:00
Prospector
b11934054d Reapply "fix: firefox backup download issues (#4679)" (#4683) (#4704)
This reverts commit 4c1020d2ba.
2025-11-02 19:51:48 +00:00
Prospector
40cbe92dbc Affiliates frontend (#4380)
* Begin affiliates frontend

* Significant work on hooking up affiliates ui

* Clean up server nodes menu

* affiliates work

* update affiliate time

* oops

* fix local import

* fix local import x2

* remove line in dashboard

* lint
2025-11-02 19:32:18 +00:00
aecsocket
b7f0988399 Decouple project deletion from thread deletion (#4673)
* Decouple project deletion from thread deletion

* Allow a thread to exist without a project

* attempt 2

* Modify migration to set orphaned threads' mods to NULL instead of removing constraint entirely

* Use mod PAT for mod threads
2025-10-31 19:04:01 +00:00
Prospector
4c1020d2ba Revert "fix: firefox backup download issues (#4679)" (#4683)
This reverts commit c74460fffa.
2025-10-31 02:36:52 -07:00
thedarkcolour
00f9cf0e2c Fix inconsistent PAT display order (#4662)
* Fix inconsistent PAT display order
Closes #4661

* Fix side effect in computed property

* Fix lint

---------

Co-authored-by: Calum H. <contact@cal.engineer>
2025-10-30 23:28:18 +00:00
Prospector
1dd7e3bcdc add changelog 2025-10-30 16:05:31 -07:00
541 changed files with 74292 additions and 9119 deletions

View File

@@ -1,9 +1,6 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
[target.'cfg(windows)']
rustflags = ["--cfg", "tokio_unstable", "-C", "link-args=/STACK:16777220"]
rustflags = ["-C", "link-args=/STACK:16777220"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"

View File

@@ -9,6 +9,7 @@ Please follow these rules precisely:
1. Identify translatable strings
- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.
- There may be strings within the <script> block, e.g dropdown option labels, notifications etc.
2. Create message definitions

3
.gitignore vendored
View File

@@ -65,3 +65,6 @@ app-playground-data/*
.astro
.claude
# labrinth demo fixtures
apps/labrinth/fixtures/demo

View File

@@ -13,7 +13,7 @@
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@@ -57,3 +57,7 @@ Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse
### Postgres
Use `docker exec labrinth-postgres psql -U postgres` to access the PostgreSQL instance.
# Guidelines
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to.

1307
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ actix-rt = "2.11.0"
actix-web = "4.11.0"
actix-web-prom = "0.10.0"
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 }
@@ -54,7 +55,6 @@ clap = "4.5.48"
clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
console-subscriber = "0.4.1"
const_format = "0.2.34"
daedalus = { path = "packages/daedalus" }
dashmap = "6.1.0"
@@ -109,6 +109,7 @@ maxminddb = "0.26.0"
meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-maxmind = { path = "packages/modrinth-maxmind" }
modrinth-util = { path = "packages/modrinth-util" }
muralpay = { path = "packages/muralpay" }
murmur2 = "0.1.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
@@ -139,6 +140,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
secrecy = "0.10.3"
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
"contexts",
@@ -161,6 +163,7 @@ sha2 = "0.10.9"
shlex = "1.3.0"
spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
strum = "0.27.2"
sysinfo = { version = "0.37.2", default-features = false }
tar = "0.4.44"
tauri = "2.8.5"
@@ -246,6 +249,7 @@ redundant_clone = "warn"
redundant_feature_names = "warn"
redundant_type_annotations = "warn"
todo = "warn"
too_many_arguments = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"

122
README.md
View File

@@ -1,119 +1,11 @@
# 📘 Navigation
RocketMC
- [🔧 Install Instructions](#install-instructions)
- [✨ Features](#features)
- [🚀 Getting Started](#getting-started)
- [⚠️ Disclaimer](#disclaimer)
- [💰 Donate](#support-our-project-crypto-wallets)
Based on AstralRinth
https://github.com/AstralRinth
## Other languages
> [Русский](readme/ru_ru/README.md)
AstralRinth is based on MultiMC
Copyright © MultiMC Contributors
## Support channel
> [Telegram](https://me.astralium.su/ref/telegram_channel)
RocketMC modifications © 2025 Pernoise
---
# About Project
## **AstralRinth • Empowering Your Minecraft Experience**
**AstralRinth** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinths API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
## **About the Software**
**AstralRinth** is a dedicated branch of the Modrinth (a.k.a Theseus) project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
---
# Install Instructions
To install the launcher:
1. Visit the [releases page](https://git.astralium.su/didirus/AstralRinth/releases) to download the correct version for your system.
2. Run the downloaded file or extract and launch it, depending on the format.
### Downloadable File Extensions
| Extension | OS | Notes |
| --------- | ------- | --------------------------------------------------------------------- |
| `.msi` | Windows | Supported on all recent Windows versions (10/11) |
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia, Tahoe _(may also support older versions)_ |
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
### Installation Warnings
Avoid using builds with these prefixes — they may be unstable or experimental:
- `dev`
- `nightly`
- `dirty`
- `dirty-dev`
- `dirty-nightly`
- `dirty_dev`
- `dirty_nightly`
---
# Features
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
## Included exclusive features
- No ads in the entire launcher.
- Custom `.svg` vector icons for a distinct UI.
- Improved compatibility with both licensed and pirate accounts.
- Use **official microsoft accounts** or **offline/pirate accounts**.
- Supports license-free access for testing or personal use.
- No dependence on official authentication services.
- Discord Rich Presence integration:
- Dynamic status messages.
- In-game timer and AFK counter.
- Strict disabling of statistics and other Modrinth metrics.
- Optimized archive/package size.
- Integrated update fetcher for seamless version management.
- Built-in update alerts for new versions posted on Git Astralium.
- Automatic download and installation capabilities.
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
- Ely.by full integration
- The official account skin system is managed by ely.by
- Offline accounts must install AuthLib through the instance settings
---
# Getting Started
To begin using AstralRinth:
1. **Download Latest Release**
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
- [How to choose a file](#downloadable-file-extensions)
- [How to choose a release](#installation-warnings)
2. **Log in or create new offline account**
- Use your official Microsoft account (MSA), or test using a non-licensed account (Offline).
3. **Launch Minecraft**
- Start Minecraft from the launcher.
- The launcher will auto-detect the recommended JVM version.
- You can also configure Java manually in the settings.
---
# Disclaimer
- **AstralRinth** is intended **solely for educational and experimental use**.
- We **do not condone piracy** — users are encouraged to purchase a legitimate Minecraft license.
- Respect all relevant licensing agreements and support Minecraft developers.
---
# Support Our Project (Crypto Wallets)
If you'd like to support development, you can donate via the following crypto wallets:
- Toncoin (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
- USDT (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
Licensed under the GNU General Public License v3.0

View File

@@ -9,7 +9,7 @@ extend-exclude = [
# contains licenses like `CC-BY-ND-4.0`
"packages/moderation/src/data/stages/license.ts",
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
"apps/labrinth/src/queue/payouts.rs",
"apps/labrinth/src/queue/payouts/mod.rs",
]
[default.extend-words]

View File

@@ -13,11 +13,13 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@tanstack/vue-query": "^5.90.7",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-http": "^2.5.0",
@@ -41,9 +43,9 @@
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@modrinth/tooling-config": "workspace:*",
"@eslint/compat": "^1.1.1",
"@formatjs/cli": "^6.2.12",
"@modrinth/tooling-config": "workspace:*",
"@nuxt/eslint-config": "^0.5.6",
"@taijased/vue-render-tracker": "^1.0.7",
"@vitejs/plugin-vue": "^5.0.4",

View File

@@ -1,4 +1,5 @@
<script setup>
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
@@ -18,12 +19,14 @@ import {
RefreshCwIcon,
RestoreIcon,
RightArrowIcon,
ServerIcon,
SettingsIcon,
UserIcon,
WorldIcon,
XIcon,
} from '@modrinth/assets'
import {
Admonition,
Avatar,
Button,
ButtonStyled,
@@ -32,9 +35,12 @@ import {
NotificationPanel,
OverflowMenu,
ProgressSpinner,
provideNotificationManager
provideModrinthClient,
provideNotificationManager,
useDebugLogger,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
@@ -63,7 +69,8 @@ import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
@@ -97,6 +104,16 @@ const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
const { handleError, addNotification } = notificationManager
const tauriApiClient = new TauriModrinthClient({
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
features: [
new AuthFeature({
token: async () => (await getCreds()).session,
}),
],
})
provideModrinthClient(tauriApiClient)
const news = ref([])
const availableSurvey = ref(false)
@@ -121,6 +138,27 @@ const criticalErrorMessage = ref()
const isMaximized = ref(false)
const authUnreachableDebug = useDebugLogger('AuthReachableChecker')
const authServerQuery = useQuery({
queryKey: ['authServerReachability'],
queryFn: async () => {
await check_reachable()
authUnreachableDebug('Auth servers are reachable')
return true
},
refetchInterval: 5 * 60 * 1000, // 5 minutes
retry: false,
refetchOnWindowFocus: false,
})
const authUnreachable = computed(() => {
if (authServerQuery.isError.value && !authServerQuery.isLoading.value) {
console.warn('Failed to reach auth servers', authServerQuery.error.value)
return true
}
return false
})
onMounted(async () => {
await useCheckDisableMouseover()
await getRemote(false) // [AR] Check for updates
@@ -156,6 +194,15 @@ const messages = defineMessages({
id: 'app.update.downloading-update',
defaultMessage: 'Downloading update ({percent}%)',
},
authUnreachableHeader: {
id: 'app.auth-servers.unreachable.header',
defaultMessage: 'Cannot reach authentication servers',
},
authUnreachableBody: {
id: 'app.auth-servers.unreachable.body',
defaultMessage:
'Minecraft authentication servers may be down right now. Check your internet connection and try again later.',
},
})
async function setupApp() {
@@ -315,7 +362,11 @@ const handleClose = async () => {
const router = useRouter()
router.afterEach((to, from, failure) => {
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
trackEvent('PageView', {
path: to.path,
fromPath: from.path,
failed: failure,
})
})
const route = useRoute()
@@ -339,7 +390,7 @@ async function fetchCredentials() {
if (creds && creds.user_id) {
creds.user = await get_user(creds.user_id).catch(handleError)
}
credentials.value = creds
credentials.value = creds ?? null
}
async function signIn() {
@@ -570,7 +621,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<template>
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<div id="teleports"></div>
<div v-if="stateInitialized" class="app-grid-layout experimental-styles-within relative">
<div
v-if="stateInitialized"
class="app-grid-layout experimental-styles-within relative"
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
>
<Suspense>
<AppSettingsModal ref="settingsModal" />
</Suspense>
@@ -589,6 +644,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<NavButton v-if="themeStore.featureFlags.worlds_tab" v-tooltip.right="'Worlds'" to="/worlds">
<WorldIcon />
</NavButton>
<NavButton
v-if="themeStore.featureFlags.servers_in_app"
v-tooltip.right="'Servers'"
to="/servers/manage"
>
<ServerIcon />
</NavButton>
<NavButton
v-tooltip.right="'Discover content'"
to="/browse/modpack"
@@ -612,7 +674,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
>
<LibraryIcon />
</NavButton>
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
<suspense>
<QuickInstanceSwitcher />
</suspense>
@@ -773,7 +835,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<div
v-if="stateInitialized"
class="app-contents experimental-styles-within"
:class="{ 'sidebar-enabled': sidebarVisible }"
:class="{
'sidebar-enabled': sidebarVisible,
'disable-advanced-rendering': !themeStore.advancedRendering,
}"
>
<div class="app-viewport flex-grow router-view">
<transition name="popup-survey">
@@ -821,16 +886,25 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
width: 'calc(100% - var(--right-bar-width))',
}"
></div>
<div
<Admonition
v-if="criticalErrorMessage"
class="m-6 mb-0 flex flex-col border-red bg-bg-red rounded-2xl border-2 border-solid p-4 gap-1 font-semibold text-contrast"
type="critical"
:header="criticalErrorMessage.header"
class="m-6 mb-0"
>
<h1 class="m-0 text-lg font-extrabold">{{ criticalErrorMessage.header }}</h1>
<div
class="markdown-body text-primary"
v-html="renderString(criticalErrorMessage.body ?? '')"
></div>
</div>
</Admonition>
<Admonition
v-if="authUnreachable"
type="warning"
:header="formatMessage(messages.authUnreachableHeader)"
class="m-6 mb-0"
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
@@ -1032,7 +1106,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
.app-sidebar::before {
content: '';
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.1) inset;
top: 0;
bottom: 0;
left: -2rem;
@@ -1057,9 +1131,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
right: calc(-1 * var(--left-bar-width));
bottom: calc(-1 * var(--left-bar-width));
border-radius: var(--radius-xl);
box-shadow:
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.1) inset;
border-color: var(--surface-5);
border-width: 1px;
border-style: solid;
pointer-events: none;
}

View File

@@ -12,6 +12,7 @@ import {
} from '@modrinth/assets'
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
@@ -121,40 +122,50 @@ const handleOptionsClick = async (args) => {
}
}
const state = useStorage(
`${props.label}-grid-display-state`,
{
group: 'Group',
sortBy: 'Name',
},
localStorage,
{ mergeDefaults: true },
)
const search = ref('')
const group = ref('Group')
const sortBy = ref('Name')
const filteredResults = computed(() => {
const { group = 'Group', sortBy = 'Name' } = state.value
const instances = props.instances.filter((instance) => {
return instance.name.toLowerCase().includes(search.value.toLowerCase())
})
if (sortBy.value === 'Name') {
if (sortBy === 'Name') {
instances.sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
if (sortBy.value === 'Game version') {
if (sortBy === 'Game version') {
instances.sort((a, b) => {
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
})
}
if (sortBy.value === 'Last played') {
if (sortBy === 'Last played') {
instances.sort((a, b) => {
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
})
}
if (sortBy.value === 'Date created') {
if (sortBy === 'Date created') {
instances.sort((a, b) => {
return dayjs(b.date_created).diff(dayjs(a.date_created))
})
}
if (sortBy.value === 'Date modified') {
if (sortBy === 'Date modified') {
instances.sort((a, b) => {
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
})
@@ -162,7 +173,7 @@ const filteredResults = computed(() => {
const instanceMap = new Map()
if (group.value === 'Loader') {
if (group === 'Loader') {
instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.loader)
if (!instanceMap.has(loader)) {
@@ -171,7 +182,7 @@ const filteredResults = computed(() => {
instanceMap.get(loader).push(instance)
})
} else if (group.value === 'Game version') {
} else if (group === 'Game version') {
instances.forEach((instance) => {
if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.game_version, [])
@@ -179,7 +190,7 @@ const filteredResults = computed(() => {
instanceMap.get(instance.game_version).push(instance)
})
} else if (group.value === 'Group') {
} else if (group === 'Group') {
instances.forEach((instance) => {
if (instance.groups.length === 0) {
instance.groups.push('None')
@@ -199,7 +210,7 @@ const filteredResults = computed(() => {
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
if (sortBy.value === 'Name') {
if (sortBy === 'Name') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
// None should always be first
if (a[0] === 'None' && b[0] !== 'None') {
@@ -217,7 +228,7 @@ const filteredResults = computed(() => {
}
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
if (group.value === 'Game version') {
if (group === 'Game version') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
return a[0].localeCompare(b[0], undefined, { numeric: true })
})
@@ -241,7 +252,7 @@ const filteredResults = computed(() => {
</div>
<DropdownSelect
v-slot="{ selected }"
v-model="sortBy"
v-model="state.sortBy"
name="Sort Dropdown"
class="max-w-[16rem]"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
@@ -252,7 +263,7 @@ const filteredResults = computed(() => {
</DropdownSelect>
<DropdownSelect
v-slot="{ selected }"
v-model="group"
v-model="state.group"
class="max-w-[16rem]"
name="Group Dropdown"
:options="['Group', 'Loader', 'Game version', 'None']"

View File

@@ -571,12 +571,12 @@ onUnmounted(() => {
z-index: 11;
gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
width: max-content;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
max-height: 98vh;
max-height: calc(100vh - 300px);
overflow-y: auto;
&::-webkit-scrollbar-track {
@@ -673,7 +673,7 @@ onUnmounted(() => {
text-align: left;
&.expanded {
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
padding: 1rem;
}
}

View File

@@ -119,7 +119,7 @@ onBeforeUnmount(() => {
background-color: var(--color-raised-bg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-floating);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
margin: 0;
position: fixed;
z-index: 1000000;
@@ -163,7 +163,7 @@ onBeforeUnmount(() => {
}
.divider {
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
margin: var(--gap-sm);
pointer-events: none;
}

View File

@@ -34,7 +34,7 @@
</div>
<div class="input-row">
<p class="input-label">Game version</p>
<div class="versions">
<div class="flex gap-4 items-center">
<multiselect
v-model="game_version"
class="selector"
@@ -45,7 +45,7 @@
open-direction="top"
:show-labels="false"
/>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Show all versions" />
<Checkbox v-model="showSnapshots" class="shrink-0" label="Show all versions" />
</div>
</div>
<div v-if="loader !== 'vanilla'" class="input-row">
@@ -563,12 +563,6 @@ const next = async () => {
font-style: italic;
}
.versions {
display: flex;
flex-direction: row;
gap: 1rem;
}
:deep(button.checkbox) {
border: none;
}

View File

@@ -69,7 +69,7 @@ onUnmounted(() => {
<SpinnerIcon class="animate-spin w-4 h-4" />
</div>
</NavButton>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></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>

View File

@@ -293,7 +293,7 @@ onBeforeUnmount(() => {
align-items: center;
gap: 0.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
padding: var(--gap-sm) var(--gap-lg);
}
@@ -356,7 +356,7 @@ onBeforeUnmount(() => {
gap: 1rem;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
&.hidden {
transform: translateY(-100%);
@@ -454,7 +454,7 @@ onBeforeUnmount(() => {
flex-direction: column;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
padding: var(--gap-md);
&.hidden {

View File

@@ -15,8 +15,8 @@ import {
ButtonStyled,
Checkbox,
Chips,
Combobox,
injectNotificationManager,
TeleportDropdownMenu,
} from '@modrinth/ui'
import {
formatCategory,
@@ -164,6 +164,21 @@ const selectableGameVersionNumbers = computed(() => {
.map((x) => x.version)
})
const gameVersionOptions = computed(() =>
(selectableGameVersionNumbers.value ?? []).map((v) => ({ value: v, label: v })),
)
const loaderVersionOptions = computed(() =>
(selectableLoaderVersions.value ?? []).map((opt, index) => ({ value: index, label: opt.id })),
)
const loaderVersionLabel = computed(() => {
const idx = loaderVersionIndex.value
return idx >= 0 && selectableLoaderVersions.value
? selectableLoaderVersions.value[idx]?.id
: 'Select version'
})
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
if (gameVersion.value) {
if (loader.value === 'fabric') {
@@ -687,11 +702,11 @@ async function handleInitAuthLibPatching(ismojang: boolean) {
{{ formatMessage(messages.gameVersion) }}
</h2>
<div class="flex flex-wrap mt-2 gap-2">
<TeleportDropdownMenu
<Combobox
v-if="selectableGameVersionNumbers !== undefined"
v-model="gameVersion"
:options="selectableGameVersionNumbers"
name="Game Version Dropdown"
:options="gameVersionOptions"
:display-value="gameVersion || formatMessage(messages.unknownVersion)"
/>
<Checkbox
v-if="hasSnapshots"
@@ -703,14 +718,13 @@ async function handleInitAuthLibPatching(ismojang: boolean) {
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
</h2>
<TeleportDropdownMenu
<Combobox
v-if="selectableLoaderVersions"
:model-value="selectableLoaderVersions[loaderVersionIndex]"
:options="selectableLoaderVersions"
:display-name="(option: ManifestLoaderVersion) => option?.id"
v-model="loaderVersionIndex"
:options="loaderVersionOptions"
:display-value="loaderVersionLabel"
name="Version selector"
class="mt-2"
@change="(value) => (loaderVersionIndex = value.index)"
/>
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
<IssuesIcon />

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
@@ -50,7 +50,7 @@ watch(
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = e
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
@@ -86,12 +86,13 @@ watch(
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
</div>
<TeleportDropdownMenu
<Combobox
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="w-40"
:options="['Home', 'Library']"
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
:display-value="settings.default_page ?? 'Select an option'"
/>
</div>
@@ -122,7 +123,7 @@ watch(
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = e
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"

View File

@@ -130,7 +130,7 @@ onUnmounted(() => {
/>
</template>
<div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow rounded-xl smart-clickable:highlight-on-hover"
>
<Avatar
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"

View File

@@ -181,7 +181,7 @@ const messages = defineMessages({
/>
</template>
<div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow smart-clickable:highlight-on-hover rounded-xl"
:class="{
'world-item-highlighted': highlighted,
}"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { TeleportDropdownMenu } from '@modrinth/ui'
import { Combobox } from '@modrinth/ui'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import type { ServerPackStatus } from '@/helpers/worlds.ts'
@@ -74,12 +74,19 @@ defineExpose({ resourcePackOptions })
{{ formatMessage(messages.resourcePack) }}
</h2>
<div>
<TeleportDropdownMenu
<Combobox
v-model="resourcePack"
:options="resourcePackOptions"
:options="
resourcePackOptions.map((o) => ({
value: o,
label: formatMessage(resourcePackOptionMessages[o]),
}))
"
name="Server resource pack"
:display-name="
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
:display-value="
resourcePack
? formatMessage(resourcePackOptionMessages[resourcePack])
: 'Select an option'
"
/>
</div>

View File

@@ -35,6 +35,14 @@ export async function elyby_auth_authenticate(login, password, clientToken) {
})
}
/**
* Check if the authentication servers are reachable, throwing an exception if
* not reachable.
*/
export async function check_reachable() {
await invoke('plugin:auth|check_reachable')
}
/**
* Authenticate a user with Hydra - part 1.
* This begins the authentication flow quasi-synchronously.

View File

@@ -18,7 +18,7 @@ import { install_to_existing_profile } from '@/helpers/pack.js'
- icon is a path to an image file, which will be copied into the profile directory
*/
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
export async function create(name, gameVersion, modloader, loaderVersion, icon, skipInstall) {
//Trim string name to avoid "Unable to find directory"
name = name.trim()
return await invoke('plugin:profile-create|profile_create', {
@@ -26,7 +26,7 @@ export async function create(name, gameVersion, modloader, loaderVersion, iconPa
gameVersion,
modloader,
loaderVersion,
iconPath,
icon,
skipInstall,
})
}

View File

@@ -1,6 +1,9 @@
{
"app.settings.developer-mode-enabled": {
"message": "وضع المطوّر مُفعَّل."
"message": "تم تفعيل وضع المطوّر."
},
"app.settings.downloading": {
"message": "جار تنزيل الإصدار {version}"
},
"app.settings.tabs.appearance": {
"message": "المظهر"
@@ -9,7 +12,7 @@
"message": "خيارات النسخة الافتراضية"
},
"app.settings.tabs.feature-flags": {
"message": "إعدادات المميزات"
"message": "أعلام الميزات"
},
"app.settings.tabs.java-installations": {
"message": "تثبيتات جافا"
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.update-toast.body": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-toast.body.metered": {
"message": "تطبيق Modrinth الإصدار {version} متاح الآن!\nنظرًا لأنك تستخدم شبكة محدودة البيانات، لم نقم بتنزيل التحديث تلقائيًا.\n"
},
"app.update-toast.changelog": {
"message": "سجلّ التغييرات"
},
"app.update-toast.download": {
"message": "تنزيل ({size})"
},
"app.update-toast.downloading": {
"message": "جار التنزيل..."
},
"app.update-toast.reload": {
"message": "إعادة تحميل"
},
"app.update-toast.title": {
"message": "تحديث متاح"
},
"app.update-toast.title.download-complete": {
"message": "اكتمل التنزيل"
},
"app.update.complete-toast.text": {
"message": "انقر هنا لعرض سجلّ التغييرات."
},
"app.update.complete-toast.title": {
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.update.download-update": {
"message": "تنزيل التحديث"
},
"app.update.downloading-update": {
"message": "جار تنزيل التحديث ({percent}٪)"
},
"app.update.reload-to-update": {
"message": "أعد التحميل لتثبيت التحديث"
},
"friends.action.add-friend": {
"message": "إضافة صديق"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {طلب صداقة} other {طلبات صداقة}}"
},
"friends.add-friend.submit": {
"message": "إرسال طلب صداقة"
},
"friends.add-friend.title": {
"message": "جار إضافة صديق"
},
"friends.add-friend.username.description": {
"message": "قد يختلف عن اسم المستخدم الخاص بهم في Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "أدخل اسم مستخدم Modrinth..."
},
"friends.add-friend.username.title": {
"message": "ما اسم مستخدم صديقك في Modrinth؟"
},
"friends.add-friends-to-share": {
"message": "<link>أضف أصدقاء</link> لمعرفة ما الذي يلعبونه!"
},
"friends.friend.cancel-request": {
"message": "إلغاء الطلب"
},
"friends.friend.remove-friend": {
"message": "إزالة صديق"
},
"friends.friend.request-sent": {
"message": "إرسال طلب الصداقة"
},
"friends.friend.view-profile": {
"message": "عرض الملف الشخصي"
},
"friends.heading": {
"message": "أصدقاء"
},
"friends.heading.active": {
"message": "نشط"
},
"friends.heading.offline": {
"message": "غير متصل"
},
"friends.heading.online": {
"message": "متصل"
},
"friends.heading.pending": {
"message": "قيد الانتظار"
},
"friends.no-friends-match": {
"message": "لا يوجد أصدقاء يطابقون ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "ابحث عن الأصدقاء..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>سجّل الدخول إلى حساب Modrinth</link> لإضافة الأصدقاء ومعرفة ما الذي يلعبونه!"
},
"instance.add-server.add-and-play": {
"message": "إضافة واللعب"
},

View File

@@ -1,5 +1,8 @@
{
"app.settings.developer-mode-enabled": {
"message": ""
},
"app.settings.downloading": {
"message": ""
}
}

View File

@@ -12,7 +12,7 @@
"message": "Mga kapilian sa sukaranan nga pananglitan"
},
"app.settings.tabs.feature-flags": {
"message": "Bandera sa mga bahin"
"message": "Bandera sa mga panagway"
},
"app.settings.tabs.java-installations": {
"message": "Mga pagtaod sa Java"
@@ -21,34 +21,118 @@
"message": "Pribasiya"
},
"app.settings.tabs.resource-management": {
"message": "Pagdumala sa kabtangan"
"message": "Pagdumala sa kahinguhaan"
},
"app.update-toast.body": {
"message": "Andam na mataud ang Modrinth App v{version}! Pagkarga kausab aron mapasibo, o kinaugalingon pagtak-op sa Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Nahuman ang pagkarganug sa Modrinth App v{version}. Pagkarga kausab aron matuman ang kabag-ohan, o unya nalang sa pagsara sa Modrinth App."
"message": "Nahuman ang pagkarganug sa Modrinth App v{version}. Pagkarga kausab aron mapasibo, o kinaugalingon pagtak-op sa Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magamit na karon ang Modrinth App! Wala namo karganugi daan kay inihap man ang imong pum-ot."
},
"app.update-toast.changelog": {
"message": "Talaan sa Kausaban"
},
"app.update-toast.download": {
"message": "Karganugi ({size})"
},
"app.update-toast.downloading": {
"message": "Gakarganug..."
},
"app.update-toast.reload": {
"message": "Kargaha pag-usab"
},
"app.update-toast.title": {
"message": "Naay bag-o nga pagpasibo"
},
"app.update-toast.title.download-complete": {
"message": "Nahuman ang pagkarganug"
},
"app.update.complete-toast.text": {
"message": "Panuplok diri aron malantaw ang talaan sa kausaban."
},
"app.update.complete-toast.title": {
"message": "Malampusong nataud ang bersiyon nga {version}!"
},
"app.update.download-update": {
"message": "Karganugi ang kausaban"
},
"app.update.downloading-update": {
"message": "Gakarganug sa kausaban ({percent}%)"
"message": "Gakarganug sa pagpasibo ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Andam mataud ang pagpasibo"
},
"friends.action.add-friend": {
"message": "Pagdugang og higala"
},
"friends.action.view-friend-requests": {
"message": "{count} ka hangyo sa pakighigala"
},
"friends.add-friend.submit": {
"message": "Pagpadala og hangyo sa pakighigala"
},
"friends.add-friend.title": {
"message": "Pagdugang og higala"
},
"friends.add-friend.username.description": {
"message": "Mahimong galahi sa ngalan nila sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ibutang ang ngalan sa tiggamit sa Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Unsa man ang ngalan sa imong higala sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Pagdugang og mga higala</link> aron makit-an ang ilang ginadula!"
},
"friends.friend.cancel-request": {
"message": "Bawia ang hangyo"
},
"friends.friend.remove-friend": {
"message": "Tangtangi ang higala"
},
"friends.friend.request-sent": {
"message": "Gipadala na ang hangyo sa pakighigala"
},
"friends.friend.view-profile": {
"message": "Tan-awa ang propayl"
},
"friends.heading": {
"message": "Mga higala"
},
"friends.heading.active": {
"message": "Malihokon"
},
"friends.heading.offline": {
"message": "Sira"
},
"friends.heading.online": {
"message": "Buka"
},
"friends.heading.pending": {
"message": "Gahulat"
},
"friends.no-friends-match": {
"message": "Walay higala nga motukma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Mangita sa mga higala..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Pag-sign-in sa Modrinth account</link> aron makadugang og mga higala ug mahibal-an ang ginadula nila!"
},
"instance.add-server.add-and-play": {
"message": "Idugang ug dulaa"
},
"instance.add-server.add-server": {
"message": "Idugang ang tigtagad"
"message": "Idugang ang magsisilbi"
},
"instance.add-server.resource-pack.disabled": {
"message": "Dili motugot"
@@ -60,13 +144,13 @@
"message": "Magpatugot"
},
"instance.add-server.title": {
"message": "Pagdugang og tigtagad"
"message": "Pagdugang og magsisilbi"
},
"instance.edit-server.title": {
"message": "Usba ang tigtagad"
"message": "Usba ang magsisilbi"
},
"instance.edit-world.hide-from-home": {
"message": "Ayaw ipakita sa panid sa Balay"
"message": "Ayaw ipakita sa Puluy-anang panid"
},
"instance.edit-world.name": {
"message": "Ngalan"
@@ -75,37 +159,55 @@
"message": "Minecraft nga Kalibutan"
},
"instance.edit-world.reset-icon": {
"message": "Walaa ang amoy"
"message": "Pag-usab sa amoy"
},
"instance.edit-world.title": {
"message": "Usba ang kalibutan"
},
"instance.filter.disabled": {
"message": "Di-gagana nga mga proyekto"
"message": "Di-paganhong mga proyekto"
},
"instance.filter.updates-available": {
"message": "Naay bag-ong mga kausaban"
},
"instance.server-modal.address": {
"message": "Padad-anan"
},
"instance.server-modal.name": {
"message": "Ngalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft nga Tigtagad"
"message": "Minecraft nga Magsisilbi"
},
"instance.server-modal.resource-pack": {
"message": "Putos sa kabtangan"
"message": "Putos sa kahinguhaan"
},
"instance.settings.tabs.general": {
"message": "Tinanan"
},
"instance.settings.tabs.general.delete": {
"message": "Tangtangi ang pananglitan"
"message": "Panas-i kining pananglitan"
},
"instance.settings.tabs.general.delete.button": {
"message": "Tangtangi ang pananglitan"
"message": "Panas-i kining pananglitan"
},
"instance.settings.tabs.general.delete.description": {
"message": "Malungtarong matangtang ang pananglitan sa imong himan, apil na ang imong mga kalibutan, paghan-ay, ug tanang gitaod nga sulod. Pag-amping, dili na mabawi kung tangtangon na nimo ang pananglitan."
"message": "Malungtarong mopanas ang pananglitan sa imong himan, apil na ang imong mga kalibutan, paghan-ay, ug tanang gitaod nga sulod. Pag-amping, dili na mabawi kung gipanas na nimo ang pananglitan."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Gapanas..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Paghulad"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Dili makahulad samtang nga gataud."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Paghulad sa pananglitan"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Buhatan og kopya kining pananglitan, apil na ang imong mga kalibutan, paghan-ay, kausaban, ug uban pa."
"message": "Buhatan og awat kining pananglitan, apil na ang imong mga kalibutan, paghan-ay, kausaban, ug uban pa."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Usba ang amoy"
@@ -113,19 +215,259 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tangtangi ang amoy"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Pulihan ang amoy"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pamili og amoy"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga pundok sa librarya"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Pagbuhat og bag-o nga pundok"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Gitugotan sa mga pundok sa librarya nga imong mahan-ay ang imong mga pananglitan sa nagkalain-lain nga bahin sa imong librarya."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ibutang ang ngalan sa pundok"
},
"instance.settings.tabs.general.name": {
"message": "Ngalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga kaw-it sa paglansad"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga pinatuyo nga kaw-it sa paglansad"
},
"instance.settings.tabs.hooks.description": {
"message": "Gitugotan sa mga kaw-it ang mga eksperto nga mga tiggamit nga makapadagan og mga sistema nga sugo ayha ug paghuman malansad ang dula."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Human-matak-op"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipadagan paghuman matak-op ang dula."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ibutang ang human-matak-op nga sugo..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Ayha-malansad"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipadagan ayha malansad ang pananglitan."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ibutang ang ayha-malansad nga sugo..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga kaw-it sa paglansad sa dula"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Pamutos"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Pamutos nga sugo sa paglansad sa Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ibutang ang pamutos nga sugo..."
},
"instance.settings.tabs.installation": {
"message": "Pagtaud"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Nataud naman ang {platform} {version} alang sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Nataud naman ang Banilya nga {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Pulihan og bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "Itaud"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Gataud"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Gapangita og mga bersiyon sa mga putos sa kausaban"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Gataud sa bag-o nga bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Pagkakarong taud"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Kasayoran sa pagputli:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon sa dula"
},
"instance.settings.tabs.installation.install": {
"message": "Itaud"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nagtaud karon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon sa {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Dili magamit ang {loader} sa Minecraft {version}. Sulayi ang ubang tigkarga sa kausaban."
},
"instance.settings.tabs.installation.platform": {
"message": "Pantawan"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Itaud pag-usab ang putos sa kausaban"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Nagtaud pag-usab sa putos sa kusaban"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Mahimo nga mobalik sa sinugdan ang tanang gitaod o giusab nga sulod sa unsay ihatag sa putos sa kausaban, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban. Mahimo nga maayo ang mga tuhaw nga batasan kon naay pagbag-o sa pananglitan, apan kon gasalig na ang imong kalibutan sa dinugang nga sulod, mahimo nga madaut ani ang daan nga mga kalibutan."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Ibalik ang mga sulod sa pananglitan sa unang kahimtang, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Itaud pag-usab ang putos sa kausaban"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Ayohon"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Gaayo"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pag-ayo, mataud pagbalik ang mga sinaligan sa Minecraft ug mangita og mga kadunot. Mahimo nga masulbad niini ang mga isyu kun dili malunsad ang dula tungod sa mga kasaypan matud sa tiglansad, apan dili ni masulbad ang mga isyu o pagdusmog matud sa mga gitaud nga kausaban."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Ayohon ang pananglitan?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Nag-ayo karon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasamtang pag-usab "
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang tanang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "pulihan og bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "itaud"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "itaud pag-usab"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "ayohon"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Dili maka-{action} samtang nga gataud"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Dili maka-{action} samtang binugto"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Dili maka-{action} samtang nag-ayo"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(diinilang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "Pagbugto sa pananglitan"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kun imong ipadayon, dili na nimo makatay pagbalik nga wala mohimo og bag-o nga pananglitan. Dili na ka makadawat og pagpasibo sa putos sa kausaban ug mahimo kining naandan nga."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Nakakatay kining pananglitan sa usa ka putos sa kausaban, pasabot ani nga dili mapasibo ang mga kausaban og dili nimo mausab ang tigkarga sa kausaban o ang bersiyon sa Minecraft. Kanunay nga mabugto kining pananglitan og putos sa kausabon kun tangtangon ang katay."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Pagbugto sa putos sa kausaban"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga kaw-it"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga lantugi sa java"
},
"instance.settings.tabs.java.java-installation": {
"message": "Pagtaud sa Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang gigahin"
},
"instance.settings.tabs.window": {
"message": "Tamboanan"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga gusto sa pinatuyo nga tamboanan"
"message": "Mga himutangan sa pinatuyo nga tamboanan"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Punong-tabil"
},
"instance.settings.tabs.window.height": {
"message": "Gitas-on"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ibutang ang gitas-on..."
},
"instance.settings.tabs.window.width": {
"message": "Gilapdon"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ibutang ang gilapdon..."
},
"instance.settings.title": {
"message": "Mga Gusto"
"message": "Mga Himutangan"
},
"instance.worlds.a_minecraft_server": {
"message": "Usa ka Minecraft nga Magsisilbi"
},
"instance.worlds.copy_address": {
"message": "Awata ang padad-anan"
},
"instance.worlds.dont_show_on_home": {
"message": "Ayaw pakit-a sa Puloy-anan"
},
"instance.worlds.filter.available": {
"message": "Magamit"
},
"instance.worlds.hardcore": {
"message": "Mahanasnon nga paagi"
},
"instance.worlds.play_instance": {
"message": "Dulai ang pananglitan"
},
"instance.worlds.type.server": {
"message": "Magsisilbi"
},
"instance.worlds.type.singleplayer": {
"message": "Inusara nga dula"
},
"instance.worlds.view_instance": {
"message": "Tan-awa ang pananglitan"
},
"instance.worlds.world_in_use": {
"message": "Gigamit ang kalibotan"
},
"search.filter.locked.instance.sync": {
"message": "Pagdungan sa pananglitan"
}
}

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Udvikler-tilstand aktiveret."
},
"app.settings.downloading": {
"message": "Downloader v{version}"
},
"app.settings.tabs.appearance": {
"message": "Udseende"
},
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcestyring"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} er allerede installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
"app.update-toast.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."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} er nu tilgængelig! Siden du er på et begrænset netværk, vi downloadede den ikke automatisk."
},
"app.update-toast.changelog": {
"message": "Ændringslog"
},
"app.update-toast.download": {
"message": "Download ({size})"
},
"app.update-toast.downloading": {
"message": "Downloader..."
},
"app.update-toast.reload": {
"message": "Geninlæs"
},
"app.update-toast.title": {
"message": "Opdatering tilgængelig"
},
"app.update-toast.title.download-complete": {
"message": "Download færdiggjort"
},
"app.update.complete-toast.text": {
"message": "Klik her for at vise ændringslog."
},
"app.update.complete-toast.title": {
"message": "Version {version} var installeret med succes!"
},
"app.update.download-update": {
"message": "Download opdatering"
},
"app.update.downloading-update": {
"message": "Downloader opdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Genindlæs for at installere opdatering"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
},
"friends.action.view-friend-requests": {
"message": "{count} venne{count, plural, one {anmodning} other {anmodninger}}"
},
"friends.add-friend.submit": {
"message": "Send en venneanmodning"
},
"friends.add-friend.title": {
"message": "Tilføjer en ven"
},
"friends.add-friend.username.description": {
"message": "Det er muligvis anderledes end deres Minecraft brugernavn!"
},
"friends.add-friend.username.placeholder": {
"message": "Indskriv Modrinth brugernavn..."
},
"friends.add-friend.username.title": {
"message": "Hvad er din vens Modrinth brugernavn?"
},
"friends.add-friends-to-share": {
"message": "<link>Tilføj venner</link> for at se hvad de spiller!"
},
"friends.friend.cancel-request": {
"message": "Annuller anmodning"
},
"friends.friend.remove-friend": {
"message": "Fjern ven"
},
"friends.friend.request-sent": {
"message": "Venneanmodning sendt"
},
"friends.friend.view-profile": {
"message": "Vis profil"
},
"friends.heading": {
"message": "Venner"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Afventer"
},
"friends.no-friends-match": {
"message": "Ingen venner som matcher \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Søg venner..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
},
"instance.add-server.add-and-play": {
"message": "Tilføj og spil"
},

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Lade neu um Aktualisierung zu installieren"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
"friends.action.view-friend-requests": {
"message": "{count} Freundschaftsanfrage{count, plural, one {} other {n}}"
},
"friends.add-friend.submit": {
"message": "Freundschaftsanfrage senden"
},
"friends.add-friend.title": {
"message": "Einen Freund hinzufügen"
},
"friends.add-friend.username.description": {
"message": "Es könnte anders als ihr Minecraft Nutzername sein!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth Nutzernamen eingeben..."
},
"friends.add-friend.username.title": {
"message": "Was ist der Modrinth Nutzername deines Freundes?"
},
"friends.add-friends-to-share": {
"message": "<link>Füge Freunde hinzu</link> um zu sehen, was sie spielen!"
},
"friends.friend.cancel-request": {
"message": "Anfrage abbrechen"
},
"friends.friend.remove-friend": {
"message": "Freund entfernen"
},
"friends.friend.request-sent": {
"message": "Freundschaftsanfrage gesendet"
},
"friends.friend.view-profile": {
"message": "Profil anzeigen"
},
"friends.heading": {
"message": "Freunde"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Ausstehend"
},
"friends.no-friends-match": {
"message": "Keine Freunde, die mit \"{query}\" übereinstimmen"
},
"friends.search-friends-placeholder": {
"message": "Freunde suchen..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Logge dich in ein Modrinth Konto ein</link> um Freunde hinzuzufügen und zu sehen, was sie spielen!"
},
"instance.add-server.add-and-play": {
"message": "Ersteue u starte"
},

View File

@@ -15,7 +15,7 @@
"message": "Funktionsflaggen"
},
"app.settings.tabs.java-installations": {
"message": "Java Installationen"
"message": "Java-Installationen"
},
"app.settings.tabs.privacy": {
"message": "Datenschutz"
@@ -24,16 +24,16 @@
"message": "Ressourcenmanagement"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} ist bereit zum Installieren! Lade neu um es jetzt zu updaten, oder automatisch wenn du die Modrinth App schließt."
"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.update-toast.body.download-complete": {
"message": "Modrinth App v{version} ist heruntergeladen. Lade neu um es jetzt zu updaten, oder wenn du die Modrinth App schließt."
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
},
"app.update-toast.body.metered": {
"message": "Die Modrinth App v{version} ist jetzt verfügbar! Da Sie sich in einem getakteten Netzwerk befinden, haben wir es nicht automatisch heruntergeladen."
"message": "Modrinth App v{version} ist jetzt verfügbar! Da du ein getaktetes Netzwerk nutzt, haben wir den Download nicht automatisch gestartet."
},
"app.update-toast.changelog": {
"message": "Änderungen"
"message": "Änderungsverlauf"
},
"app.update-toast.download": {
"message": "Herunterladen ({size})"
@@ -45,26 +45,89 @@
"message": "Neu laden"
},
"app.update-toast.title": {
"message": "Aktualisierung verfügbar"
"message": "Update verfügbar"
},
"app.update-toast.title.download-complete": {
"message": "Herunterladen abgeschlossen"
"message": "Download abgeschlossen"
},
"app.update.complete-toast.text": {
"message": "Drücke hier um die Änderungen zu sehen."
"message": "Hier klicken, um das Änderungsprotokoll anzuzeigen."
},
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Lade Update herunter"
"message": "Update herunterladen"
},
"app.update.downloading-update": {
"message": "Lade Update Herunter ({percent}%)"
"message": "Update wird heruntergeladen ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Neu laden um Update zu installieren"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {Freundesanfrage} other {Freundesanfragen}}"
},
"friends.add-friend.submit": {
"message": "Freundesanfrage senden"
},
"friends.add-friend.title": {
"message": "Einen Freund hinzufügen"
},
"friends.add-friend.username.description": {
"message": "Er kann vom Minecraft-Nutzernamen abweichen!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth-Benutzernamen eingeben..."
},
"friends.add-friend.username.title": {
"message": "Wie lautet der Modrinth-Benutzername deines Freundes?"
},
"friends.add-friends-to-share": {
"message": "<link>Freunde hinzufügen</link>, um zu sehen, was sie spielen!"
},
"friends.friend.cancel-request": {
"message": "Anfrage abbrechen"
},
"friends.friend.remove-friend": {
"message": "Freund entfernen"
},
"friends.friend.request-sent": {
"message": "Freundesanfrage gesendet"
},
"friends.friend.view-profile": {
"message": "Profil anzeigen"
},
"friends.heading": {
"message": "Freunde"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Ausstehend"
},
"friends.no-friends-match": {
"message": "Keine Freunde die \"{query}\" entsprechen"
},
"friends.search-friends-placeholder": {
"message": "Freunde durchsuchen..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Melde dich bei einem Modrinth-Konto an</link>, um Freunde hinzuzufügen und zu sehen, was sie gerade spielen!"
},
"instance.add-server.add-and-play": {
"message": "Hinzufügen und spielen"
},
@@ -93,7 +156,7 @@
"message": "Name"
},
"instance.edit-world.placeholder-name": {
"message": "Minecraft Welt"
"message": "Minecraft-Welt"
},
"instance.edit-world.reset-icon": {
"message": "Icon zurücksetzen"
@@ -114,7 +177,7 @@
"message": "Name"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft Server"
"message": "Minecraft-Server"
},
"instance.server-modal.resource-pack": {
"message": "Ressourcenpaket"
@@ -129,16 +192,16 @@
"message": "Instanz löschen"
},
"instance.settings.tabs.general.delete.description": {
"message": "Löscht eine Instanz dauerhaft von deinem Gerät, einschließlich deiner Welten, Einstellungen und aller installierten Inhalte. Sei vorsichtig, eine gelöschte Installation ist nicht wiederherstellbar."
"message": "Löscht eine Instanz dauerhaft von deinem Gerät, einschließlich deiner Welten, Einstellungen und aller installierten Inhalte. Sei vorsichtig, da eine gelöschte Instanz nicht wiederhergestellt werden kann."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Duplizieren"
"message": "Kopieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht dupliziert werden."
"message": "Kann während der Installation nicht kopiert werden."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Instanz duplizieren"
@@ -165,7 +228,7 @@
"message": "Neue Gruppe erstellen"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Gruppen ermöglichen dir, deine Instanzen in verschiedenen Abteilen deiner Bibliothek einzuteilen."
"message": "Bibliotheksgruppen ermöglichen es dir, deine Instanzen in verschiedene Abschnitte deiner Bibliothek zu organisieren."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Gruppenname eingeben"
@@ -183,25 +246,25 @@
"message": "Hooks ermöglichen es fortgeschrittenen Benutzern, bestimmte Systembefehle vor und nach dem Spielstart auszuführen."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Nach dem Schließen des Spiels"
"message": "Nach dem Beenden"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Wird ausgeführt nach dem Beenden des Spiels."
"message": "Wird nach dem Beenden des Spiels ausgeführt."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ausgeführter Befehl nach dem Beenden des Spiels eingeben..."
"message": "Nach Spielbeendigung auszuführender Befehl eingeben..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Vor dem Starten des Spiels"
"message": "Vor dem Start"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Wird ausgeführt nach dem Starten des Spiels."
"message": "Wird vor dem Starten der Instanz ausgeführt."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ausgeführter Befehl nach dem Starten des Spiels eingeben..."
"message": "Vor Spielstart auszuführender Befehl eingeben..."
},
"instance.settings.tabs.hooks.title": {
"message": "Start Hooks"
"message": "Start-Hooks"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
@@ -237,7 +300,7 @@
"message": "Neue Version wird installiert"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Aktuell installiert"
"message": "Derzeit installiert"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Informationen für die Fehlerbehebung:"
@@ -252,7 +315,7 @@
"message": "Installieren"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Wird installiert"
"message": "Installation im Gange"
},
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} Version"
@@ -279,13 +342,13 @@
"message": "Modpack wird neu installiert"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Durch die Neuinstallation werden alle installierten oder geänderten Inhalte auf die vom Modpack bereitgestellten Inhalte zurückgesetzt, wobei alle Mods oder Inhalte entfernt werden, die zusätzlich zur ursprünglichen Installation hinzugefügt wurden. Dies kann unerwartetes Verhalten beheben, wenn Änderungen an der Instanz vorgenommen wurden. Wenn Ihre Welten jedoch von zusätzlich installierten Inhalten abhängig sind, kann dies zu Fehlern in bestehenden Welten führen."
"message": "Eine Neuinstallation setzt alle installierten oder geänderten Inhalte auf den Zustand zurück, der vom Modpack bereitgestellt wird, und entfernt alle Mods oder Inhalte, die du zusätzlich zur ursprünglichen Installation hinzugefügt hast.\nDies kann unerwartetes Verhalten beheben, falls Änderungen an der Instanz vorgenommen wurden. Wenn deine Welten jedoch von zusätzlich installierten Inhalten abhängen, kann dies bestehende Welten beschädigen."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Bist du dir sicher, dass du diese Instanz neu installieren willst?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Setzt den Inhalt der Instanz auf seinen ursprünglichen Zustand zurück und entfernt alle Mods oder Inhalte, die zusätzlich zum ursprünglichen Modpack hinzugefügt wurden."
"message": "Setzt den Inhalt der Instanz auf den ursprünglichen Zustand zurück und entfernt alle Mods oder Inhalte, die du zusätzlich zum ursprünglichen Modpack hinzugefügt hast."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Modpack neu installieren"
@@ -306,7 +369,7 @@
"message": "Reparatur im Gange"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Auf aktuellen Wert zurücksetzen"
"message": "Auf aktuellen Stand zurücksetzen"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Alle Versionen anzeigen"
@@ -315,19 +378,19 @@
"message": "Version ändern"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "installieren"
"message": "Installieren"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "neuinstallieren"
"message": "Neuinstallieren"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "reparieren"
"message": "Reparieren"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "{action} während der Installation nicht möglich"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "{action} wegen fehlender Internetverbindung nicht möglich"
"message": "{action} offline nicht möglich"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "{action} während der Reparation nicht möglich"
@@ -339,7 +402,7 @@
"message": "Verknüpfung der Instanz trennen"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Wenn du fortfährst, kann die Instanz nicht erneut verknüpft werden, ohne eine völlig neue Instanz zu erstellen. Du erhältst keine Modpack-Updates mehr und es wird zu einer normalen Instanz."
"message": "Wenn du fortfährst, kannst du sie nicht erneut verknüpfen, ohne eine völlig neue Instanz zu erstellen. Du wirst keine Modpack-Updates mehr erhalten, und sie wird zu einer normalen Instanz."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "Möchtest du die Verknüpfungen dieser Instanz wirklich trennen?"
@@ -363,19 +426,19 @@
"message": "Java-Argumente"
},
"instance.settings.tabs.java.java-installation": {
"message": "Java Installation"
"message": "Java-Installation"
},
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.window": {
"message": "Spielfenster"
"message": "Fenster"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Benutzerdefinierte Spielfenstereinstellungen"
"message": "Benutzerdefinierte Fenstereinstellungen"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Vollbildschirm"
"message": "Vollbild"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Lässt das Spiel im Vollbildmodus starten (mit Verwendung von options.txt)."
@@ -384,7 +447,7 @@
"message": "Höhe"
},
"instance.settings.tabs.window.height.description": {
"message": "Die Höhe des spiel Fensters beim Starten."
"message": "Die Höhe des Spielfensters beim Start."
},
"instance.settings.tabs.window.height.enter": {
"message": "Höhe eingeben..."
@@ -393,7 +456,7 @@
"message": "Breite"
},
"instance.settings.tabs.window.width.description": {
"message": "Die Breite des spiel Fensters beim Starten."
"message": "Die Breite des Spielfensters beim Start."
},
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
@@ -444,7 +507,7 @@
"message": "Einzelspieler"
},
"instance.worlds.view_instance": {
"message": "Instanzen anzeigen"
"message": "Instanz anzeigen"
},
"instance.worlds.world_in_use": {
"message": "Welt wird aktuell benutzt"

View File

@@ -1,4 +1,10 @@
{
"app.auth-servers.unreachable.body": {
"message": "Minecraft authentication servers may be down right now. Check your internet connection and try again later."
},
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"friends.action.add-friend": {
"message": "Añadir un amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {solicitud} other {solicitudes}} de amistad"
},
"friends.add-friend.submit": {
"message": "Enviar solicitud de amistad"
},
"friends.add-friend.title": {
"message": "Añadiendo un amigo"
},
"friends.add-friend.username.description": {
"message": "¡Podría ser distinto a su nombre de Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Escribe el nombre de usuario de Modrinth..."
},
"friends.add-friend.username.title": {
"message": "¿Cuál es el nombre de usuario de Modrinth de tu amigo?"
},
"friends.add-friends-to-share": {
"message": "<link>¡Añade amigos</link> para ver qué están jugando!"
},
"friends.friend.cancel-request": {
"message": "Cancelar solicitud"
},
"friends.friend.remove-friend": {
"message": "Eliminar amigo"
},
"friends.friend.request-sent": {
"message": "Solicitud de amistad enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Activos"
},
"friends.heading.offline": {
"message": "Desconectados"
},
"friends.heading.online": {
"message": "Conectados"
},
"friends.heading.pending": {
"message": "Pendiente"
},
"friends.no-friends-match": {
"message": "Ningún amigo coincide con \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Buscar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>¡Inicia sesión en una cuenta de Modrinth</link> para añadir amigos y ver qué están jugando!"
},
"instance.add-server.add-and-play": {
"message": "Añadir y jugar"
},
@@ -117,7 +180,7 @@
"message": "Servidor de Minecraft"
},
"instance.server-modal.resource-pack": {
"message": "Paquete de recursos"
"message": "Resource pack"
},
"instance.settings.tabs.general": {
"message": "General"

View File

@@ -24,13 +24,13 @@
"message": "Gestión de recursos"
},
"app.update-toast.body": {
"message": "¡La aplicación Modrinth v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación Modrinth."
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
},
"app.update-toast.body.download-complete": {
"message": "La descarga de la aplicación Modrinth v{version} ha finalizado. Actualice ahora o automáticamente al cerrar la aplicación Modrinth."
"message": "La descarga de la versión v{version} de Modrinth ha finalizado. Actualice ahora o automáticamente al cerrar la aplicación."
},
"app.update-toast.body.metered": {
"message": "¡La aplicación Modrinth v{version} ya está disponible! Como estás conectado a una red con límite de datos, no la hemos descargado automáticamente."
"message": "¡La versión v{version} de Modrinth ya está disponible! Como estás conectado a una red con límite de datos, no la hemos descargado automáticamente."
},
"app.update-toast.changelog": {
"message": "Registro de cambios"
@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"friends.action.add-friend": {
"message": "Agrega a un amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {petición} other {peticiones}} de amistad"
},
"friends.add-friend.submit": {
"message": "Envía una petición de amistad"
},
"friends.add-friend.title": {
"message": "Agrega a un amigo"
},
"friends.add-friend.username.description": {
"message": "¡Puede ser diferente de su apodo en Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Escribe su apodo en Modrinth..."
},
"friends.add-friend.username.title": {
"message": "¿Cuál es el apodo de tu amigo en Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Añade amigos</link> para ver a qué están jugando."
},
"friends.friend.cancel-request": {
"message": "Cancelar petición"
},
"friends.friend.remove-friend": {
"message": "Eliminar amigo"
},
"friends.friend.request-sent": {
"message": "Petición de amistad enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Activos"
},
"friends.heading.offline": {
"message": "Desconectados"
},
"friends.heading.online": {
"message": "Conectados"
},
"friends.heading.pending": {
"message": "Pendientes"
},
"friends.no-friends-match": {
"message": "Ningún apodo concuerda con ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Buscando amigos..."
},
"friends.section.heading": {
"message": "{title}: {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicia sesión en una cuenta Modrinth</link> para añadir amigos y ver a qué están jugando."
},
"instance.add-server.add-and-play": {
"message": "Añadir y jugar"
},
@@ -141,7 +204,7 @@
"message": "No puedes duplicar mientras se instala."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Duplicar instancia."
"message": "Duplicar instancia"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Crea una copia de esta instancia, incluyendo mundos, configuraciones, mods, etc."
@@ -216,7 +279,7 @@
"message": "Instancia"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} para Minecraft {game_version} ya esta instalada"
"message": "{platform} {version} para Minecraft {game_version} ya está instalada"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "La versión vanilla {game_version} ya está instalada"

View File

@@ -23,32 +23,116 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng resource"
},
"app.update-toast.body": {
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko pagsara ng Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o mamaya nalang sa pagsara ng Modrinth App."
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko pagsara ng Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magagamit na ngayon ang Modrinth App v{version}! Hindi namin dinanload kaagad dahil naka-metro ang inyong network."
},
"app.update-toast.changelog": {
"message": "Changelog"
},
"app.update-toast.download": {
"message": "I-download ({size})"
},
"app.update-toast.downloading": {
"message": "Nagda-download..."
},
"app.update-toast.reload": {
"message": "Mag-reload"
},
"app.update-toast.title": {
"message": "May bagong update"
},
"app.update-toast.title.download-complete": {
"message": "Nakumpleto ang pagdownload"
},
"app.update.complete-toast.text": {
"message": "Magpindot rito upang matingnan ang changelog."
},
"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"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {{count}} other {{count} na}} hiling na makipagkaibigan"
},
"friends.add-friend.submit": {
"message": "Magpadala ng hiling na makipagkaibigan"
},
"friends.add-friend.title": {
"message": "Pagdaragdag ng kaibigan"
},
"friends.add-friend.username.description": {
"message": "Maaaraing hindi pareho sa kanilang pangalan sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ilagay ang username sa Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Ano ang pangalan ng iyong kaibigan sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Magdagdag ng mga kaibigan</link> upang makita ang kanilang nilalaro!"
},
"friends.friend.cancel-request": {
"message": "Kanselahin ang hiling"
},
"friends.friend.remove-friend": {
"message": "Tanggalin ang kaibing"
},
"friends.friend.request-sent": {
"message": "Ipinadala na ang hiling na makipagkaibigan"
},
"friends.friend.view-profile": {
"message": "Tingnan ang profile"
},
"friends.heading": {
"message": "Mga kaibigan"
},
"friends.heading.active": {
"message": "Aktibo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Nakabinbin"
},
"friends.no-friends-match": {
"message": "Walang kaibigang tumugma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Hanapin ang mga kaibigan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Mag-sign-in sa Modrinth account</link> upang maidagdag ang mga kaibigan at malaman ang kanilang nilalaro!"
},
"instance.add-server.add-and-play": {
"message": "Idagdag at laruin"
},
"instance.add-server.add-server": {
"message": "Idagdag ang serber"
"message": "Idagdag ang server"
},
"instance.add-server.resource-pack.disabled": {
"message": "Hindi pinahihintulotan"
@@ -60,10 +144,10 @@
"message": "Magpahintulot"
},
"instance.add-server.title": {
"message": "Magdagdag ng serber"
"message": "Magdagdag ng server"
},
"instance.edit-server.title": {
"message": "Baguhin ang serber"
"message": "Baguhin ang server"
},
"instance.edit-world.hide-from-home": {
"message": "Huwag ipakita sa Home na pahina"
@@ -75,7 +159,7 @@
"message": "Minecraft na Mundo"
},
"instance.edit-world.reset-icon": {
"message": "Walain ang ikono"
"message": "I-reset ang ikono"
},
"instance.edit-world.title": {
"message": "Baguhin ang mundo"
@@ -86,15 +170,21 @@
"instance.filter.updates-available": {
"message": "May bagong mga update"
},
"instance.server-modal.address": {
"message": "Adres"
},
"instance.server-modal.name": {
"message": "Pangalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft na Serber"
"message": "Minecraft Server"
},
"instance.server-modal.resource-pack": {
"message": "Resource pack"
},
"instance.settings.tabs.general": {
"message": "General"
},
"instance.settings.tabs.general.delete": {
"message": "I-delete ang instansiya"
},
@@ -104,6 +194,18 @@
"instance.settings.tabs.general.delete.description": {
"message": "Permanenteng matatanggal ang instansiya sa iyong device, kasali na ang iyong mga mundo, konpigurasyon, at lahat ng nakainstall na kontento. Mag-ingat, kapag magtanggal ka ng instansiya ay hindi na ito mababawi."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Nagde-delete..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "I-duplicate"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Hindi makaka-duplicate habang nag-i-install."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "I-duplicate ang instansiya"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Gagawan ng kopya ng instansiyang ito, kasali na ang mga mundo, konpigurasyon, mods, at iba pa."
},
@@ -113,19 +215,259 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tanggalin ang ikono"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Palitan ang ikono"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pumili ng ikono"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga grupo ng librerya"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Gumawa ng bagong grupo"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Binibigyan ng mga grupo ng librerya na iyong maayos ang iyong mga instansiya sa iba't-ibang pangkat in iyong librerya."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ilagay ang pangalan ng grupo"
},
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga launch hook"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga custom na launch hook"
},
"instance.settings.tabs.hooks.description": {
"message": "Binibigyan-daan ng mga hook ang mga ekspertong user na makapagtakbo ng mga system command bago at pagkatapos ma-launch ang laro."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exist"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipapatakbo pagkatapos magsara ang laro."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ilagay ang post-exit command..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipapatakbo bago mai-launch ang instansiya."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ilagay ang pre-launch command..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga launch hook ng laro"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Wrapper command sa pag-launch ng Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ilagay ang wrapper command..."
},
"instance.settings.tabs.installation": {
"message": "Instalasyon"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Naka-install naman ang {platform} {version} para sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Naka-install naman ang Vanilla {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Palitan ang bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "I-install"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Ini-install"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Nagfe-fetch ng mga bersiyon ng modpack"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Ini-install ang bagong bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Kasalukuyang naka-install"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Impormasyon sa pagdebug:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon ng laro"
},
"instance.settings.tabs.installation.install": {
"message": "I-install"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nag-i-install ngayon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon ng {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Hindi magagamit ang {loader} sa Minecraft {version}. Sumubok ng ibang mod loader."
},
"instance.settings.tabs.installation.platform": {
"message": "Plataporma"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "I-reinstall ang modpack"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Ini-re-reinstall ang modpack"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Ang pagrere-install ay maaaring mare-reset ang lahat ng na-install o binago na kontento sa kung anong ibibigay ng modpack, tatanggalin ang mga mods o kontentong idinagdag mo lalo na ang mga orihinal na modpack. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa instansiya, ngunit kung dumedepende na ang iyong mundo sa karagdagang kontento, maaari nitong masira ang mga umiiral na mundo."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "I-reset ang mga kontento ng instansiya sa orihinal niyang estado, tatanggalin ang mga mods o kontentong idinagdag mo lalo na ang mga orihinal na modpack."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "I-reinstall ang modpack"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Ayusin"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Inaayos"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pagre-repair, mare-reinstall ang mga dependency ng Minecraft at maghahanap ng mga kurapsiyon. Maaaring maresolbe nito ang mga isyu kung hindi malu-launch ang laro dahil sa mga launcher-related error, ngunit hindi nito mareresolbe ang mga isyu o pag-crash na dulot nga mga na-install na mod."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Ayusin ang instansiya?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Inaayos ngayon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasalukuyan i-reset"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang lahat ng bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "palitan ang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "i-install"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "i-reinstall"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "ayusin"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Hindi makaka-{action} habang nag-i-install"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Hindi makaka-{action} habang nasa offline"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Hindi makaka-{action} habang nag-aayos"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(hindi kilalang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "I-unlink sa instansiya"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kapag ipagpatuloy mo, hindi mo na itong mai-link muli ng hindi gagawa ng bagong instansiya. Hindi ka makakatanggap ng mga update ng modpack at magiging normal na itong."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Ang instansiyang ito ay naka-link sa isang modpack, ibig sabihin ang mga mod ay hindi mai-update at hindi mo mapapalitan ang mod loader o ang bersiyon ng Minecraft."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "I-unlink sa modpack"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga hook"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga java argument"
},
"instance.settings.tabs.java.java-installation": {
"message": "Instalasyon ng Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang inilaan"
},
"instance.settings.tabs.window": {
"message": "Window"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga setting sa custom na window"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Fullscreen"
},
"instance.settings.tabs.window.height": {
"message": "Taas"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ilagay ang taas..."
},
"instance.settings.tabs.window.width": {
"message": "Lapad"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ilagay ang lapad..."
},
"instance.settings.title": {
"message": "Mga Setting"
},
"instance.worlds.a_minecraft_server": {
"message": "Isang Minecraft Server"
},
"instance.worlds.copy_address": {
"message": "Kopyahin ang adres"
},
"instance.worlds.dont_show_on_home": {
"message": "Huwag ipakita sa Home"
},
"instance.worlds.filter.available": {
"message": "Magagamit"
},
"instance.worlds.hardcore": {
"message": "Modong eksperto"
},
"instance.worlds.play_instance": {
"message": "Laruin ang instansiya"
},
"instance.worlds.type.server": {
"message": "Server"
},
"instance.worlds.type.singleplayer": {
"message": "Pang-isahang laro"
},
"instance.worlds.view_instance": {
"message": "Tingnan ang instansiya"
},
"instance.worlds.world_in_use": {
"message": "Ginagamit ang mundo"
},
"search.filter.locked.instance.sync": {
"message": "Maki-sync sa instansiya"
}
}

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Mode développeur activé."
},
"app.settings.downloading": {
"message": "Téléchargement de la version {version}"
},
"app.settings.tabs.appearance": {
"message": "Apparence"
},
@@ -20,6 +23,21 @@
"app.settings.tabs.resource-management": {
"message": "Gestion des ressources"
},
"app.update-toast.body": {
"message": "L'application Modrinth v{version} est prêtes à être installé ! Relancez l'application pour faire la mise à jour maintenant ou la mise à jour se fera automatiquement lorsque vous fermerez l'application Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "L'application Modrinth v{version} a finis d'être téléchargé ! Relancez l'application pour faire la mise à jour maintenant ou la mise à jour se fera automatiquement lorsque vous fermerez l'application Modrinth."
},
"app.update-toast.body.metered": {
"message": "L'application Modrinth v{version} est disponible dès maintenant ! Lorsque vous êtes sur un réseau limité ou en donnée mobile, nous ne téléchargerons pas les mises à jour automatiquement."
},
"app.update-toast.changelog": {
"message": "Notes de changement"
},
"app.update-toast.download": {
"message": "Télécharger ({size})"
},
"app.update-toast.downloading": {
"message": "Téléchargement..."
},
@@ -32,9 +50,84 @@
"app.update-toast.title.download-complete": {
"message": "Téléchargement terminé"
},
"app.update.complete-toast.text": {
"message": "Cliquez ici pour voir les changements récents."
},
"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"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural,one {demande}other {demandes}} d'ami"
},
"friends.add-friend.submit": {
"message": "Envoyer une demande d'ami"
},
"friends.add-friend.title": {
"message": "Ajouter un ami"
},
"friends.add-friend.username.description": {
"message": "Ça peut être différent de son pseudo Minecraft !"
},
"friends.add-friend.username.placeholder": {
"message": "Entrez un pseudo Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Quel est le pseudo Modrinth de votre ami ?"
},
"friends.add-friends-to-share": {
"message": "<link>Ajouter des amis</link> pour voir à quoi ils jouent !"
},
"friends.friend.cancel-request": {
"message": "Annuler la demande"
},
"friends.friend.remove-friend": {
"message": "Supprimer l'ami"
},
"friends.friend.request-sent": {
"message": "La demande d'ami a été envoyé"
},
"friends.friend.view-profile": {
"message": "Voir le profile"
},
"friends.heading": {
"message": "Amis"
},
"friends.heading.active": {
"message": "Actif"
},
"friends.heading.offline": {
"message": "Hors ligne"
},
"friends.heading.online": {
"message": "En ligne"
},
"friends.heading.pending": {
"message": "En attente"
},
"friends.no-friends-match": {
"message": "Aucuns amis ne correspondent à \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Chercher des amis..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Créer un compte Modrinth</link> pour ajouter des amis et voir à quoi ils jouent !"
},
"instance.add-server.add-and-play": {
"message": "Ajouter et jouer"
},

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "מצב מפתח מופעל."
},
"app.settings.downloading": {
"message": "מוריד גרסה {version}"
},
"app.settings.tabs.appearance": {
"message": "מראה"
},
@@ -20,6 +23,57 @@
"app.settings.tabs.resource-management": {
"message": "ניהול משאבים"
},
"app.update-toast.body": {
"message": "Modrinth App גרסה: {version} מוכנה להורדה!\nרענן כדי להוריד עכשיו, או באופן אוטומטי כאשר תסגור את האפליקציה."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App גרסה {version} סיימה את תהליך ההורדה. רענן כדי לעדכן עכשיו, או באופן אוטומטי כאשר תסגור את האפליקציה."
},
"app.update-toast.body.metered": {
"message": "אפליקצית מודרינת' גרסה {version} זמינה עכשיו! מכיוון שאתה על נתונים, אנחנו לא הורדנו אותה אוטומטית."
},
"app.update-toast.changelog": {
"message": "יומן שינויים"
},
"app.update-toast.download": {
"message": "הורד ({size})"
},
"app.update-toast.downloading": {
"message": "מוריד..."
},
"app.update-toast.reload": {
"message": "רענן"
},
"app.update-toast.title": {
"message": "עדכונים זמינים"
},
"app.update-toast.title.download-complete": {
"message": "הורדה הושלמה"
},
"app.update.complete-toast.text": {
"message": "לחץ כאן כדי לראות את יומן השינויים."
},
"app.update.complete-toast.title": {
"message": "גרסה {version} הותקנה בהצלחה!"
},
"app.update.download-update": {
"message": "הורד עדכון"
},
"app.update.downloading-update": {
"message": "מוריד עדכון ({percent}%)"
},
"app.update.reload-to-update": {
"message": "רענן בכדי להתקין את העדכונים"
},
"friends.add-friends-to-share": {
"message": "<link>הוסף חברים</link> כדי לראות במה הם משחקים!"
},
"friends.heading": {
"message": "חברים"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"instance.add-server.add-and-play": {
"message": "הוסף ושחק"
},
@@ -51,7 +105,7 @@
"message": "עולם מיינקראפט"
},
"instance.edit-world.reset-icon": {
"message": יפוס אייקון"
"message": פס סמל"
},
"instance.edit-world.title": {
"message": "ערוך עולם"
@@ -84,7 +138,7 @@
"message": "מחק התקנה"
},
"instance.settings.tabs.general.delete.description": {
"message": "מוחק לצמיתות את התקנה זו מהמכשיר שלך, כולל העולמות שלך, הגדרות, וכל התוכן המותקן. תיזהר, מכיוון שלאחר מחיקת התקנה אין דרך להחזיר אותה."
"message": "מוחק לצמיתות את התקנה זו מהמכשיר שלך, כולל העולמות שלך, הגדרות, וכל התוכן המותקן. שים לב, לאחר מחיקת ההתקנה אין דרך להחזיר אותה."
},
"instance.settings.tabs.general.deleting.button": {
"message": "מוחק..."
@@ -99,19 +153,19 @@
"message": "שכפל התקנה"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "יוצר עותק של התקנה זו, כולל עולם, הגדרות, מודים, וכו."
"message": "יוצר עותק של התקנה זו, כולל עולמות, הגדרות, מודים, וכדומה."
},
"instance.settings.tabs.general.edit-icon": {
"message": "ערוך אייקון"
"message": "ערוך סמל"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "הסר אייקון"
"message": "הסר סמל"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "החלף אייקון"
"message": "החלף סמל"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "בחר אייקון"
"message": "בחר סמל"
},
"instance.settings.tabs.general.library-groups": {
"message": "קבוצות ספרייה"
@@ -120,7 +174,7 @@
"message": "צור קבוצה חדשה"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "קבוצות ספרייה מאפשרות לך לארגן את ההתקנות שלך לפי חלקים שונים."
"message": "קבוצות ספרייה מאפשרות לך לארגן את ההתקנות שלך לחלקים שונים בספרייה שלך."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "הכנס שם קבוצה"
@@ -132,7 +186,7 @@
"message": "פעולות בעת הפעלה"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "מותאם אישית"
"message": "פעולות בעת הפעלה מותאמות אישית"
},
"instance.settings.tabs.hooks.description": {
"message": "פעולות בעת הפעלה מאפשרות למשתמשים מתקדמים להריץ פקודות מערכת מסוימות לפני ואחרי שהמשחק מופעל."
@@ -147,16 +201,16 @@
"message": "הכנס פקודה לאחר יציאה..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "טרום-השקה"
"message": "טרום-הפעלה"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "מופעל לפני שהאינסטנס מתחיל."
"message": "מופעל לפני שההתקנה מופעלת."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "הכנס פקודה לפני ההפעלה..."
},
"instance.settings.tabs.hooks.title": {
"message": קודות הפעלה מותאמות"
"message": עולות הפעלה"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "מעטפת"
@@ -174,7 +228,7 @@
"message": "{platform} {version} בשביל מיינקראפט {game_version} כבר מותקן"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "ונילה {game_version} כבר מותקן"
"message": "וונילה {game_version} כבר מותקן"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "שנה גרסה"
@@ -186,7 +240,7 @@
"message": "מתקין"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "אחזור גרסאות מודפאקים"
"message": "מאחזר גרסאות חבילת מודים"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "מתקין גרסה חדשה"
@@ -198,7 +252,7 @@
"message": "מידע ניפוי שגיאות:"
},
"instance.settings.tabs.installation.fetching-modpack-details": {
"message": "אחזור פרטי מודפאקים"
"message": "מאחזר פרטי חבילת מודים"
},
"instance.settings.tabs.installation.game-version": {
"message": "גרסת משחק"
@@ -216,13 +270,13 @@
"message": "מיינקראפט {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "לא ניתן לאחזר את פרטי המודפאק המקושר. אנא בדוק את חיבור האינטרנט שלך."
"message": "לא ניתן לאחזר את פרטי חבילת המודים המקושרת. אנא בדוק את חיבור האינטרנט שלך."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader}\" אינו זמין עבור מיינקראפט {version}. נסה מעלה מודים אחר."
"message": "{loader} אינו זמין עבור מיינקראפט {version}. אנא נסה טוען מודים אחר."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "אינסטנס זה מקושר למודפק, אך המודפק לא נמצא ב-Modrinth."
"message": "התקנה זאת מקושרת לחבילת מודים, אך חבילת המודים לא נמצאה במודרינת'."
},
"instance.settings.tabs.installation.platform": {
"message": "פלטפורמה"
@@ -234,13 +288,13 @@
"message": "מתקין מחדש את חבילת המודים"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "התקנה מחדש תאפס את כל התוכן המותקן או ששונה, ותחזיר אותו למצב שסופק על ידי חבילת המודים, תוך הסרת כל מוד או תוכן שהוספתם מעבר להתקנה המקורית. פעולה זו עשויה לתקן תקלות שנגרמו משינויים במופע, אך אם העולמות שלכם תלויים בתוכן נוסף שהותקן, היא עלולה לשבש אותם."
"message": "התקנה מחדש תאפס את כל התוכן המותקן או ששונה, ותחזיר אותו למצב שסופק על ידי חבילת המודים, תוך הסרת כל מוד או תוכן שהוספתם מעבר להתקנה המקורית. פעולה זו עשויה לתקן תקלות שנגרמו משינויים בהתקנה, אך אם העולמות שלכם תלויים בתוכן נוסף שהותקן, היא עלולה לשבש אותם."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "האם אתה בטוח שברצונך להתקין מחדש instance זה?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "מאפס את התוכן שנמצא ב-instance למצבו המקורי, תוך הסרת כל מוד או תוכן שהוספת מעבר למודפק המקורי."
"message": "מאפס את התוכן שנמצא בהתקנה למצבו המקורי, תוך הסרת כל מוד או תוכן ששונה מעבר לחבילת המודים המקורית."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "התקן מחדש חבילת מודים"
@@ -294,16 +348,16 @@
"message": "לבטל את הקישור של ההתקנה הזו"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "אם תמשיך, לא תוכל לקשר אותו מחדש מבלי ליצור instance חדש לחלוטין. לא תקבל עוד עדכונים למודפקים, והוא יהפוך לרגיל."
"message": "אם תמשיך, לא תוכל לקשר אותה מחדש מבלי ליצור התקנה חדשה לחלוטין. לא תקבל עוד עדכונים לחבילת המודים, והיא תהפוך להתקנה רגילה."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "האם אתה בטוח שברצונך לנתק את הקישור ל-instance הזה?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Instance זה מקושר ל-modpack, מה שאומר שלא ניתן לעדכן מודים ולא ניתן לשנות את ה-mod loader או את גרסת מיינקראפט. הניתוק יבטל לצמיתות את הקישור של instance זה מה-modpack."
"message": "התקנה זאת מקושרת לחבילת מודים, מה שאומר שלא ניתן לעדכן מודים ולא ניתן לשנות את טוען המודים או את גרסת המיינקראפט. הניתוק יבטל לצמיתות את הקישור של התקנה זאת לחבילת המודים."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "נתק קישור מ-modpack"
"message": "נתק קישור מחבילת מודים"
},
"instance.settings.tabs.java": {
"message": "ג'אווה וזיכרון"
@@ -383,6 +437,12 @@
"instance.worlds.no_contact": {
"message": "לא ניתן ליצור קשר עם השרת"
},
"instance.worlds.no_server_quick_play": {
"message": "אתה יכול רק לקפוץ ישירות לשרתים מהגרסא אלפא 1.0.5 ואלך"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "אתה יכול לקפוץ ישירות לעולמות החל מהגרסא 1.20 ומעלה"
},
"instance.worlds.play_instance": {
"message": "שחק בהתקנה"
},
@@ -404,6 +464,9 @@
"search.filter.locked.instance-game-version.title": {
"message": "גרסאת המשחק מסופקת על ידי ההתקנה"
},
"search.filter.locked.instance-loader.title": {
"message": "מטען מודים מסופק ע\"י ההתקן"
},
"search.filter.locked.instance.sync": {
"message": "סנכרן עם התקנה"
}

View File

@@ -3,7 +3,7 @@
"message": "Fejlesztői mód bekapcsolva."
},
"app.settings.downloading": {
"message": "Letöltés v{version}"
"message": "v{version} letöltése"
},
"app.settings.tabs.appearance": {
"message": "Megjelenés"
@@ -60,11 +60,74 @@
"message": "Frissítés letöltése"
},
"app.update.downloading-update": {
"message": "Frissítés folyamatban ({percent}%)"
"message": "Frissítés letöltése ({percent}%)"
},
"app.update.reload-to-update": {
"message": "A telepítéshez újraindítás szükséges"
},
"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"
},
"friends.add-friend.title": {
"message": "Barát hozzáadása"
},
"friends.add-friend.username.description": {
"message": "Más is lehet, mint a Minecraft felhasználóneve!"
},
"friends.add-friend.username.placeholder": {
"message": "Barát Modrinth felhasználóneve..."
},
"friends.add-friend.username.title": {
"message": "Mi a barátod Modrinth felhasználóneve?"
},
"friends.add-friends-to-share": {
"message": "<link>Vegyél fel barátokat</link>, hogy lásd mivel játszanak!"
},
"friends.friend.cancel-request": {
"message": "Barátkérelem visszavonása"
},
"friends.friend.remove-friend": {
"message": "Barát eltávolítása"
},
"friends.friend.request-sent": {
"message": "Barátkérelem elküldve"
},
"friends.friend.view-profile": {
"message": "Profil megtekintése"
},
"friends.heading": {
"message": "Barátok"
},
"friends.heading.active": {
"message": "Aktív"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Jóváhagyásra vár"
},
"friends.no-friends-match": {
"message": "Nincsen \"{query}\" nevű barátod"
},
"friends.search-friends-placeholder": {
"message": "Keresés a barátaid között..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Lépj be Modrinth fiókba</link>, hogy felvehess barátokat és lásd mivel játszanak!"
},
"instance.add-server.add-and-play": {
"message": "Hozzáadás és játék"
},

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
"app.settings.downloading": {
"message": "Mengunduh v{version}"
},
"app.settings.tabs.appearance": {
"message": "Tampilan"
},
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "Manajemen sumber"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} sudah tersedia! Karena Anda saat ini sedang berada dalam jaringan terukur, kami tidak mengunduhnya secara otomatis."
},
"app.update-toast.changelog": {
"message": "Log perubahan"
},
"app.update-toast.download": {
"message": "Unduh ({size})"
},
"app.update-toast.downloading": {
"message": "Mengunduh..."
},
"app.update-toast.reload": {
"message": "Muat ulang"
},
"app.update-toast.title": {
"message": "Pembaruan tersedia"
},
"app.update-toast.title.download-complete": {
"message": "Selesai mengunduh"
},
"app.update.complete-toast.text": {
"message": "Klik di sini untuk melihat log perubahan."
},
"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"
},
"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"
},
"friends.add-friend.title": {
"message": "Menambah teman"
},
"friends.add-friend.username.description": {
"message": "Ia mungkin memiliki nama yang berbeda dari nama pengguna Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Masukkan nama pengguna Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Apakah nama pengguna Modrinth teman Anda?"
},
"friends.add-friends-to-share": {
"message": "<link>Tambah teman</link> untuk melihat apa yang mereka mainkan!"
},
"friends.friend.cancel-request": {
"message": "Batalkan permintaan"
},
"friends.friend.remove-friend": {
"message": "Hapus teman"
},
"friends.friend.request-sent": {
"message": "Permintaan teman dikirim"
},
"friends.friend.view-profile": {
"message": "Lihat profil"
},
"friends.heading": {
"message": "Teman"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Luring"
},
"friends.heading.online": {
"message": "Daring"
},
"friends.heading.pending": {
"message": "Menunggu"
},
"friends.no-friends-match": {
"message": "Tidak ada teman dengan nama \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Cari teman..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Masuk ke akun Modrinth</link> untuk menambah teman dan melihat apa yang mereka mainkan!"
},
"instance.add-server.add-and-play": {
"message": "Tambah dan mainkan"
},

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Ricarica per installare aggiornamento"
},
"friends.action.add-friend": {
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {richiesta} other {richieste}} d'amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
},
"friends.add-friend.title": {
"message": "Stringendo l'amicizia"
},
"friends.add-friend.username.description": {
"message": "Potrebbe essere diverso dal nome utente di Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Inserisci il nome utente Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Con quale utente Modrinth vuoi stringere l'amicizia?"
},
"friends.add-friends-to-share": {
"message": "<link>Stringi un'amicizia</link> per sapere a cosa stanno giocando!"
},
"friends.friend.cancel-request": {
"message": "Annulla richiesta"
},
"friends.friend.remove-friend": {
"message": "Annulla amicizia"
},
"friends.friend.request-sent": {
"message": "Richiesta d'amicizia inviata"
},
"friends.friend.view-profile": {
"message": "Visita profilo"
},
"friends.heading": {
"message": "Amicizie"
},
"friends.heading.active": {
"message": "Attivo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "In sospeso"
},
"friends.no-friends-match": {
"message": "Nessuna amicizia pertinente a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Accedi all'account Modrinth</link> per stringere amicizie e sapere a cosa stanno giocando!"
},
"instance.add-server.add-and-play": {
"message": "Aggiungi e gioca"
},

View File

@@ -2,8 +2,11 @@
"app.settings.developer-mode-enabled": {
"message": "開発者モードがオンになっています。"
},
"app.settings.downloading": {
"message": "v{version}をダウンロード中"
},
"app.settings.tabs.appearance": {
"message": "表示設定"
"message": "外観"
},
"app.settings.tabs.default-instance-options": {
"message": "インスタンスの基本設定"
@@ -12,14 +15,119 @@
"message": "機能設定"
},
"app.settings.tabs.java-installations": {
"message": "Javaのインストール設定"
"message": "Javaのインストール"
},
"app.settings.tabs.privacy": {
"message": "プライバシー設定"
"message": "プライバシー"
},
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.update-toast.body": {
"message": "Modrinth App v{version}は今すぐインストールできます!再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version}のダウンロードが完了しました。再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version}は今すぐダウンロードできます!従量課金制ネットワークを使用しているため自動でダウンロードはされていません。"
},
"app.update-toast.changelog": {
"message": "更新履歴"
},
"app.update-toast.download": {
"message": "ダウンロード ({size})"
},
"app.update-toast.downloading": {
"message": "ダウンロード中..."
},
"app.update-toast.reload": {
"message": "再起動"
},
"app.update-toast.title": {
"message": "アップデートが可能"
},
"app.update-toast.title.download-complete": {
"message": "ダウンロード完了"
},
"app.update.complete-toast.text": {
"message": "クリックすると更新履歴を表示できます。"
},
"app.update.complete-toast.title": {
"message": "バージョン {version} のインストールが正常に完了しました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
},
"app.update.downloading-update": {
"message": "アップデートをダウンロード中 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "再起動して今すぐ更新"
},
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
"friends.add-friend.title": {
"message": "フレンドを追加中"
},
"friends.add-friend.username.description": {
"message": "これはMinecraftユーザーネームと違う可能性があります"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinthユーザーネームを入力..."
},
"friends.add-friend.username.title": {
"message": "あなたのフレンドのModrinthユーザーネームは何ですか"
},
"friends.add-friends-to-share": {
"message": "<link>友達を追加</link>して、彼らが何をしているか見てみよう!"
},
"friends.friend.cancel-request": {
"message": "申請をキャンセル"
},
"friends.friend.remove-friend": {
"message": "フレンドを削除"
},
"friends.friend.request-sent": {
"message": "フレンド申請が送信されました"
},
"friends.friend.view-profile": {
"message": "プロフィールを表示"
},
"friends.heading": {
"message": "フレンド"
},
"friends.heading.active": {
"message": "活動中"
},
"friends.heading.offline": {
"message": "オフライン"
},
"friends.heading.online": {
"message": "オンライン"
},
"friends.heading.pending": {
"message": "保留中"
},
"friends.no-friends-match": {
"message": "フレンド\"{query}\"は見つかりませんでした"
},
"friends.search-friends-placeholder": {
"message": "フレンドを検索…"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Modrinthアカウントにサインイン</link>して友達を追加し、彼らがプレイしているゲームをチェックしよう!"
},
"instance.add-server.add-and-play": {
"message": "追加してプレイ"
},

View File

@@ -23,6 +23,18 @@
"app.settings.tabs.resource-management": {
"message": "리소스 관리"
},
"app.update-toast.changelog": {
"message": "변경사항"
},
"app.update-toast.download": {
"message": "다운로드 ({size})"
},
"app.update-toast.downloading": {
"message": "다운로드 중..."
},
"app.update-toast.reload": {
"message": "리로드"
},
"instance.add-server.add-and-play": {
"message": "추가하고 플레이"
},

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "موات سمولا اونتوق مماسڠ کمس کيني"
},
"friends.action.add-friend": {
"message": "تمبه راکن"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {ڤرمينتاٴن}} راکن"
},
"friends.add-friend.submit": {
"message": "هانتر ڤرمينتاٴن راکن"
},
"friends.add-friend.title": {
"message": "منمبه راکن"
},
"friends.add-friend.username.description": {
"message": "اي موڠکين بربيذا درڤد نام ڤڠݢونا ماٴينکرف‌ت مريک!"
},
"friends.add-friend.username.placeholder": {
"message": "ماسوقکن نام ڤڠݢونا Modrinth..."
},
"friends.add-friend.username.title": {
"message": "اڤاکه نام ڤڠݢونا Modrinth راکن اندا؟"
},
"friends.add-friends-to-share": {
"message": "<link>تمبه راکن</link> اونتوق مليهت اڤ يڠ مريک ماءينکن!"
},
"friends.friend.cancel-request": {
"message": "بطلکن ڤرمينتاٴن"
},
"friends.friend.remove-friend": {
"message": "اليه کلوار راکن"
},
"friends.friend.request-sent": {
"message": "ڤرمينتاٴن راکن تله دهانتر"
},
"friends.friend.view-profile": {
"message": "ليهت ڤروفيل"
},
"friends.heading": {
"message": "راکن"
},
"friends.heading.active": {
"message": "اکتيف"
},
"friends.heading.offline": {
"message": "لوار تالين"
},
"friends.heading.online": {
"message": "دالم تالين"
},
"friends.heading.pending": {
"message": "منوڠݢو"
},
"friends.no-friends-match": {
"message": "تياد راکن يڠ سڤادن دڠن ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "چاري راکن..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>لوݢ ماسوق کأکاٴون Modrinth</link> اونتوق منمبه راکن دان مليهت اڤ يڠ مريک ماءينکن!"
},
"instance.add-server.add-and-play": {
"message": "تمبه دان ماءين"
},

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Muat semula untuk memasang kemas kini"
},
"friends.action.add-friend": {
"message": "Tambah rakan"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} rakan"
},
"friends.add-friend.submit": {
"message": "Hantar permintaan rakan"
},
"friends.add-friend.title": {
"message": "Menambah rakan"
},
"friends.add-friend.username.description": {
"message": "Ia mungkin berbeza daripada nama pengguna Minecraft mereka!"
},
"friends.add-friend.username.placeholder": {
"message": "Masukkan nama pengguna Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Apakah nama pengguna Modrinth rakan anda?"
},
"friends.add-friends-to-share": {
"message": "<link>Tambah rakan</link> untuk melihat apa yang mereka mainkan!"
},
"friends.friend.cancel-request": {
"message": "Batalkan permintaan"
},
"friends.friend.remove-friend": {
"message": "Alih keluar rakan"
},
"friends.friend.request-sent": {
"message": "Permintaan rakan telah dihantar"
},
"friends.friend.view-profile": {
"message": "Lihat profil"
},
"friends.heading": {
"message": "Rakan"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Luar Talian"
},
"friends.heading.online": {
"message": "Dalam Talian"
},
"friends.heading.pending": {
"message": "Menunggu"
},
"friends.no-friends-match": {
"message": "Tiada rakan yang sepadan dengan ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cari rakan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log masuk ke akaun Modrinth</link> untuk menambah rakan dan melihat apa yang mereka mainkan!"
},
"instance.add-server.add-and-play": {
"message": "Tambah dan main"
},

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Ontwikkelaarsmodus ingeschakeld."
},
"app.settings.downloading": {
"message": "v{version} wordt gedownload"
},
"app.settings.tabs.appearance": {
"message": "Uiterlijk"
},
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "Bronnenbeheer"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} is klaar om geïnstalleerd te worden! Herlaad om nu te updaten, of automatisch wanneer je de Modrinth App afsluit."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} is klaar met downloaden. Herlaad om nu te updaten, of automatisch wanneer je de Modrinth App afsluit."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} is nu beschikbaar! Omdat je nu op een netwerk met datalimiet zit, is de download niet automatisch gestart."
},
"app.update-toast.changelog": {
"message": "Changelog"
},
"app.update-toast.download": {
"message": "Download ({size})"
},
"app.update-toast.downloading": {
"message": "Aan het downloaden..."
},
"app.update-toast.reload": {
"message": "Herlaad"
},
"app.update-toast.title": {
"message": "Update beschikbaar"
},
"app.update-toast.title.download-complete": {
"message": "Downloaden voltooid"
},
"app.update.complete-toast.text": {
"message": "Klik hier om de changelog te bekijken."
},
"app.update.complete-toast.title": {
"message": "Versie {version} is succesvol geïnstalleerd!"
},
"app.update.download-update": {
"message": "Download update"
},
"app.update.downloading-update": {
"message": "Update downloaden ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Herlaad om de update te installeren"
},
"friends.action.add-friend": {
"message": "Voeg een vriend toe"
},
"friends.action.view-friend-requests": {
"message": "{count} vriend {count, plural,one {verzoek} other {verzoeken}}"
},
"friends.add-friend.submit": {
"message": "Stuur een vriendschapsverzoek"
},
"friends.add-friend.title": {
"message": "Een vriend toevoegen"
},
"friends.add-friend.username.description": {
"message": "Het kan verschillen van hun Minecraft gebruikersnaam!"
},
"friends.add-friend.username.placeholder": {
"message": "Voer Modrinth gebruikersnaam in..."
},
"friends.add-friend.username.title": {
"message": "Wat is de Modrinth gebruikersnaam van uw vriend?"
},
"friends.add-friends-to-share": {
"message": "<link>Voeg vrienden</link> om te zien wat hun spelen!"
},
"friends.friend.cancel-request": {
"message": "Annuleer verzoek"
},
"friends.friend.remove-friend": {
"message": "Verwijder vriend"
},
"friends.friend.request-sent": {
"message": "Vriendschapsverzoek gestuurd"
},
"friends.friend.view-profile": {
"message": "Bekijk profiel"
},
"friends.heading": {
"message": "Vrienden"
},
"friends.heading.active": {
"message": "Actief"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "In behandeling"
},
"friends.no-friends-match": {
"message": "Geen vrienden die overeenkomen met \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Zoek vrienden..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log in op een Modrinth account</link> om vrienden toe te voegen en te kijken wat zij spelen!"
},
"instance.add-server.add-and-play": {
"message": "Toevoegen en spelen"
},

View File

@@ -24,7 +24,10 @@
"message": "Zarządzanie zasobami"
},
"app.update-toast.body": {
"message": "Nowa wersja Modrinth v{version} jest gotowa do pobrania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz aplikację Modrinth."
"message": "Wersja Modrinth App v{version} jest gotowa do pobrania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Wersja Modrinth App v{version} została pobrana. Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Wersja v{version} Modrinth App jest dostępna! Skoro korzystasz z sieci taryfowej, nie pobraliśmy jej automatycznie."
@@ -62,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Załaduj ponownie, aby zainstalować aktualizację"
},
"friends.action.add-friend": {
"message": "Dodaj znajomego"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {zaproszenie} few {zaproszenia} other {zaproszeń}} do znajomych"
},
"friends.add-friend.submit": {
"message": "Wyślij zaproszenie"
},
"friends.add-friend.title": {
"message": "Dodawanie znajomego"
},
"friends.add-friend.username.description": {
"message": "Może różnić się od nazwy użytkownika Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Wpisz nazwę użytkownika Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Jaka jest nazwa użytkownika Twojego znajomego?"
},
"friends.add-friends-to-share": {
"message": "<link>Dodaj znajomych</link> by widzieć, w co grają!"
},
"friends.friend.cancel-request": {
"message": "Anuluj zaproszenie"
},
"friends.friend.remove-friend": {
"message": "Usuń z znajomych"
},
"friends.friend.request-sent": {
"message": "Wysłano zaproszenie"
},
"friends.friend.view-profile": {
"message": "Pokaż profil"
},
"friends.heading": {
"message": "Znajomi"
},
"friends.heading.active": {
"message": "Aktywny"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Oczekujące"
},
"friends.no-friends-match": {
"message": "Nie znaleziono znajomych pasujących do zapytania \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Szukaj znajomych..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Zaloguj się na konto Modrinth</link> by dodać znajomych i widzieć, w co grają!"
},
"instance.add-server.add-and-play": {
"message": "Dodaj i graj"
},

View File

@@ -1,6 +1,6 @@
{
"app.settings.developer-mode-enabled": {
"message": "Modo desenvolvedor ativado."
"message": "Modo desenvolvedor."
},
"app.settings.downloading": {
"message": "Baixando v{version}"
@@ -54,7 +54,7 @@
"message": "Clique aqui para ver as mudanças."
},
"app.update.complete-toast.title": {
"message": "Versão {version} instalada com sucesso!"
"message": "Versão {version} instalada!"
},
"app.update.download-update": {
"message": "Baixar atualização"
@@ -63,7 +63,70 @@
"message": "Baixando atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarregar para instalar a atualização"
"message": "Recarregue para instalar a atualização"
},
"friends.action.add-friend": {
"message": "Adicionar um amigo"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, =0 {Nenhuma solicitação de amizade} one {{count} solicitação de amizade} other {{count} solicitações de amizade}}"
},
"friends.add-friend.submit": {
"message": "Enviar amizade"
},
"friends.add-friend.title": {
"message": "Adicionando amigo"
},
"friends.add-friend.username.description": {
"message": "Pode ser diferente do nome que ele usa no Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Insira o nome de usuário..."
},
"friends.add-friend.username.title": {
"message": "Qual o nome de usuário do seu amigo no Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Adicione amigos</link> para ver o que eles estão jogando!"
},
"friends.friend.cancel-request": {
"message": "Cancelar solicitação"
},
"friends.friend.remove-friend": {
"message": "Remover amigo"
},
"friends.friend.request-sent": {
"message": "Solicitação de amizade enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Ativo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Pendente"
},
"friends.no-friends-match": {
"message": "Nenhum amigo corresponde a \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Buscar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicie uma sessão com o Modrinth</link> para adicionar amigos e ver o que eles estão jogando!"
},
"instance.add-server.add-and-play": {
"message": "Adicionar e jogar"
@@ -129,7 +192,7 @@
"message": "Excluir instância"
},
"instance.settings.tabs.general.delete.description": {
"message": "Apaga permanente a instância do seu dispositivo, incluindo seus mundos, configurações e todo o conteúdo instalado. Tenha cuidado, porque não será possível recuperá-la após apaga-la."
"message": "Exclui permanentemente uma instância do seu dispositivo, incluindo seus mundos, configurações e todo o conteúdo instalado. Tome cuidado, pois, uma vez excluída, a instância não poderá ser recuperada."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Excluindo..."
@@ -174,13 +237,13 @@
"message": "Nome"
},
"instance.settings.tabs.hooks": {
"message": "Gatilhos de inicialização"
"message": "Ações de inicialização"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Gatilho de inicialização personalizado"
"message": "Ações de inicialização personalizadas"
},
"instance.settings.tabs.hooks.description": {
"message": "Os Gatilhos permitem que usuários mais experientes executem comandos do sistema antes e depois de inicializar o jogo."
"message": "Essas ações permitem que usuários mais experientes executem comandos do sistema antes e depois de inicializar o jogo."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Ao sair"
@@ -189,10 +252,10 @@
"message": "Executado após o jogo fechar."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Insira o comando de pós-saída..."
"message": "Insira o comando a ser executado após o jogo fechar..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Antes de inicializar"
"message": "Pré-inicialização"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Executado antes que a instância seja inicializada."
@@ -201,7 +264,7 @@
"message": "Insira o comando a ser executado antes da inicialização..."
},
"instance.settings.tabs.hooks.title": {
"message": "Gatilhos de inicialização do jogo"
"message": "Ações de inicialização do jogo"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Comando auxiliar"
@@ -210,7 +273,7 @@
"message": "Comando auxiliar para iniciar o Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Insira um comando auxiliar..."
"message": "Insira um comando..."
},
"instance.settings.tabs.installation": {
"message": "Instalação"
@@ -237,7 +300,7 @@
"message": "Instalando nova versão"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Instalado atualmente"
"message": "Versão instalada"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Informação de depuração:"
@@ -357,7 +420,7 @@
"message": "Variáveis de ambiente"
},
"instance.settings.tabs.java.hooks": {
"message": "Gatilhos"
"message": "Ações"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Argumentos do Java"

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Modo de desenvolvedor ativado."
},
"app.settings.downloading": {
"message": "A transferir v{version}"
},
"app.settings.tabs.appearance": {
"message": "Aparência"
},
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "Gestão de recursos"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} está pronta para ser instalada! Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} acabou de ser transferida. Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} está disponível! Como estás numa rede com tráfego limitado, não a transferimos automaticamente."
},
"app.update-toast.changelog": {
"message": "Lista de alterações"
},
"app.update-toast.download": {
"message": "Transferir ({size})"
},
"app.update-toast.downloading": {
"message": "A transferir..."
},
"app.update-toast.reload": {
"message": "Recarregar"
},
"app.update-toast.title": {
"message": "Atualização disponível"
},
"app.update-toast.title.download-complete": {
"message": "Transferência concluída"
},
"app.update.complete-toast.text": {
"message": "Clica aqui para ver a lista de alterações."
},
"app.update.complete-toast.title": {
"message": "Versão {version} foi instalada com sucesso!"
},
"app.update.download-update": {
"message": "Transferir atualização"
},
"app.update.downloading-update": {
"message": "A transferir atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarrega para instalar a atualização"
},
"friends.action.add-friend": {
"message": "Adicionar um amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural,one {pedido} other {pedidos}} de amizade"
},
"friends.add-friend.submit": {
"message": "Enviar pedido de amizade"
},
"friends.add-friend.title": {
"message": "Adicionar um amigo"
},
"friends.add-friend.username.description": {
"message": "Pode ser diferente do nome no Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Insere o nome do utilizador no Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Qual é o nome de utilizador do teu amigo no Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Adiciona amigos</link> para ver o que estão a jogar!"
},
"friends.friend.cancel-request": {
"message": "Cancelar pedido"
},
"friends.friend.remove-friend": {
"message": "Remover amigo"
},
"friends.friend.request-sent": {
"message": "Pedido de amizade enviado"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Ativo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Pendente"
},
"friends.no-friends-match": {
"message": "Nenhum amigo corresponde a \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Procurar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicia a sessão com uma conta Modrinth</link> para adicionar amigos e ver o que estão a jogar!"
},
"instance.add-server.add-and-play": {
"message": "Adicionar e jogar"
},

View File

@@ -12,7 +12,7 @@
"message": "Предустановки"
},
"app.settings.tabs.feature-flags": {
"message": "Экспериментальные функции"
"message": "Флаги функций"
},
"app.settings.tabs.java-installations": {
"message": "Установки Java"
@@ -30,7 +30,7 @@
"message": "Скачивание Modrinth App v{version} завершено. Перезапустите приложение, чтобы обновить его, или оно обновится автоматически при закрытии."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} доступно к скачиванию! Используется лимитное подключение, поэтому скачивание не началось автоматически."
"message": "Modrinth App v{version} доступно для скачивания! Используется лимитное подключение, поэтому скачивание не началось автоматически."
},
"app.update-toast.changelog": {
"message": "Список изменений"
@@ -63,7 +63,70 @@
"message": "Скачивание обновления ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Перезапустить для обновления"
"message": "Перезапустить и обновить"
},
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {запрос} few {запроса} many {запросов} other {запроса}} дружбы"
},
"friends.add-friend.submit": {
"message": "Отправить запрос дружбы"
},
"friends.add-friend.title": {
"message": "Добавление в друзья"
},
"friends.add-friend.username.description": {
"message": "Оно может быть не таким, как в Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Введите имя на Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Какое имя у друга на Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Добавьте друзей</link>, чтобы знать, во что они играют!"
},
"friends.friend.cancel-request": {
"message": "Отменить запрос"
},
"friends.friend.remove-friend": {
"message": "Удалить из друзей"
},
"friends.friend.request-sent": {
"message": "Отправлен запрос дружбы"
},
"friends.friend.view-profile": {
"message": "Открыть профиль"
},
"friends.heading": {
"message": "Друзья"
},
"friends.heading.active": {
"message": "В игре"
},
"friends.heading.offline": {
"message": "Не в сети"
},
"friends.heading.online": {
"message": "В сети"
},
"friends.heading.pending": {
"message": "В ожидании"
},
"friends.no-friends-match": {
"message": "Нет друзей по запросу «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Поиск по друзьям..."
},
"friends.section.heading": {
"message": "{title} — {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Войдите в Modrinth</link>, чтобы добавлять друзей и знать, во что они играют!"
},
"instance.add-server.add-and-play": {
"message": "Добавить и играть"
@@ -84,7 +147,7 @@
"message": "Добавление сервера"
},
"instance.edit-server.title": {
"message": "Изменение сервера"
"message": "Настройка сервера"
},
"instance.edit-world.hide-from-home": {
"message": "Не показывать на главной"
@@ -99,7 +162,7 @@
"message": "Сбросить иконку"
},
"instance.edit-world.title": {
"message": "Изменение информации о мире"
"message": "Изменение мира"
},
"instance.filter.disabled": {
"message": "Отключённые"
@@ -174,13 +237,13 @@
"message": "Название"
},
"instance.settings.tabs.hooks": {
"message": "Настройки запуска"
"message": "Команды запуска"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Изменение команд запуска"
},
"instance.settings.tabs.hooks.description": {
"message": "Позволяют опытным пользователям задать системные команды, выполняемые перед запуском и после закрытия игры."
"message": "Позволяет опытным пользователям задать системные команды, выполняемые до и после запуска игры."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "После выхода"
@@ -201,13 +264,13 @@
"message": "Введите команду перед запуском..."
},
"instance.settings.tabs.hooks.title": {
"message": "Настройки запуска игры"
"message": "Команды запуска игры"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Обёртка"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Команда обёртки для запуска Minecraft."
"message": "Команда-обёртка для запуска Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Команда обёртки..."
@@ -252,7 +315,7 @@
"message": "Установить"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Выполняется установка..."
"message": "Выполняется установка"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Версия {loader}"
@@ -303,7 +366,7 @@
"message": "Восстановить сборку?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Выполняется исправление..."
"message": "Выполняется исправление"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Сбросить выбор"
@@ -357,7 +420,7 @@
"message": "Переменные среды"
},
"instance.settings.tabs.java.hooks": {
"message": "Настройки запуска"
"message": "Команды запуска"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Аргументы Java"

View File

@@ -2,6 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "Utvecklarläge aktiverat."
},
"app.settings.downloading": {
"message": "Ladda ner v{version}"
},
"app.settings.tabs.appearance": {
"message": "Utseende"
},
@@ -20,6 +23,111 @@
"app.settings.tabs.resource-management": {
"message": "Resurshantering"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} är redo att laddas ner! Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} har laddats ner. Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} är nu tillgänglig! Eftersom du använder ett nätverk med datatrafikbegränsningar har vi inte laddat ner det automatiskt."
},
"app.update-toast.changelog": {
"message": "Ändringslogg"
},
"app.update-toast.download": {
"message": "Ladda ner ({size})"
},
"app.update-toast.downloading": {
"message": "Laddar ner..."
},
"app.update-toast.reload": {
"message": "Ladda om"
},
"app.update-toast.title": {
"message": "Uppdatering tillgänglig"
},
"app.update-toast.title.download-complete": {
"message": "Nedladdning slutförd"
},
"app.update.complete-toast.text": {
"message": "Tryck här för att visa ändringsloggen."
},
"app.update.complete-toast.title": {
"message": "Version {version} har installerats!"
},
"app.update.download-update": {
"message": "Ladda ner uppdatering"
},
"app.update.downloading-update": {
"message": "Laddar ner uppdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ladda om för att installera uppdatering"
},
"friends.action.add-friend": {
"message": "Lägg till en vän"
},
"friends.action.view-friend-requests": {
"message": "{count} vän{count, plural, one {förfrågan} other {förfrågningar}}"
},
"friends.add-friend.submit": {
"message": "Skicka vänförfrågan"
},
"friends.add-friend.title": {
"message": "Lägga till en vän"
},
"friends.add-friend.username.description": {
"message": "Det kan vara annorlunda från deras Minecraft-användarnamn!"
},
"friends.add-friend.username.placeholder": {
"message": "Ange Modrinth-användarnamn..."
},
"friends.add-friend.username.title": {
"message": "Vad är din väns Modrinth-användarnamn?"
},
"friends.add-friends-to-share": {
"message": "<link>Lägg till vänner</link> för att se vad de spelar!"
},
"friends.friend.cancel-request": {
"message": "Avbryt förfrågan"
},
"friends.friend.remove-friend": {
"message": "Ta bort vän"
},
"friends.friend.request-sent": {
"message": "Vänförfrågan skickad"
},
"friends.friend.view-profile": {
"message": "Visa profil"
},
"friends.heading": {
"message": "Vänner"
},
"friends.heading.active": {
"message": "Aktiva"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Väntar"
},
"friends.no-friends-match": {
"message": "Inga vänner matchar ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Sök efter vänner..."
},
"friends.section.heading": {
"message": "{title}: {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Logga in på ett Modrinth-konto</link> för att lägga till vänner och se vad de spelar!"
},
"instance.add-server.add-and-play": {
"message": "Lägg till och spela"
},

View File

@@ -2,9 +2,18 @@
"app.settings.developer-mode-enabled": {
"message": "กำลังอยู่ในโหมดผู้พัฒนา"
},
"app.settings.downloading": {
"message": "ดาวน์โหลด เวอร์ชั่น{version}"
},
"app.settings.tabs.appearance": {
"message": "รูปลักษณ์"
},
"app.settings.tabs.default-instance-options": {
"message": "ตัวเลือกอินสแตนซ์เริ่มต้น"
},
"app.settings.tabs.feature-flags": {
"message": "ระบบควบคุมการเปิดใช้งานฟีเจอร์"
},
"app.settings.tabs.java-installations": {
"message": "การจัดการ Java ที่ติดตั้ง"
},
@@ -14,6 +23,51 @@
"app.settings.tabs.resource-management": {
"message": "การจัดการทรัพยากร"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} พร้อมติดตั้งแล้ว! รีโหลดเพื่ออัปเดตทันที หรือจะอัปเดตอัตโนมัติเมื่อคุณปิดแอป"
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} ดาวน์โหลดเสร็จแล้ว! รีโหลดเพื่ออัปเดตทันที หรือจะอัปเดตอัตโนมัติเมื่อคุณปิดแอป"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} พร้อมให้ดาวน์โหลดแล้ว! เนื่องจากคุณกำลังใช้งานเครือข่ายที่มีการคิดค่าใช้จ่ายตามปริมาณข้อมูล ระบบจึงไม่ได้ดาวน์โหลดอัตโนมัติ"
},
"app.update-toast.changelog": {
"message": "บันทึกการเปลี่ยนแปลง"
},
"app.update-toast.download": {
"message": "ดาวน์โหลด ({size})"
},
"app.update-toast.downloading": {
"message": "กำลังดาวน์โหลด...."
},
"app.update-toast.reload": {
"message": "รีโหลด"
},
"app.update-toast.title": {
"message": "อัพเดตพร้อมแล้ว"
},
"app.update-toast.title.download-complete": {
"message": "ดาวน์โหลดเรียบร้อยแล้ว"
},
"app.update.complete-toast.text": {
"message": "คลิกที่นี่เพื่อดูบันทึกการเปลี่ยนแปลง"
},
"app.update.complete-toast.title": {
"message": "เวอร์ชั่น {version} ถูกติดตั้งแล้ว"
},
"app.update.download-update": {
"message": "ดาวน์โหลดอัพเดต"
},
"app.update.downloading-update": {
"message": "ดาวน์โหลดอัพเดตไปแล้ว ({percent}%)"
},
"app.update.reload-to-update": {
"message": "รีโหลดเพื่อติดตั้งอัพเดต"
},
"friends.heading": {
"message": "เพื่อน"
},
"instance.add-server.add-and-play": {
"message": "เพิ่มและเล่นทันที"
},
@@ -50,6 +104,9 @@
"instance.edit-world.title": {
"message": "แก้ไขโลก"
},
"instance.filter.disabled": {
"message": "ปิดโปรเจ็ค"
},
"instance.filter.updates-available": {
"message": "พบอัพเดท"
},
@@ -68,6 +125,15 @@
"instance.settings.tabs.general": {
"message": "ทั่วไป"
},
"instance.settings.tabs.general.delete": {
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.button": {
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.description": {
"message": "ลบอินสแตนซ์ออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
},
"instance.settings.tabs.general.deleting.button": {
"message": "กำลังลบ..."
},
@@ -77,6 +143,12 @@
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "ไม่สามารถทำซ้ำได้ขณะติดตั้ง"
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "สร้างสำเนาอินสแตนซ์"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "สร้างสำเนาของอินสแตนซ์นี้ รวมถึงโลก การตั้งค่า ม็อด และอื่นๆ"
},
"instance.settings.tabs.general.edit-icon": {
"message": "แก้ไขไอคอน"
},
@@ -95,6 +167,9 @@
"instance.settings.tabs.general.library-groups.create": {
"message": "สร้างกลุ่มใหม่"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "กลุ่มไลบรารีช่วยให้คุณจัดระเบียบอินสแตนซ์ของคุณเป็นหมวดหมู่ต่างๆ ภายในไลบรารี"
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "ใส่ชื่อกลุ่ม"
},

View File

@@ -9,7 +9,7 @@
"message": "Kiyas"
},
"app.settings.tabs.default-instance-options": {
"message": "Mga pagpipilian sa batayang pangyayari"
"message": "Mga pagpipilian sa batayang tularan"
},
"app.settings.tabs.feature-flags": {
"message": "Watawat ng mga tampok"
@@ -23,32 +23,116 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng mapagkukunan"
},
"app.update-toast.body": {
"message": "Handa nang maikabit ang Modrinth App v{version}. Muling dalhin upang maisapanahon, o pagkusaan pagpinid ng Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Tapos nang maidalamba ang Modrinth App v{version}. Muling dalhin upang matakda ang pagbabago ngayon, o mamaya nalang sa pagpinid ng Modrinth App."
"message": "Tapos nang maidalamba ang Modrinth App v{version}. Muling dalhin upang maisapanahon, o pagkusaan pagpinid ng Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magagamit na ngayon ang Modrinth App v{version}! Hindi namin dinalamba kaagad dahil bilang ang inyong kabalagan."
},
"app.update-toast.changelog": {
"message": "Tala ng Pagbabago"
},
"app.update-toast.download": {
"message": "Idalamba ({size})"
},
"app.update-toast.downloading": {
"message": "Nagdadalamba..."
},
"app.update-toast.reload": {
"message": "Magmulindala"
},
"app.update-toast.title": {
"message": "May kamakailang pagsasapanahon"
},
"app.update-toast.title.download-complete": {
"message": "Natapos ang pagdalamba"
},
"app.update.complete-toast.text": {
"message": "Magpindot rito upang matingnan ang talaan ng pagbabago."
},
"app.update.complete-toast.title": {
"message": "Tagumpay na nakabit ang bersiyong {version}!"
},
"app.update.download-update": {
"message": "Idalamba ang pagbabago"
},
"app.update.downloading-update": {
"message": "Nagdadalamba ng pagbabago ({percent}%)"
"message": "Nagdadalamba ng pagsasapanahon ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Handang makabit ang pagsasapanahon"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {{count}} other {{count} na}} hiling na makipagkaibigan"
},
"friends.add-friend.submit": {
"message": "Magpadala ng hiling na makipagkaibigan"
},
"friends.add-friend.title": {
"message": "Pagdaragdag ng kaibigan"
},
"friends.add-friend.username.description": {
"message": "Maaaraing hindi katulad sa kanilang pangalan sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ilagay ang pangalan ng gagamit sa Modrinth... "
},
"friends.add-friend.username.title": {
"message": "Ano ang pangalan ng iyong kaibigan sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Magdagdag ng mga kaibigan</link> upang makita ang kanilang nilalaro!"
},
"friends.friend.cancel-request": {
"message": "Bawiin ang hiling"
},
"friends.friend.remove-friend": {
"message": "Tanggalin ang kaibing"
},
"friends.friend.request-sent": {
"message": "Ipinadala na ang hiling na makipagkaibigan"
},
"friends.friend.view-profile": {
"message": "Tingnan ang propayl"
},
"friends.heading": {
"message": "Mga kaibigan"
},
"friends.heading.active": {
"message": "Masigla"
},
"friends.heading.offline": {
"message": "Pinid"
},
"friends.heading.online": {
"message": "Bukas"
},
"friends.heading.pending": {
"message": "Nakabinbin"
},
"friends.no-friends-match": {
"message": "Walang kaibigang tumugma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Hanapin ang mga kaibigan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Mag-sign-in sa Modrinth account</link> upang maidagdag ang mga kaibigan at malaman ang kanilang nilalaro!"
},
"instance.add-server.add-and-play": {
"message": "Idagdag at laruin"
},
"instance.add-server.add-server": {
"message": "Idagdag ang tagapagsilbi"
"message": "Idagdag ang pansilbi"
},
"instance.add-server.resource-pack.disabled": {
"message": "Hindi pinahihintulotan"
@@ -60,10 +144,10 @@
"message": "Magpahintulot"
},
"instance.add-server.title": {
"message": "Magdagdag ng tagapagsilbi"
"message": "Magdagdag ng pansilbi"
},
"instance.edit-server.title": {
"message": "Baguhin ang tagapagsilbi"
"message": "Baguhin ang pansilbi"
},
"instance.edit-world.hide-from-home": {
"message": "Huwag ipakita sa panig ng Bahay"
@@ -75,7 +159,7 @@
"message": "Minecraft na Daigdig"
},
"instance.edit-world.reset-icon": {
"message": "Walain ang lambana"
"message": "Isauli ang lambana"
},
"instance.edit-world.title": {
"message": "Baguhin ang daigdig"
@@ -86,23 +170,41 @@
"instance.filter.updates-available": {
"message": "May mga bagong pagbabago"
},
"instance.server-modal.address": {
"message": "Tinitirhan"
},
"instance.server-modal.name": {
"message": "Pangalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft na Tagapagsilbi"
"message": "Minecraft na Pansilbi"
},
"instance.server-modal.resource-pack": {
"message": "Balot ng mapagkukunan"
},
"instance.settings.tabs.general": {
"message": "Pangkalahatan"
},
"instance.settings.tabs.general.delete": {
"message": "Tanggalin ang pangyayari"
"message": "Tanggalin ang tularan"
},
"instance.settings.tabs.general.delete.button": {
"message": "Tanggalin ang pangyayari"
"message": "Tanggalin ang tularan"
},
"instance.settings.tabs.general.delete.description": {
"message": "Lagiang matatanggal ang pangyayari sa iyong pakasam, kasali na ang iyong mga daigdig, pagsasaayos, at lahat ng kinabitang nilalaman. Mag-ingat, hindi na mababawi kapag magtanggal ka ng pangyayari."
"message": "Habam buhay na matatanggal ang tularan sa iyong pakasam, kasali na ang iyong mga daigdig, pagsasaayos, at lahat ng kinabitang nilalaman. Mag-ingat, hindi na mababawi kapag nagtanggal ka na ng tularan."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Binubura..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Isaduhasipi"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Hindi makakasaduhasipi habang nagkakabit."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Isaduhasipi ang tularan"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Gagawan ng sipi ang pangyayaring ito, kasali na ang mga daigdig, pagsasaaayos, pagbabago, at iba pa."
@@ -113,19 +215,259 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tanggalin ang lambana"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Palitan ang lambana"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pumili ng lambana"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga pangkat ng aklatan"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Gumawa ng bagong pangkat"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Binibigyang-daan ng mga pangkat ng aklatan na iyong maayos ang iyong mga tularan sa iba't-ibang tapyas ng iyong aklatan."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ilagay ang pangalan ng pangkat"
},
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga kawit sa paglunsad"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga pasadyang kawit sa paglunsad"
},
"instance.settings.tabs.hooks.description": {
"message": "Binibigyan-daan ng mga kawit ang mga madalubhasang gumagamit na makapagtakbo ng mga utos sa kaayusan bago at pagkatapos malunsad ang laro."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Tapos-mapinid"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipapatakbo pagkatapos mapinid ang laro."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ilagay ang tapos-mapinid na utos..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Bago-malunsad"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipapatakbo bago mailunsad ang tularan."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ilagay ang bago-malunsad na utos..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga kawit sa paglunsad ng laro"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Pambalot"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Pambalot na utos sa paglunsad ng Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ilagay ang pambalot na utos..."
},
"instance.settings.tabs.installation": {
"message": "Pagkakabit"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Naka-install naman ang {platform} {version} para sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Nakakabit naman ang Baynilyang {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Palitan ang bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "Ikabit"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Kinakabit"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Naghahanap ng mga bersiyon ng mga balot ng pambago"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Kinakabit ang bagong bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Kasalukuyang nakakabit"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Kaalaman sa pagdalisay:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon ng laro"
},
"instance.settings.tabs.installation.install": {
"message": "Ikabit"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nagkakabit ngayon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon ng {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Hindi magagamit ang {loader} sa Minecraft {version}. Sumubok ng ibang tagapagtala ng pambago."
},
"instance.settings.tabs.installation.platform": {
"message": "Batyawan"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Ikabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Kinakabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Maaaring mababalik sa dati ang lahat ng kinabit o binago na nilalaman sa kung anong ibibigay ng balot ng pambago, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa pangyayari, ngunit kung nakabatay na ang iyong daigdig sa karagdagang nilalaman, maaari nitong masira ang mga umiiral na daigdig."
"message": "Maaaring mababalik sa dati ang lahat ng kinabit o binago na nilalaman sa kung anong ibibigay ng balot ng pambago, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa tularan, ngunit kung nakabatay na ang iyong daigdig sa karagdagang nilalaman, maaari nitong masira ang mga umiiral na daigdig."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Ibalik ang mga nilalaman ng pangyayari sa pangunahing kalagayan, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago."
"message": "Ibalik ang mga nilalaman ng tularan sa pangunahing kalagayan, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Ikabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Kumpunihin"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Kinukumpuni"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pag-aayos, magkakabit muli ng mga sandalan ng Minecraft at maghahanap ng mga katiwalian. Maaaring malutas nito ang mga isyu kung hindi malulunsad ang laro dahil sa mga kamalian sa tagapaglunsad, ngunit hindi nito malulutas ang mga isyu o pagbagsak na dulot nga mga pambagong nakakabit."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Kumpunihin ang tularan?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Kinukumpuni ngayon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasalukuyan isauli"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang lahat ng bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "palitan ang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "ikabit"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "ikabit muli"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "kumpunihin"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Hindi makaka-{action} habang nagkakabit"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Hindi makaka-{action} habang di-nakakabit"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Hindi makaka-{action} habang nagkukumpuni"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(hindi kilalang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "Paghiwalayin sa tularan"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kung ipapatuloy mo, hindi mo na itong maaaring maikawing muli ng hindi lilikha ng panibagong tularan. Hindi ka makatatanggap ng mga pagsasapanahon ng pambalot ng pambago at magiging pangkaraniwan na itong."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Nakakawing ang tularang ito sa isang pambalot ng pambago, ibig sabihin hindi maisapanahon ang mga pambago at hindi mo mapapalitan ang tagapagdala ng pambago o ang bersiyon ng Minecraft."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Paghiwalayin sa balot ng pambago"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga kawit"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga sadyansukat ng java"
},
"instance.settings.tabs.java.java-installation": {
"message": "Kabit ng Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang inilaan"
},
"instance.settings.tabs.window": {
"message": "Durungawan"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga kagustuhan sa pasadyang durungawan"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Buong-tabing"
},
"instance.settings.tabs.window.height": {
"message": "Tayog"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ilagay ang taas..."
},
"instance.settings.tabs.window.width": {
"message": "Lapad"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ilagay ang lapad..."
},
"instance.settings.title": {
"message": "Mga Kagustuhan"
},
"instance.worlds.a_minecraft_server": {
"message": "Isang Minecraft na Pansilbi"
},
"instance.worlds.copy_address": {
"message": "Sipiin ang tinitirhan"
},
"instance.worlds.dont_show_on_home": {
"message": "Huwag ipakita sa Tirahan"
},
"instance.worlds.filter.available": {
"message": "Magagamit"
},
"instance.worlds.hardcore": {
"message": "Paraang pangdalubhasan"
},
"instance.worlds.play_instance": {
"message": "Laruin ang tularan"
},
"instance.worlds.type.server": {
"message": "Pansilbi"
},
"instance.worlds.type.singleplayer": {
"message": "Pang-isahang laro"
},
"instance.worlds.view_instance": {
"message": "Tingnan ang pangyayari"
"message": "Tingnan ang tularan"
},
"instance.worlds.world_in_use": {
"message": "Ginagamit ang daigdig"
},
"search.filter.locked.instance.sync": {
"message": "Makipagsabayan sa tularan"
}
}

View File

@@ -3,7 +3,7 @@
"message": "Geliştirici modu açıldı."
},
"app.settings.downloading": {
"message": "V{version} İndiriliyor"
"message": "v{version} indiriliyor"
},
"app.settings.tabs.appearance": {
"message": "Görünüm"
@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Güncellemeyi yüklemek için yeniden yükleyin"
},
"friends.action.add-friend": {
"message": "Bir arkadaş ekle"
},
"friends.action.view-friend-requests": {
"message": "{count} Arkadaşlık {count, plural,one {isteği} other {istekleri}}"
},
"friends.add-friend.submit": {
"message": "Arkadaşlık isteği gönder"
},
"friends.add-friend.title": {
"message": "Bir arkadaş ekleme"
},
"friends.add-friend.username.description": {
"message": "Minecraft kullanıcı adlarından farklı olabilir!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth kullanıcı adını girin..."
},
"friends.add-friend.username.title": {
"message": "Arkadaşının Modrinth kullanıcı adı nedir?"
},
"friends.add-friends-to-share": {
"message": "Arkadaşlarının ne oynadığını görmek için <link>onları ekle</link>!"
},
"friends.friend.cancel-request": {
"message": "İsteği iptal et"
},
"friends.friend.remove-friend": {
"message": "Arkadaşı çıkar"
},
"friends.friend.request-sent": {
"message": "Arkadaşlık isteği gönderildi"
},
"friends.friend.view-profile": {
"message": "Profili gör"
},
"friends.heading": {
"message": "Arkadaşlar"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Çevrimdışı"
},
"friends.heading.online": {
"message": "Çevrimiçi"
},
"friends.heading.pending": {
"message": "Bekleniyor"
},
"friends.no-friends-match": {
"message": "''{query}'' ile eşleşen arkadaş yok"
},
"friends.search-friends-placeholder": {
"message": "Arkadaşları ara..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "Arkadaş eklemek ve ne oynadıklarını görmek için bir <link>Modrinth hesabına giriş yap</link>!"
},
"instance.add-server.add-and-play": {
"message": "Ekle ve oyna"
},

View File

@@ -92,6 +92,9 @@
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Утырту"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Төзәтү мәгълүматы:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Уен версиясе"
},

View File

@@ -24,16 +24,16 @@
"message": "Керування ресурсами"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} готова до встановлення. Перезапустіть програму, щоб встановити зараз, чи вона оновиться автоматично після закриття."
"message": "Modrinth App v{version} готова до встановлення! Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Завантаження Modrinth App v{version} завершене. Перезапустіть програму щоб оновити зараз, чи вона оновиться автоматично після закриття."
"message": "Modrinth App v{version} уже завантажилась. Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} вже доступна до завантаження! Оскільки ви на лімітному підключенні, ми не завантажили його автоматично."
"message": "Modrinth App v{version} доступна зараз! Оскільки ви на лімітному з’єднанні, ми не завантажили її автоматично."
},
"app.update-toast.changelog": {
"message": "Список змін"
"message": "Журнал змін"
},
"app.update-toast.download": {
"message": "Завантажити ({size})"
@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Перезавантажте, щоб установити оновлення"
},
"friends.action.add-friend": {
"message": "Додати друга"
},
"friends.action.view-friend-requests": {
"message": "{count} запит{count, plural, one { у} few {и в} many {ів у} other {у в}} друзі"
},
"friends.add-friend.submit": {
"message": "Надіслати запит у друзі"
},
"friends.add-friend.title": {
"message": "Додавання друга"
},
"friends.add-friend.username.description": {
"message": "Може відрізнятися від його або її імені користувача Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Уведіть ім’я користувача Modrinth…"
},
"friends.add-friend.username.title": {
"message": "Яке ім’я користувача Modrinth у вашого друга?"
},
"friends.add-friends-to-share": {
"message": "<link>Додайте друзів</link>, щоб бачити, у що вони грають!"
},
"friends.friend.cancel-request": {
"message": "Скасувати запит"
},
"friends.friend.remove-friend": {
"message": "Видалити друга"
},
"friends.friend.request-sent": {
"message": "Запит у друзі надіслано"
},
"friends.friend.view-profile": {
"message": "Переглянути профіль"
},
"friends.heading": {
"message": "Друзі"
},
"friends.heading.active": {
"message": "Активний"
},
"friends.heading.offline": {
"message": "Поза мережею"
},
"friends.heading.online": {
"message": "У мережі"
},
"friends.heading.pending": {
"message": "Очікується"
},
"friends.no-friends-match": {
"message": "Немає друзів, котрі збігаються з «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Шукати друзів…"
},
"friends.section.heading": {
"message": "{title} — {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Увійдіть в обліковий запис Modrinth</link>, щоб додати друзів і бачити, у що вони грають!"
},
"instance.add-server.add-and-play": {
"message": "Додати та грати"
},
@@ -123,7 +186,7 @@
"message": "Загальні"
},
"instance.settings.tabs.general.delete": {
"message": "Видалити інсталяцію"
"message": "Видалити примірник"
},
"instance.settings.tabs.general.delete.button": {
"message": "Видалити профіль"
@@ -279,7 +342,7 @@
"message": "Перевстановлення збірки"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Перевстановлення скине увесь доданий або модифікований вміст до наданого збіркою, прибираючи будь-які, додатково встановлені вами модифікації чи вміст.\nЯкщо ви змінювали профіль, це може виправити непередбачувану поведінку, однак, якщо ваші світи залежать від додаткового вмісту, це може зламати ці світи."
"message": "Перевстановлення скине увесь доданий або модифікований вміст до наданого збіркою, прибираючи будь-які, додатково встановлені вами модифікації чи вміст.\nЯкщо ви змінювали інсталяцію, це може виправити непередбачувану поведінку, однак, якщо ваші світи залежать від додаткового вмісту, це може зламати ці світи."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Ви впевнені, що хочете перевстановити цей профіль?"
@@ -303,7 +366,7 @@
"message": "Відновити профіль?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Відновлення в процессі"
"message": "Лагодження в процесі"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Скинути до поточного"
@@ -321,7 +384,7 @@
"message": "перевстановити"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "виправити"
"message": "полагодити"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Неможливо {action} під час встановлення"

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "重新启动以安装更新"
},
"friends.action.add-friend": {
"message": "加一好友"
},
"friends.action.view-friend-requests": {
"message": "{count}好友请求"
},
"friends.add-friend.submit": {
"message": "发送好友请求"
},
"friends.add-friend.title": {
"message": "添加好友"
},
"friends.add-friend.username.description": {
"message": "可能和他们的 Minecraft 用户名不一样!"
},
"friends.add-friend.username.placeholder": {
"message": "输入Modrinth用户名……"
},
"friends.add-friend.username.title": {
"message": "你朋友的 Modrinth 用户名是什么?"
},
"friends.add-friends-to-share": {
"message": "<link>添加好友</link>查看他们在玩什么!"
},
"friends.friend.cancel-request": {
"message": "取消请求"
},
"friends.friend.remove-friend": {
"message": "删除好友"
},
"friends.friend.request-sent": {
"message": "已发送好友请求"
},
"friends.friend.view-profile": {
"message": "查看个人资料"
},
"friends.heading": {
"message": "好友"
},
"friends.heading.active": {
"message": "活跃的"
},
"friends.heading.offline": {
"message": "离线"
},
"friends.heading.online": {
"message": "在线"
},
"friends.heading.pending": {
"message": "待确认"
},
"friends.no-friends-match": {
"message": "没有匹配“{query}”的朋友"
},
"friends.search-friends-placeholder": {
"message": "搜索好友……"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "请登录你的 <link>Modrinth账号</link> 添加好友并查看他们在玩什么!"
},
"instance.add-server.add-and-play": {
"message": "添加并游玩"
},

View File

@@ -36,7 +36,7 @@
"message": "變更紀錄"
},
"app.update-toast.download": {
"message": "下載{size}"
"message": "下載 ({size})"
},
"app.update-toast.downloading": {
"message": "下載中..."
@@ -60,11 +60,74 @@
"message": "下載更新"
},
"app.update.downloading-update": {
"message": "正在下載更新{percent}%"
"message": "正在下載更新 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "重新載入即可安裝更新"
},
"friends.action.add-friend": {
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友請求"
},
"friends.add-friend.submit": {
"message": "傳送好友請求"
},
"friends.add-friend.title": {
"message": "新增好友"
},
"friends.add-friend.username.description": {
"message": "這可能與對方的 Minecraft 使用者名稱不同!"
},
"friends.add-friend.username.placeholder": {
"message": "輸入 Modrinth 使用者名稱..."
},
"friends.add-friend.username.title": {
"message": "你好友的 Modrinth 使用者名稱是什麼?"
},
"friends.add-friends-to-share": {
"message": "<link>新增好友</link>即可查看對方正在玩什麼!"
},
"friends.friend.cancel-request": {
"message": "取消請求"
},
"friends.friend.remove-friend": {
"message": "移除好友"
},
"friends.friend.request-sent": {
"message": "好友請求已送出"
},
"friends.friend.view-profile": {
"message": "查看個人檔案"
},
"friends.heading": {
"message": "好友"
},
"friends.heading.active": {
"message": "活躍中"
},
"friends.heading.offline": {
"message": "離線"
},
"friends.heading.online": {
"message": "線上"
},
"friends.heading.pending": {
"message": "待處理"
},
"friends.no-friends-match": {
"message": "沒有符合「{query}」的好友"
},
"friends.search-friends-placeholder": {
"message": "搜尋好友..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>登入 Modrinth 帳號</link>即可新增好友並查看對方正在玩什麼!"
},
"instance.add-server.add-and-play": {
"message": "新增並遊玩"
},

View File

@@ -2,6 +2,7 @@ import 'floating-vue/dist/style.css'
import * as Sentry from '@sentry/vue'
import { VueScanPlugin } from '@taijased/vue-render-tracker'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createPlugin } from '@vintl/vintl/plugin'
import FloatingVue from 'floating-vue'
import { createPinia } from 'pinia'
@@ -45,6 +46,7 @@ Sentry.init({
tracesSampleRate: 0.1,
})
app.use(VueQueryPlugin)
app.use(vueScan)
app.use(router)
app.use(pinia)

View File

@@ -427,7 +427,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
<div
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
class="bg-bg-raised card-shadow rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
>
<img
:src="ExcitedRinthbot"

View File

@@ -67,6 +67,7 @@
direction="vertical"
:item-size="20"
key-field="id"
buffer="200"
>
<div class="user no-wrap">
<span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{
@@ -508,7 +509,7 @@ onUnmounted(() => {
background-color: var(--color-accent-contrast);
color: var(--color-contrast);
border-radius: var(--radius-lg);
padding: 1.5rem;
padding-top: 1.5rem;
overflow-x: auto; /* Enables horizontal scrolling */
overflow-y: hidden; /* Disables vertical scrolling on this wrapper */
white-space: nowrap; /* Keeps content on a single line */
@@ -557,9 +558,10 @@ onUnmounted(() => {
.user {
height: 32%;
padding: 0 12px;
padding: 0 1.5rem;
display: flex;
align-items: center;
user-select: text;
}
</style>

View File

@@ -285,6 +285,7 @@ import type { Organization, Project, TeamMember, Version } from '@modrinth/utils
import { formatProjectType } from '@modrinth/utils'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
@@ -300,11 +301,13 @@ import {
get_organization_many,
get_project_many,
get_team_many,
get_version,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import {
add_project_from_path,
get,
get_projects,
remove_project,
toggle_disable_project,
@@ -313,6 +316,7 @@ import {
} from '@/helpers/profile.js'
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { installVersionDependencies } from '@/store/install'
const { handleError } = injectNotificationManager()
@@ -531,7 +535,13 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
return options
})
const selectedFilters = ref<string[]>([])
const selectedFilters = useStorage<string[]>(
`${props.instance.name}-mod-selected-filters`,
[],
sessionStorage,
{ mergeDefaults: true },
)
const filteredProjects = computed(() => {
const updatesFilter = selectedFilters.value.includes('updates')
const disabledFilter = selectedFilters.value.includes('disabled')
@@ -620,10 +630,15 @@ const sortProjects = (filter: string) => {
const updateAll = async () => {
const setProjects = []
const outdatedProjects = []
for (const [i, project] of projects.value.entries()) {
if (project.outdated) {
project.updating = true
setProjects.push(i)
if (project.updateVersion) {
outdatedProjects.push(project.updateVersion)
}
}
}
@@ -639,6 +654,21 @@ const updateAll = async () => {
projects.value[index].updateVersion = undefined
}
}
if (outdatedProjects.length > 0) {
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
for (const versionId of outdatedProjects) {
const versionData = await get_version(versionId, 'must_revalidate').catch(handleError)
if (versionData) {
await installVersionDependencies(profile, versionData).catch(handleError)
}
}
}
}
for (const project of setProjects) {
projects.value[project].updating = false
}
@@ -655,6 +685,19 @@ const updateProject = async (mod: ProjectListEntry) => {
mod.updating = true
await new Promise((resolve) => setTimeout(resolve, 0))
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
if (mod.updateVersion) {
const versionData = await get_version(mod.updateVersion, 'must_revalidate').catch(handleError)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, versionData).catch(handleError)
}
}
}
mod.updating = false
mod.outdated = false

View File

@@ -1,3 +1,4 @@
import { ServersManagePageIndex } from '@modrinth/ui'
import { createRouter, createWebHistory } from 'vue-router'
import * as Pages from '@/pages'
@@ -27,6 +28,14 @@ export default new createRouter({
breadcrumb: [{ name: 'Worlds' }],
},
},
{
path: '/servers/manage/',
name: 'Servers',
component: ServersManagePageIndex,
meta: {
breadcrumb: [{ name: 'Servers' }],
},
},
{
path: '/browse/:projectType',
name: 'Discover content',

View File

@@ -42,11 +42,24 @@ export const useInstall = defineStore('installStore', {
})
export const findPreferredVersion = (versions, project, instance) => {
// When `project` is passed in from this stack trace:
// - `installVersionDependencies`
// - `install.js/install` - `installVersionDependencies` call
//
// ..then `project` is actually a `Dependency` struct of a cached `Version`.
// `Dependency` does not have a `project_type` field,
// so we default it to `mod`.
//
// If we don't default here, then this `.find` will ignore version/instance
// loader mismatches, and you'll end up e.g. installing NeoForge mods for a
// Fabric instance.
const projectType = project.project_type ?? 'mod'
// If we can find a version using strictly the instance loader then prefer that
let version = versions.find(
(v) =>
v.game_versions.includes(instance.game_version) &&
(project.project_type === 'mod' ? v.loaders.includes(instance.loader) : true),
(projectType === 'mod' ? v.loaders.includes(instance.loader) : true),
)
if (!version) {

View File

@@ -5,6 +5,7 @@ export const DEFAULT_FEATURE_FLAGS = {
page_path: false,
worlds_tab: false,
worlds_in_home: true,
servers_in_app: false,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const

View File

@@ -15,6 +15,7 @@ fn main() {
"offline_login",
"elyby_login",
"elyby_auth_authenticate",
"check_reachable",
"login",
"remove_user",
"get_default_user",

View File

@@ -11,6 +11,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
offline_login,
elyby_login,
elyby_auth_authenticate,
check_reachable,
login,
remove_user,
get_default_user,
@@ -79,6 +80,13 @@ pub async fn elyby_auth_authenticate(
Ok(text)
}
/// Checks if the authentication servers are reachable.
#[tauri::command]
pub async fn check_reachable() -> Result<()> {
minecraft_auth::check_reachable().await?;
Ok(())
}
/// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
#[tauri::command]

View File

@@ -2,6 +2,7 @@
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![recursion_limit = "256"]
use native_dialog::{DialogBuilder, MessageLevel};
use std::env;

View File

@@ -48,7 +48,7 @@
]
},
"productName": "AstralRinth App",
"version": "0.10.1601",
"version": "0.10.2101",
"mainBinaryName": "AstralRinth App",
"identifier": "AstralRinthApp",
"plugins": {

View File

@@ -703,8 +703,8 @@ components:
description: All gallery images attached to the project
example:
[
https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png,
https://cdn.modrinth.com/data/AABBCCDD/images/c21776867afb6046fdc3c21dbcf5cc50ae27a236.png,
'https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png',
'https://cdn.modrinth.com/data/AABBCCDD/images/c21776867afb6046fdc3c21dbcf5cc50ae27a236.png',
]
items:
type: string

View File

@@ -1,12 +1,12 @@
import { pathToFileURL } from 'node:url'
import { match as matchLocale } from '@formatjs/intl-localematcher'
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import serverSidedVue from '@vitejs/plugin-vue'
import { consola } from 'consola'
import { promises as fs } from 'fs'
import { globIterate } from 'glob'
import { defineNuxtConfig } from 'nuxt/config'
import { $fetch } from 'ofetch'
import { basename, relative, resolve } from 'pathe'
import svgLoader from 'vite-svg-loader'
@@ -133,20 +133,7 @@ export default defineNuxtConfig({
// 30 minutes
const TTL = 30 * 60 * 1000
let state: {
lastGenerated?: string
apiUrl?: string
categories?: any[]
loaders?: any[]
gameVersions?: any[]
donationPlatforms?: any[]
reportTypes?: any[]
homePageProjects?: any[]
homePageSearch?: any[]
homePageNotifs?: any[]
products?: any[]
errors?: number[]
} = {}
let state: Partial<Labrinth.State.GeneratedState & Record<string, any>> = {}
try {
state = JSON.parse(await fs.readFile('./src/generated/state.json', 'utf8'))
@@ -172,67 +159,19 @@ export default defineNuxtConfig({
return
}
const client = new GenericModrinthClient({
labrinthBaseUrl: API_URL.replace('/v2/', ''),
userAgent: 'Knossos generator (support@modrinth.com)',
})
const generatedState = await client.labrinth.state.build()
state.lastGenerated = new Date().toISOString()
state.apiUrl = API_URL
const headers = {
headers: {
'user-agent': 'Knossos generator (support@modrinth.com)',
},
state = {
...state,
...generatedState,
}
const caughtErrorCodes = new Set<number>()
function handleFetchError(err: any, defaultValue: any) {
console.error('Error generating state: ', err)
caughtErrorCodes.add(err.status)
return defaultValue
}
const [
categories,
loaders,
gameVersions,
donationPlatforms,
reportTypes,
homePageProjects,
homePageSearch,
homePageNotifs,
products,
] = await Promise.all([
$fetch(`${API_URL}tag/category`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/loader`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/game_version`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/donation_platform`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}tag/report_type`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}projects_random?count=60`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL.replace('/v2/', '/_internal/')}billing/products`, headers).catch((err) =>
handleFetchError(err, []),
),
])
state.categories = categories
state.loaders = loaders
state.gameVersions = gameVersions
state.donationPlatforms = donationPlatforms
state.reportTypes = reportTypes
state.homePageProjects = homePageProjects
state.homePageSearch = homePageSearch
state.homePageNotifs = homePageNotifs
state.products = products
state.errors = [...caughtErrorCodes]
await fs.writeFile('./src/generated/state.json', JSON.stringify(state))
console.log('Tags generated!')
@@ -475,6 +414,12 @@ export default defineNuxtConfig({
'Critical-CH': 'Sec-CH-Prefers-Color-Scheme',
},
},
'/dashboard/revenue/withdraw': {
redirect: {
to: '/dashboard/revenue',
statusCode: 410,
},
},
'/email/**': {
redirect: '/_internal/templates/email/**',
},

View File

@@ -37,12 +37,14 @@
"@formatjs/intl-localematcher": "^0.5.4",
"@intercom/messenger-js-sdk": "^0.0.14",
"@ltd/j-toml": "^1.38.0",
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
"@modrinth/blog": "workspace:*",
"@modrinth/moderation": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@pinia/nuxt": "^0.5.1",
"@tanstack/vue-query": "^5.90.7",
"@types/three": "^0.172.0",
"@vintl/vintl": "^4.4.1",
"@vitejs/plugin-vue": "^5.0.4",
@@ -56,7 +58,6 @@
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",
"highlight.js": "^11.7.0",
"iso-3166-1": "^2.1.1",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"markdown-it": "14.1.0",

View File

@@ -6,31 +6,21 @@
</NuxtLayout>
</template>
<script setup lang="ts">
import { NotificationPanel, provideNotificationManager } from '@modrinth/ui'
import { provideApi } from '@modrinth/ui/src/providers/api.ts'
import { RestModrinthApi } from '@modrinth/utils'
import { NotificationPanel, provideModrinthClient, provideNotificationManager } from '@modrinth/ui'
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
import { createModrinthClient } from '~/helpers/api.ts'
import { FrontendNotificationManager } from '~/providers/frontend-notifications.ts'
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
const auth = await useAuth()
const config = useRuntimeConfig()
provideNotificationManager(new FrontendNotificationManager())
provideApi(
new RestModrinthApi((url: string, options?: object) => {
const match = url.match(/^\/v(\d+)\/(.+)$/)
if (match) {
const apiVersion = Number(match[1])
const path = match[2]
return useBaseFetch(path, {
...options,
apiVersion,
}) as Promise<Response>
} else {
throw new Error('Invalid format')
}
}),
)
const client = createModrinthClient(auth, {
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
rateLimitKey: config.rateLimitKey,
})
provideModrinthClient(client)
</script>

View File

@@ -1290,3 +1290,7 @@ svg.inline-svg {
}
}
}
.card-shadow {
box-shadow: var(--shadow-card);
}

View File

@@ -37,6 +37,7 @@ html {
--icon-32: 2rem;
interpolate-size: allow-keywords;
scrollbar-gutter: stable;
}
.light-mode {
@@ -89,7 +90,7 @@ html {
--color-hr: var(--color-text);
--color-table-border: #dfe2e5;
--color-table-alternate-row: #f2f4f7;
--color-table-alternate-row: #f0f1f2;
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');

View File

@@ -53,12 +53,15 @@
</svg>
</template>
<script setup>
<script setup lang="ts">
const loading = useLoading()
const config = useRuntimeConfig()
const flags = useFeatureFlags()
const api = computed(() => {
if (flags.value.demoMode) return 'prod'
const apiUrl = config.public.apiBaseUrl
if (apiUrl.startsWith('https://api.modrinth.com')) {
return 'prod'

View File

@@ -1,151 +0,0 @@
<template>
<div
class="checkbox-outer button-within"
:class="{ disabled, checked: modelValue }"
role="presentation"
@click="toggle"
>
<button
class="checkbox"
role="checkbox"
:disabled="disabled"
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }"
:aria-label="description ?? label"
:aria-checked="modelValue"
>
<CheckIcon v-if="modelValue && !collapsingToggleStyle" aria-hidden="true" />
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
</button>
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
<p v-if="label" aria-hidden="true">
{{ label }}
</p>
<slot v-else />
</div>
</template>
<script>
import { CheckIcon, DropdownIcon } from '@modrinth/assets'
export default {
components: {
CheckIcon,
DropdownIcon,
},
props: {
label: {
type: String,
required: false,
default: '',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
description: {
type: String,
required: false,
default: null,
},
modelValue: Boolean,
clickEvent: {
type: Function,
required: false,
default: () => {},
},
collapsingToggleStyle: {
type: Boolean,
required: false,
default: false,
},
},
emits: ['update:modelValue'],
methods: {
toggle() {
if (!this.disabled) {
this.$emit('update:modelValue', !this.modelValue)
}
},
},
}
</script>
<style lang="scss" scoped>
.checkbox-outer {
display: flex;
align-items: center;
cursor: pointer;
p {
user-select: none;
padding: 0.2rem 0;
margin: 0;
}
&.disabled {
cursor: not-allowed;
}
&.checked {
outline: 2px solid transparent;
outline-offset: 4px;
border-radius: 0.25rem;
}
}
.checkbox {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-width: 1rem;
min-height: 1rem;
padding: 0;
margin: 0 0.5rem 0 0;
color: var(--color-button-text);
background-color: var(--color-button-bg);
border-radius: var(--size-rounded-control);
box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
&.checked {
background-color: var(--color-brand);
}
svg {
color: var(--color-accent-contrast, var(--color-brand-inverted));
stroke-width: 0.2rem;
height: 0.8rem;
width: 0.8rem;
flex-shrink: 0;
}
&.collapsing {
background-color: transparent !important;
box-shadow: none;
svg {
color: inherit;
height: 1rem;
width: 1rem;
transition: transform 0.25s ease-in-out;
}
&.checked {
svg {
transform: rotate(180deg);
}
}
}
&:disabled {
box-shadow: none;
cursor: not-allowed;
}
}
</style>

View File

@@ -90,7 +90,7 @@ defineProps({
},
})
const tags = useTags()
const tags = useGeneratedState()
</script>
<style lang="scss" scoped>
.environment {

View File

@@ -1,32 +1,155 @@
<template>
<nav>
<ul>
<slot />
<nav :aria-label="ariaLabel" class="w-full">
<ul
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"
>
<slot v-if="hasSlotContent" />
<template v-else>
<li v-for="(item, idx) in filteredItems" :key="getKey(item, idx)" class="contents">
<hr v-if="isSeparator(item)" class="my-1 w-full border-t border-solid" />
<div
v-else-if="isHeading(item)"
class="px-4 pb-1 pt-2 text-xs font-bold uppercase tracking-wide text-secondary"
>
{{ item.label }}
</div>
<NuxtLink
v-else-if="item.link ?? item.to"
:to="(item.link ?? item.to) as string"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
:class="{ 'is-active': isActive(item as NavStackLinkItem) }"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
<span v-if="item.chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>
<button
v-else-if="item.action"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
:class="{ 'danger-button': item.danger }"
@click="item.action"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
</button>
<span v-else>You frog. 🐸</span>
</li>
</template>
</ul>
</nav>
</template>
<script>
export default {}
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
import { type Component, computed, useSlots } from 'vue'
type NavStackBaseItem = {
label: string
icon?: Component | string
badge?: string | number | null
chevron?: boolean
danger?: boolean
}
type NavStackLinkItem = NavStackBaseItem & {
type?: 'item'
link?: string | null
to?: string | null
action?: (() => void) | null
matchNested?: boolean
}
type NavStackSeparator = { type: 'separator' }
type NavStackHeading = { type: 'heading'; label: string }
export type NavStackEntry = (NavStackLinkItem | NavStackSeparator | NavStackHeading) & {
shown?: boolean
}
const props = defineProps<{
items?: NavStackEntry[]
ariaLabel?: string
}>()
const ariaLabel = computed(() => props.ariaLabel ?? 'Section navigation')
const route = useRoute()
const slots = useSlots()
const hasSlotContent = computed(() => {
const content = slots.default?.()
return !!(content && content.length)
})
function isSeparator(item: NavStackEntry): item is NavStackSeparator {
return (item as any).type === 'separator'
}
function isHeading(item: NavStackEntry): item is NavStackHeading {
return (item as any).type === 'heading'
}
function getKey(item: NavStackEntry, idx: number) {
if (isSeparator(item)) return `sep-${idx}`
if (isHeading(item)) return `head-${item.label}-${idx}`
const link = (item as NavStackLinkItem).link ?? (item as NavStackLinkItem).to
return link ? `link-${link}` : `action-${(item as NavStackLinkItem).label}-${idx}`
}
function isActive(item: NavStackLinkItem): boolean {
const link = item.link ?? item.to
if (!link) return false
const currentPath = route.path
if (item.matchNested) {
return currentPath.startsWith(link)
}
return currentPath === link
}
const filteredItems = computed(() => props.items?.filter((x) => x.shown === undefined || x.shown))
</script>
<style lang="scss" scoped>
ul {
display: flex;
flex-direction: column;
grid-gap: var(--spacing-card-xs);
flex-wrap: wrap;
list-style-type: none;
margin: 0;
padding: 0;
> :first-child {
margin-top: 0;
}
}
li {
display: unset;
text-align: unset;
}
.router-link-exact-active.nav-item,
.nav-item.is-active {
background: var(--color-button-bg-selected);
color: var(--color-button-text-selected);
}
.router-link-exact-active.nav-item .text-contrast,
.nav-item.is-active .text-contrast {
color: var(--color-button-text-selected);
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<NuxtLink v-if="link !== null" :to="link" class="nav-item">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
<span v-if="chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>
<button v-else-if="action" class="nav-item" :class="{ 'danger-button': danger }" @click="action">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
</button>
<span v-else>i forgor 💀</span>
</template>
<script>
import { ChevronRightIcon } from '@modrinth/assets'
export default {
components: {
ChevronRightIcon,
},
props: {
link: {
default: null,
type: String,
},
action: {
default: null,
type: Function,
},
label: {
required: true,
type: String,
},
badge: {
default: null,
type: String,
},
chevron: {
default: false,
type: Boolean,
},
danger: {
default: false,
type: Boolean,
},
},
}
</script>
<style lang="scss" scoped>
.nav-item {
@apply flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2 text-left font-semibold text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97];
}
.router-link-exact-active.nav-item {
@apply bg-button-bgSelected text-button-textSelected;
}
</style>

View File

@@ -376,7 +376,7 @@ const props = defineProps({
})
const flags = useFeatureFlags()
const tags = useTags()
const tags = useGeneratedState()
const type = computed(() =>
!props.notification.body || props.notification.body.type === 'legacy_markdown'

View File

@@ -152,7 +152,7 @@ const onSubmitHandler = () => {
border-radius: var(--radius-md);
overflow: hidden;
margin-top: var(--gap-md);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
background-color: var(--color-raised-bg);
.table-row {

View File

@@ -212,7 +212,7 @@ export default {
},
},
setup() {
const tags = useTags()
const tags = useGeneratedState()
const formatRelativeTime = useRelativeTime()
return { tags, formatRelativeTime }

View File

@@ -0,0 +1,264 @@
<template>
<NewModal ref="modal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Batch credit</span>
</template>
<div class="flex w-[500px] max-w-[90vw] flex-col gap-6">
<div class="flex flex-col gap-2">
<label class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Type </span>
<span>Select target to credit.</span>
</label>
<Combobox
v-model="mode"
:options="modeOptions"
placeholder="Select type"
class="max-w-[8rem]"
/>
</div>
<div class="flex flex-col gap-2">
<label for="days" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Days to credit </span>
</label>
<input
id="days"
v-model.number="days"
class="w-32"
type="number"
min="1"
autocomplete="off"
/>
</div>
<div v-if="mode === 'nodes'" class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="node-input" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Node hostnames </span>
</label>
<div class="flex items-center gap-2">
<input
id="node-input"
v-model="nodeInput"
class="w-32"
type="text"
autocomplete="off"
/>
<ButtonStyled color="blue" color-fill="text">
<button class="shrink-0" @click="addNode">
<PlusIcon />
Add
</button>
</ButtonStyled>
</div>
<div v-if="selectedNodes.length" class="mt-1 flex flex-wrap gap-2">
<TagItem v-for="h in selectedNodes" :key="`node-${h}`" :action="() => removeNode(h)">
<XIcon />
{{ h }}
</TagItem>
</div>
</div>
</div>
<div v-else class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="region-select" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Region </span>
<span>This will credit all active servers in the region.</span>
</label>
<Combobox
v-model="selectedRegion"
:options="regions"
placeholder="Select region"
class="max-w-[24rem]"
/>
</div>
</div>
<div class="between flex items-center gap-4">
<label for="send-email-batch" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Send email </span>
</label>
<Toggle id="send-email-batch" v-model="sendEmail" />
</div>
<div v-if="sendEmail" class="flex flex-col gap-2">
<label for="message-batch" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Customize Email </span>
<span>
Unless a particularly bad or out of the ordinary event happened, keep this to the
default
</span>
</label>
<div
class="text-muted flex flex-col gap-2 rounded-lg border border-divider bg-button-bg p-4"
>
<span>Hi {user.name},</span>
<div class="textarea-wrapper">
<textarea
id="message-batch"
v-model="message"
rows="3"
class="w-full overflow-hidden !bg-surface-3"
/>
</div>
<span>
To make up for it, we've added {{ days }} day{{ pluralize(days) }} to your Modrinth
Servers subscription.
</span>
<span>
Your next charge was scheduled for {credit.previous_due} and will now be on
{credit.next_due}.
</span>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="applyDisabled" @click="apply">
<CheckIcon aria-hidden="true" />
Apply credits
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="modal?.hide?.()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
injectNotificationManager,
NewModal,
TagItem,
Toggle,
} from '@modrinth/ui'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import { computed, ref } from 'vue'
import { useBaseFetch } from '#imports'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const modal = ref<InstanceType<typeof NewModal>>()
const days = ref(1)
const sendEmail = ref(true)
const message = ref('')
const modeOptions = [
{ value: 'nodes', label: 'Nodes' },
{ value: 'region', label: 'Region' },
]
const mode = ref<string>('nodes')
const nodeInput = ref('')
const selectedNodes = ref<string[]>([])
type RegionOpt = { value: string; label: string }
const regions = ref<RegionOpt[]>([])
const selectedRegion = ref<string | null>(null)
const nodeHostnames = ref<string[]>([])
function show(event?: Event) {
void ensureOverview()
message.value = DEFAULT_CREDIT_EMAIL_MESSAGE
modal.value?.show(event)
}
function hide() {
modal.value?.hide()
}
function addNode() {
const v = nodeInput.value.trim()
if (!v) return
if (!nodeHostnames.value.includes(v)) {
addNotification({
title: 'Unknown node',
text: "This hostname doesn't exist",
type: 'error',
})
return
}
if (!selectedNodes.value.includes(v)) selectedNodes.value.push(v)
nodeInput.value = ''
}
function removeNode(v: string) {
selectedNodes.value = selectedNodes.value.filter((x) => x !== v)
}
const applyDisabled = computed(() => {
if (days.value < 1) return true
if (mode.value === 'nodes') return selectedNodes.value.length === 0
return !selectedRegion.value
})
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
if (!selectedRegion.value && regions.value.length) selectedRegion.value = regions.value[0].value
} catch (err) {
addNotification({ title: 'Failed to load nodes overview', text: String(err), type: 'error' })
}
}
async function apply() {
try {
const body =
mode.value === 'nodes'
? {
nodes: selectedNodes.value.slice(),
days: Math.max(1, Math.floor(days.value)),
send_email: sendEmail.value,
message: message.value?.trim() || DEFAULT_CREDIT_EMAIL_MESSAGE,
}
: {
region: selectedRegion.value!,
days: Math.max(1, Math.floor(days.value)),
send_email: sendEmail.value,
message: message.value?.trim() || DEFAULT_CREDIT_EMAIL_MESSAGE,
}
await useBaseFetch('billing/credit', {
method: 'POST',
body: JSON.stringify(body),
internal: true,
})
addNotification({ title: 'Credits applied', type: 'success' })
modal.value?.hide()
selectedNodes.value = []
nodeInput.value = ''
message.value = ''
} catch (err: any) {
addNotification({
title: 'Error applying credits',
text: err?.data?.description ?? String(err),
type: 'error',
})
}
}
function pluralize(n: number): string {
return n === 1 ? '' : 's'
}
defineExpose({
show,
hide,
})
</script>

View File

@@ -356,7 +356,7 @@ svg {
:deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important;
border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important;
}
@@ -371,7 +371,7 @@ svg {
:deep(.apexcharts-xaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important;
border: 1px solid var(--color-divider) !important;
font-size: var(--font-size-nm) !important;
color: var(--color-base) !important;

View File

@@ -891,7 +891,7 @@ const defaultRanges: RangeObject[] = [
flex-direction: column;
background-color: var(--color-bg);
border-radius: var(--radius-sm);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
gap: var(--gap-md);
padding: var(--gap-md);
margin-top: var(--gap-md);
@@ -920,7 +920,7 @@ const defaultRanges: RangeObject[] = [
width: 100%;
height: 1rem;
background-color: var(--color-raised-bg);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
border-radius: var(--radius-sm);
overflow: hidden;

View File

@@ -159,10 +159,10 @@ defineExpose({
flex-direction: column;
gap: var(--gap-xs);
border: 1px solid var(--color-button-bg);
border: 1px solid var(--color-divider);
border-radius: var(--radius-md);
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-floating);
box-shadow: var(--shadow-card);
color: var(--color-base);
font-size: var(--font-size-nm);
@@ -192,7 +192,7 @@ svg {
:deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important;
border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important;
}

View File

@@ -0,0 +1,570 @@
<template>
<NewModal
ref="withdrawModal"
:closable="currentStage !== 'completion'"
:hide-header="currentStage === 'completion'"
:merge-header="currentStage === 'completion'"
:scrollable="true"
max-content-height="72vh"
:on-hide="onModalHide"
>
<template #title>
<div v-if="shouldShowTitle" class="flex flex-wrap items-center gap-1 text-secondary">
<template v-if="currentStage === 'tax-form'">
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.taxFormStage)
}}</span>
</template>
<template v-else-if="currentStage === 'method-selection'">
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.methodSelectionStage)
}}</span>
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
<span class="text-lg text-secondary sm:text-xl">{{
formatMessage(messages.detailsLabel)
}}</span>
</template>
<template v-else-if="isDetailsStage">
<button
class="active:scale-9 bg-transparent p-0 text-lg text-secondary transition-colors duration-200 hover:text-primary sm:text-xl"
@click="goToBreadcrumbStage('method-selection')"
>
{{ formatMessage(messages.methodSelectionStage) }}
</button>
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.detailsLabel)
}}</span>
</template>
</div>
</template>
<div class="mx-auto w-full max-w-[496px] sm:mx-0 sm:min-w-[496px]">
<TaxFormStage
v-if="currentStage === 'tax-form'"
:balance="balance"
:on-show-tax-form="showTaxFormModal"
/>
<MethodSelectionStage
v-else-if="currentStage === 'method-selection'"
:on-show-tax-form="showTaxFormModal"
@close-modal="withdrawModal?.hide()"
/>
<TremendousDetailsStage v-else-if="currentStage === 'tremendous-details'" />
<MuralpayKycStage v-else-if="currentStage === 'muralpay-kyc'" />
<MuralpayDetailsStage v-else-if="currentStage === 'muralpay-details'" />
<LegacyPaypalDetailsStage v-else-if="currentStage === 'paypal-details'" />
<CompletionStage v-else-if="currentStage === 'completion'" />
<div v-else>Something went wrong</div>
</div>
<template #actions>
<div v-if="currentStage === 'completion'" class="mt-4 flex w-full gap-3">
<ButtonStyled class="flex-1">
<button class="w-full text-contrast" @click="handleClose">
{{ formatMessage(messages.closeButton) }}
</button>
</ButtonStyled>
<ButtonStyled class="flex-1">
<button class="w-full text-contrast" @click="handleViewTransactions">
{{ formatMessage(messages.transactionsButton) }}
</button>
</ButtonStyled>
</div>
<div v-else class="mt-4 flex flex-col justify-end gap-2 sm:flex-row">
<ButtonStyled type="outlined">
<button
class="!border-surface-5"
:disabled="leftButtonConfig.disabled"
@click="leftButtonConfig.handler"
>
<component :is="leftButtonConfig.icon" />
{{ leftButtonConfig.label }}
</button>
</ButtonStyled>
<ButtonStyled :color="rightButtonConfig.color">
<button :disabled="rightButtonConfig.disabled" @click="rightButtonConfig.handler">
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'before'"
:class="rightButtonConfig.iconClass"
/>
{{ rightButtonConfig.label }}
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'after'"
:class="rightButtonConfig.iconClass"
/>
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<CreatorTaxFormModal
ref="taxFormModal"
close-button-text="Continue"
@success="onTaxFormSuccess"
@cancelled="onTaxFormCancelled"
/>
</template>
<script setup lang="ts">
import {
ArrowLeftRightIcon,
ChevronRightIcon,
FileTextIcon,
LeftArrowIcon,
RightArrowIcon,
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import {
createWithdrawContext,
type PayoutMethod,
provideWithdrawContext,
TAX_THRESHOLD_ACTUAL,
type WithdrawStage,
} from '@/providers/creator-withdraw.ts'
import CreatorTaxFormModal from './CreatorTaxFormModal.vue'
import CompletionStage from './withdraw-stages/CompletionStage.vue'
import LegacyPaypalDetailsStage from './withdraw-stages/LegacyPaypalDetailsStage.vue'
import MethodSelectionStage from './withdraw-stages/MethodSelectionStage.vue'
import MuralpayDetailsStage from './withdraw-stages/MuralpayDetailsStage.vue'
import MuralpayKycStage from './withdraw-stages/MuralpayKycStage.vue'
import TaxFormStage from './withdraw-stages/TaxFormStage.vue'
import TremendousDetailsStage from './withdraw-stages/TremendousDetailsStage.vue'
type FormCompletionStatus = 'unknown' | 'unrequested' | 'unsigned' | 'tin-mismatch' | 'complete'
interface UserBalanceResponse {
available: number
withdrawn_lifetime: number
withdrawn_ytd: number
pending: number
dates: Record<string, number>
requested_form_type: string | null
form_completion_status: FormCompletionStatus | null
}
const props = defineProps<{
balance: UserBalanceResponse | null
preloadedPaymentData?: { country: string; methods: PayoutMethod[] } | null
}>()
const emit = defineEmits<{
(e: 'refresh-data' | 'hide'): void
}>()
const withdrawModal = useTemplateRef<InstanceType<typeof NewModal>>('withdrawModal')
const taxFormModal = ref<InstanceType<typeof CreatorTaxFormModal> | null>(null)
const isSubmitting = ref(false)
function show(preferred?: WithdrawStage) {
if (preferred) {
setStage(preferred, true)
withdrawModal.value?.show()
return
}
const firstStage = stages.value[0]
if (firstStage) {
setStage(firstStage, true)
}
withdrawModal.value?.show()
}
defineExpose({
show,
})
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const withdrawContext = createWithdrawContext(
props.balance,
props.preloadedPaymentData || undefined,
)
provideWithdrawContext(withdrawContext)
const {
currentStage,
previousStep,
nextStep,
canProceed,
setStage,
withdrawData,
resetData,
stages,
submitWithdrawal,
restoreStateFromStorage,
clearSavedState,
} = withdrawContext
watch(
() => props.balance,
(newBalance) => {
if (newBalance) {
withdrawContext.balance.value = newBalance
}
},
{ deep: true },
)
onMounted(() => {
const route = useRoute()
const router = useRouter()
if (route.query.paypal_auth_return === 'true') {
const savedState = restoreStateFromStorage()
if (savedState?.data) {
withdrawData.value = { ...savedState.data }
nextTick(() => {
show(savedState.stage)
})
clearSavedState()
}
const query = { ...route.query }
delete query.paypal_auth_return
router.replace({ query })
}
})
const needsTaxForm = computed(() => {
if (!props.balance || currentStage.value !== 'tax-form') return false
const ytd = props.balance.withdrawn_ytd ?? 0
const available = props.balance.available ?? 0
const status = props.balance.form_completion_status
return status !== 'complete' && ytd + available >= 600
})
const remainingLimit = computed(() => {
if (!props.balance) return 0
const ytd = props.balance.withdrawn_ytd ?? 0
const raw = TAX_THRESHOLD_ACTUAL - ytd
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100
})
const leftButtonConfig = computed(() => {
if (previousStep.value) {
return {
icon: LeftArrowIcon,
label: formatMessage(commonMessages.backButton),
handler: () => setStage(previousStep.value, true),
disabled: isSubmitting.value,
}
}
return {
icon: XIcon,
label: formatMessage(commonMessages.cancelButton),
handler: () => withdrawModal.value?.hide(),
disabled: isSubmitting.value,
}
})
const rightButtonConfig = computed(() => {
const stage = currentStage.value
const isTaxFormStage = stage === 'tax-form'
const isDetailsStage =
stage === 'muralpay-details' || stage === 'tremendous-details' || stage === 'paypal-details'
if (isTaxFormStage && needsTaxForm.value && remainingLimit.value > 0) {
return {
icon: RightArrowIcon,
label: formatMessage(messages.continueWithLimit),
handler: continueWithLimit,
disabled: false,
color: 'standard' as const,
iconPosition: 'after' as const,
}
}
if (isTaxFormStage && needsTaxForm.value) {
return {
icon: FileTextIcon,
label: formatMessage(messages.completeTaxForm),
handler: showTaxFormModal,
disabled: false,
color: 'orange' as const,
iconPosition: 'before' as const,
}
}
if (isDetailsStage) {
return {
icon: isSubmitting.value ? SpinnerIcon : ArrowLeftRightIcon,
iconClass: isSubmitting.value ? 'animate-spin' : undefined,
label: formatMessage(messages.withdrawButton),
handler: handleWithdraw,
disabled: !canProceed.value || isSubmitting.value,
color: 'brand' as const,
iconPosition: 'before' as const,
}
}
return {
icon: RightArrowIcon,
label: formatMessage(commonMessages.nextButton),
handler: () => setStage(nextStep.value),
disabled: !canProceed.value,
color: 'standard' as const,
iconPosition: 'after' as const,
}
})
function continueWithLimit() {
withdrawData.value.tax.skipped = true
setStage(nextStep.value)
}
// TODO: God we need better errors from the backend (e.g error ids), this shit is insane
function getWithdrawalError(error: any): { title: string; text: string } {
const description = error?.data?.description?.toLowerCase() || ''
// Tax form error
if (description.includes('tax form')) {
return {
title: formatMessage(messages.errorTaxFormTitle),
text: formatMessage(messages.errorTaxFormText),
}
}
// Invalid crypto wallet address
if (
(description.includes('wallet') && description.includes('invalid')) ||
description.includes('wallet_address') ||
(description.includes('blockchain') && description.includes('invalid'))
) {
return {
title: formatMessage(messages.errorInvalidWalletTitle),
text: formatMessage(messages.errorInvalidWalletText),
}
}
// Invalid bank details
if (
(description.includes('bank') || description.includes('account')) &&
(description.includes('invalid') || description.includes('failed'))
) {
return {
title: formatMessage(messages.errorInvalidBankTitle),
text: formatMessage(messages.errorInvalidBankText),
}
}
// Invalid/fraudulent address
if (
description.includes('address') &&
(description.includes('invalid') ||
description.includes('verification') ||
description.includes('fraudulent'))
) {
return {
title: formatMessage(messages.errorInvalidAddressTitle),
text: formatMessage(messages.errorInvalidAddressText),
}
}
// Minimum amount not met
if (
description.includes('payoutminimumnotmeterror') ||
description.includes('minimum') ||
(description.includes('amount') && description.includes('less'))
) {
return {
title: formatMessage(messages.errorMinimumNotMetTitle),
text: formatMessage(messages.errorMinimumNotMetText),
}
}
// Generic fallback
return {
title: formatMessage(messages.errorGenericTitle),
text: formatMessage(messages.errorGenericText),
}
}
async function handleWithdraw() {
if (isSubmitting.value) return
try {
isSubmitting.value = true
await submitWithdrawal()
setStage('completion')
} catch (error) {
console.error('Withdrawal failed:', error)
const { title, text } = getWithdrawalError(error)
addNotification({
title,
text,
type: 'error',
})
} finally {
isSubmitting.value = false
}
}
const shouldShowTitle = computed(() => {
return currentStage.value !== 'completion'
})
const isDetailsStage = computed(() => {
const detailsStages: WithdrawStage[] = [
'tremendous-details',
'muralpay-kyc',
'muralpay-details',
'paypal-details',
]
const current = currentStage.value
return current ? detailsStages.includes(current) : false
})
function showTaxFormModal(e?: MouseEvent) {
withdrawModal.value?.hide()
taxFormModal.value?.startTaxForm(e ?? new MouseEvent('click'))
}
function onTaxFormSuccess() {
emit('refresh-data')
nextTick(() => {
show('method-selection')
})
}
function onTaxFormCancelled() {
show('tax-form')
}
function onModalHide() {
resetData()
emit('hide')
}
function goToBreadcrumbStage(stage: WithdrawStage) {
setStage(stage, true)
}
function handleClose() {
withdrawModal.value?.hide()
emit('refresh-data')
}
function handleViewTransactions() {
withdrawModal.value?.hide()
navigateTo('/dashboard/revenue/transfers')
}
const messages = defineMessages({
taxFormStage: {
id: 'dashboard.creator-withdraw-modal.stage.tax-form',
defaultMessage: 'Tax form',
},
methodSelectionStage: {
id: 'dashboard.creator-withdraw-modal.stage.method-selection',
defaultMessage: 'Method',
},
tremendousDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.tremendous-details',
defaultMessage: 'Details',
},
muralpayKycStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-kyc',
defaultMessage: 'Verification',
},
muralpayDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-details',
defaultMessage: 'Account Details',
},
completionStage: {
id: 'dashboard.creator-withdraw-modal.stage.completion',
defaultMessage: 'Complete',
},
detailsLabel: {
id: 'dashboard.creator-withdraw-modal.details-label',
defaultMessage: 'Details',
},
completeTaxForm: {
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
defaultMessage: 'Complete tax form',
},
continueWithLimit: {
id: 'dashboard.creator-withdraw-modal.continue-with-limit',
defaultMessage: 'Continue with limit',
},
withdrawButton: {
id: 'dashboard.creator-withdraw-modal.withdraw-button',
defaultMessage: 'Withdraw',
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
},
errorTaxFormTitle: {
id: 'dashboard.withdraw.error.tax-form.title',
defaultMessage: 'Please complete tax form',
},
errorTaxFormText: {
id: 'dashboard.withdraw.error.tax-form.text',
defaultMessage: 'You must complete a tax form to submit your withdrawal request.',
},
errorInvalidWalletTitle: {
id: 'dashboard.withdraw.error.invalid-wallet.title',
defaultMessage: 'Invalid wallet address',
},
errorInvalidWalletText: {
id: 'dashboard.withdraw.error.invalid-wallet.text',
defaultMessage:
'The crypto wallet address you provided is invalid. Please double-check and try again.',
},
errorInvalidBankTitle: {
id: 'dashboard.withdraw.error.invalid-bank.title',
defaultMessage: 'Invalid bank details',
},
errorInvalidBankText: {
id: 'dashboard.withdraw.error.invalid-bank.text',
defaultMessage:
'The bank account details you provided are invalid. Please verify your information.',
},
errorInvalidAddressTitle: {
id: 'dashboard.withdraw.error.invalid-address.title',
defaultMessage: 'Address verification failed',
},
errorInvalidAddressText: {
id: 'dashboard.withdraw.error.invalid-address.text',
defaultMessage:
'The address you provided could not be verified. Please check your address details.',
},
errorMinimumNotMetTitle: {
id: 'dashboard.withdraw.error.minimum-not-met.title',
defaultMessage: 'Amount too low',
},
errorMinimumNotMetText: {
id: 'dashboard.withdraw.error.minimum-not-met.text',
defaultMessage:
"The withdrawal amount (after fees) doesn't meet the minimum requirement. Please increase your withdrawal amount.",
},
errorGenericTitle: {
id: 'dashboard.withdraw.error.generic.title',
defaultMessage: 'Unable to withdraw',
},
errorGenericText: {
id: 'dashboard.withdraw.error.generic.text',
defaultMessage:
'We were unable to submit your withdrawal request, please check your details or contact support.',
},
})
</script>

View File

@@ -0,0 +1,158 @@
<template>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<input
ref="amountInput"
:value="modelValue"
type="number"
step="0.01"
:min="minAmount"
:max="safeMaxAmount"
:disabled="isDisabled"
:placeholder="formatMessage(formFieldPlaceholders.amountPlaceholder)"
class="w-full rounded-[14px] bg-surface-4 py-2.5 pl-4 pr-4 text-contrast placeholder:text-secondary"
@input="handleInput"
/>
</div>
<Combobox
v-if="showCurrencySelector"
:model-value="selectedCurrency"
:options="currencyOptions"
class="w-min"
@update:model-value="$emit('update:selectedCurrency', $event)"
>
<template v-for="option in currencyOptions" :key="option.value" #[`option-${option.value}`]>
<span class="font-semibold leading-tight">{{ option.label }}</span>
</template>
</Combobox>
<ButtonStyled>
<button class="px-4 py-2" :disabled="isDisabled" @click="setMaxAmount">
{{ formatMessage(commonMessages.maxButton) }}
</button>
</ButtonStyled>
</div>
<div>
<span class="my-1 mt-0 text-secondary">{{ formatMoney(safeMaxAmount) }} available.</span>
<Transition name="fade">
<span v-if="isBelowMinimum" class="text-red">
Amount must be at least {{ formatMoney(minAmount) }}.
</span>
</Transition>
<Transition name="fade">
<span v-if="isAboveMaximum" class="text-red">
Amount cannot exceed {{ formatMoney(safeMaxAmount) }}.
</span>
</Transition>
</div>
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, Combobox, commonMessages, formFieldPlaceholders } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed, nextTick, ref, watch } from 'vue'
const props = withDefaults(
defineProps<{
modelValue: number | undefined
maxAmount: number
minAmount?: number
showCurrencySelector?: boolean
selectedCurrency?: string
currencyOptions?: Array<{ value: string; label: string }>
}>(),
{
minAmount: 0.01,
showCurrencySelector: false,
currencyOptions: () => [],
},
)
const emit = defineEmits<{
'update:modelValue': [value: number | undefined]
'update:selectedCurrency': [value: string]
}>()
const { formatMessage } = useVIntl()
const amountInput = ref<HTMLInputElement | null>(null)
const safeMaxAmount = computed(() => {
return Math.max(0, props.maxAmount)
})
const isDisabled = computed(() => {
return safeMaxAmount.value < 0.01
})
const isBelowMinimum = computed(() => {
return (
props.modelValue !== undefined && props.modelValue > 0 && props.modelValue < props.minAmount
)
})
const isAboveMaximum = computed(() => {
return props.modelValue !== undefined && props.modelValue > safeMaxAmount.value
})
async function setMaxAmount() {
const maxValue = safeMaxAmount.value
emit('update:modelValue', maxValue)
await nextTick()
if (amountInput.value) {
amountInput.value.value = maxValue.toFixed(2)
}
}
function handleInput(event: Event) {
const input = event.target as HTMLInputElement
const value = input.value
if (value && value.includes('.')) {
const parts = value.split('.')
if (parts[1] && parts[1].length > 2) {
const rounded = Math.floor(parseFloat(value) * 100) / 100
emit('update:modelValue', rounded)
input.value = rounded.toString()
return
}
}
const numValue = value === '' ? undefined : parseFloat(value)
emit('update:modelValue', numValue)
}
watch(
() => props.modelValue,
async (newAmount) => {
if (newAmount !== undefined && newAmount !== null) {
if (newAmount > safeMaxAmount.value) {
emit('update:modelValue', safeMaxAmount.value)
await nextTick()
if (amountInput.value) {
amountInput.value.value = safeMaxAmount.value.toFixed(2)
}
} else if (newAmount < 0) {
emit('update:modelValue', 0)
}
}
},
)
</script>
<style scoped>
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 150ms ease-in;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="flex flex-row gap-2 md:gap-3">
<div
class="flex h-10 min-h-10 w-10 min-w-10 justify-center rounded-full border-[1px] border-solid border-button-bg bg-bg-raised !p-0 shadow-md md:h-12 md:min-h-12 md:w-12 md:min-w-12"
>
<ArrowDownIcon v-if="isIncome" class="my-auto size-6 text-secondary md:size-8" />
<ArrowUpIcon v-else class="my-auto size-6 text-secondary md:size-8" />
</div>
<div class="flex w-full flex-row justify-between">
<div class="flex flex-col">
<span class="text-base font-semibold text-contrast md:text-lg">{{
transaction.type === 'payout_available'
? formatPayoutSource(transaction.payout_source)
: formatMethodName(transaction.method_type || transaction.method)
}}</span>
<span class="text-xs text-secondary md:text-sm">
<template v-if="transaction.type === 'withdrawal'">
<span
:class="[
transaction.status === 'cancelling' || transaction.status === 'cancelled'
? 'text-red'
: '',
]"
>{{ formatTransactionStatus(transaction.status) }} <BulletDivider
/></span>
</template>
{{ dayjs(transaction.created).format('MMM DD YYYY') }}
<template v-if="transaction.type === 'withdrawal' && transaction.fee">
<BulletDivider /> Fee {{ formatMoney(transaction.fee) }}
</template>
</span>
</div>
<div class="my-auto flex flex-row items-center gap-2">
<span
class="text-base font-semibold md:text-lg"
:class="transaction.type === 'payout_available' ? 'text-green' : 'text-contrast'"
>{{ formatMoney(transaction.amount) }}</span
>
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
<span class="my-auto align-middle"
><ButtonStyled circular type="outlined" size="small">
<button class="align-middle" @click="cancelPayout">
<XIcon />
</button> </ButtonStyled
></span>
<template #popper>
<div class="font-semibold text-contrast">Cancel transaction</div>
</template>
</Tooltip>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowDownIcon, ArrowUpIcon, XIcon } from '@modrinth/assets'
import { BulletDivider, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { capitalizeString, formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'
type PayoutStatus = 'in-transit' | 'cancelling' | 'cancelled' | 'success' | 'failed'
type PayoutMethodType = 'paypal' | 'venmo' | 'tremendous' | 'muralpay'
type PayoutSource = 'creator_rewards' | 'affilites'
type WithdrawalTransaction = {
type: 'withdrawal'
id: string
status: PayoutStatus
created: string
amount: number
fee?: number | null
method_type?: PayoutMethodType | null
method?: string
method_id?: string
method_address?: string | null
}
type PayoutAvailableTransaction = {
type: 'payout_available'
created: string
payout_source: PayoutSource
amount: number
}
type Transaction = WithdrawalTransaction | PayoutAvailableTransaction
const props = defineProps<{
transaction: Transaction
}>()
const emit = defineEmits<{
(e: 'cancelled'): void
}>()
const { addNotification } = injectNotificationManager()
const isIncome = computed(() => props.transaction.type === 'payout_available')
function formatTransactionStatus(status: string): string {
if (status === 'in-transit') return 'In Transit'
return capitalizeString(status)
}
function formatMethodName(method: string | undefined): string {
if (!method) return 'Unknown'
switch (method) {
case 'paypal':
return 'PayPal'
case 'venmo':
return 'Venmo'
case 'tremendous':
return 'Tremendous'
case 'muralpay':
return 'Muralpay'
default:
return capitalizeString(method)
}
}
function formatPayoutSource(source: string | undefined): string {
if (!source) return 'Income'
return source
.split('_')
.map((word: string) => capitalizeString(word))
.join(' ')
}
async function cancelPayout(): Promise<void> {
startLoading()
try {
const transaction = props.transaction
if (transaction.type !== 'withdrawal') return
await useBaseFetch(`payout/${transaction.id}`, {
method: 'DELETE',
apiVersion: 3,
})
await useAuth()
emit('cancelled')
} catch (err: any) {
addNotification({
title: 'Failed to cancel transaction',
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
</script>

View File

@@ -0,0 +1,119 @@
<template>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="amount > 0" class="flex flex-col gap-2.5 rounded-[20px] bg-surface-2 p-4">
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownFee) }}</span>
<span class="h-4 font-semibold text-contrast">
<template v-if="feeLoading">
<LoaderCircleIcon class="size-5 animate-spin !text-secondary" />
</template>
<template v-else>-{{ formatMoney(fee || 0) }}</template>
</span>
</div>
<div class="h-px bg-surface-5" />
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownNetAmount) }}</span>
<span class="font-semibold text-contrast">
{{ formatMoney(netAmount) }}
<template v-if="shouldShowExchangeRate">
<span> ({{ formattedLocalCurrency }})</span>
</template>
</span>
</div>
<template v-if="shouldShowExchangeRate">
<div class="flex items-center justify-between text-sm">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownExchangeRate) }}</span>
<span class="text-secondary"
>1 USD = {{ exchangeRate?.toFixed(4) }} {{ localCurrency }}</span
>
</div>
</template>
</div>
</Transition>
</template>
<script setup lang="ts">
import { LoaderCircleIcon } from '@modrinth/assets'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
amount: number
fee: number | null
feeLoading: boolean
exchangeRate?: number | null
localCurrency?: string
}>(),
{
exchangeRate: null,
localCurrency: undefined,
},
)
const { formatMessage } = useVIntl()
const netAmount = computed(() => {
const amount = props.amount || 0
const fee = props.fee || 0
return Math.max(0, amount - fee)
})
const shouldShowExchangeRate = computed(() => {
if (!props.localCurrency) return false
if (props.localCurrency === 'USD') return false
return props.exchangeRate !== null && props.exchangeRate !== undefined && props.exchangeRate > 0
})
const netAmountInLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value) return null
return netAmount.value * (props.exchangeRate || 0)
})
const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !props.localCurrency)
return ''
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.localCurrency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${props.localCurrency} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
})
const messages = defineMessages({
feeBreakdownAmount: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-amount',
defaultMessage: 'Amount',
},
feeBreakdownFee: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-fee',
defaultMessage: 'Fee',
},
feeBreakdownNetAmount: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-net-amount',
defaultMessage: 'Net amount',
},
feeBreakdownExchangeRate: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-exchange-rate',
defaultMessage: 'FX rate',
},
})
</script>

View File

@@ -0,0 +1,301 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex w-full items-center justify-center gap-2.5">
<span class="text-xl font-semibold text-contrast sm:text-2xl">
{{ formatMessage(messages.title) }}
</span>
</div>
<div class="flex w-full flex-col gap-3">
<div class="span-4 flex w-full flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.method) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ result?.methodType || 'N/A' }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.recipient) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ result?.recipientDisplay || 'N/A' }}
</span>
</div>
<div
v-if="destinationLabel && destinationValue"
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ destinationLabel }}
</span>
<span class="break-words font-mono text-sm font-semibold text-contrast sm:text-[1rem]">
{{ destinationValue }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.date) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formattedDate }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.amount) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.amount || 0) }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.fee) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.fee || 0) }}
</span>
</div>
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-divider" />
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.netAmount) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.netAmount || 0) }}
<template v-if="shouldShowExchangeRate">
<span> ({{ formattedLocalCurrency }})</span>
</template>
</span>
</div>
<template v-if="shouldShowExchangeRate">
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.exchangeRate) }}
</span>
<span class="break-words text-sm font-normal text-secondary sm:text-[1rem]">
1 USD = {{ withdrawData.calculation.exchangeRate?.toFixed(4) }}
{{ localCurrency }}
</span>
</div>
</template>
</div>
<span
v-if="withdrawData.providerData.type === 'tremendous'"
class="w-full break-words text-center text-sm font-normal text-primary sm:text-[1rem]"
>
<IntlFormatted
:message-id="messages.emailConfirmation"
:values="{ email: withdrawData.result?.recipientDisplay }"
>
<template #b="{ children }">
<strong>
<component :is="() => normalizeChildren(children)" />
</strong>
</template>
</IntlFormatted>
</span>
</div>
<Teleport to="body">
<div
v-if="showConfetti"
class="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center"
>
<ConfettiExplosion />
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import dayjs from 'dayjs'
import { computed, onMounted, ref } from 'vue'
import ConfettiExplosion from 'vue-confetti-explosion'
import { type TremendousProviderData, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { getRailConfig } from '@/utils/muralpay-rails'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData } = useWithdrawContext()
const { formatMessage } = useVIntl()
const result = computed(() => withdrawData.value.result)
const showConfetti = ref(false)
onMounted(() => {
showConfetti.value = true
setTimeout(() => {
showConfetti.value = false
}, 5000)
})
const formattedDate = computed(() => {
if (!result.value?.created) return 'N/A'
return dayjs(result.value.created).format('MMMM D, YYYY')
})
const selectedRail = computed(() => {
const railId = withdrawData.value.selection.method
return railId ? getRailConfig(railId) : null
})
const localCurrency = computed(() => {
// Check if it's Tremendous withdrawal with currency
if (withdrawData.value.providerData.type === 'tremendous') {
return (withdrawData.value.providerData as TremendousProviderData).currency
}
// Fall back to MuralPay rail currency
return selectedRail.value?.currency
})
const shouldShowExchangeRate = computed(() => {
if (!localCurrency.value) return false
if (localCurrency.value === 'USD') return false
const exchangeRate = withdrawData.value.calculation.exchangeRate
return exchangeRate !== null && exchangeRate !== undefined && exchangeRate > 0
})
const netAmountInLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value) return null
const netAmount = result.value?.netAmount || 0
const exchangeRate = withdrawData.value.calculation.exchangeRate || 0
return netAmount * exchangeRate
})
const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !localCurrency.value)
return ''
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: localCurrency.value,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${localCurrency.value} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
})
const isMuralPayWithdrawal = computed(() => {
return withdrawData.value.providerData.type === 'muralpay'
})
const destinationLabel = computed(() => {
if (!isMuralPayWithdrawal.value) return null
const rail = selectedRail.value
if (!rail) return null
if (rail.type === 'crypto') {
return formatMessage(messages.wallet)
} else if (rail.type === 'fiat') {
return formatMessage(messages.account)
}
return null
})
const destinationValue = computed(() => {
if (!isMuralPayWithdrawal.value || withdrawData.value.providerData.type !== 'muralpay') {
return null
}
const accountDetails = withdrawData.value.providerData.accountDetails
const rail = selectedRail.value
if (rail?.type === 'crypto' && accountDetails.walletAddress) {
const addr = accountDetails.walletAddress
if (addr.length > 10) {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
return addr
} else if (rail?.type === 'fiat' && accountDetails.bankAccountNumber) {
const accountType = accountDetails.accountType || 'Account'
const last4 = accountDetails.bankAccountNumber.slice(-4)
const formattedType = accountType.charAt(0) + accountType.slice(1).toLowerCase()
return `${formattedType} (${last4})`
}
return null
})
const messages = defineMessages({
title: {
id: 'dashboard.withdraw.completion.title',
defaultMessage: 'Withdraw complete',
},
method: {
id: 'dashboard.withdraw.completion.method',
defaultMessage: 'Method',
},
recipient: {
id: 'dashboard.withdraw.completion.recipient',
defaultMessage: 'Recipient',
},
wallet: {
id: 'dashboard.withdraw.completion.wallet',
defaultMessage: 'Wallet',
},
account: {
id: 'dashboard.withdraw.completion.account',
defaultMessage: 'Account',
},
date: {
id: 'dashboard.withdraw.completion.date',
defaultMessage: 'Date',
},
amount: {
id: 'dashboard.withdraw.completion.amount',
defaultMessage: 'Amount',
},
fee: {
id: 'dashboard.withdraw.completion.fee',
defaultMessage: 'Fee',
},
netAmount: {
id: 'dashboard.withdraw.completion.net-amount',
defaultMessage: 'Net amount',
},
exchangeRate: {
id: 'dashboard.withdraw.completion.exchange-rate',
defaultMessage: 'Exchange rate',
},
emailConfirmation: {
id: 'dashboard.withdraw.completion.email-confirmation',
defaultMessage:
"You'll receive an email at <b>{email}</b> with instructions to redeem your withdrawal.",
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
},
})
</script>

View File

@@ -0,0 +1,352 @@
<template>
<div class="flex flex-col gap-4 sm:gap-5">
<div v-if="isPayPal" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(messages.paypalAccount) }} <span class="text-red">*</span></span
>
</label>
<div class="flex flex-col gap-2">
<ButtonStyled v-if="!isPayPalAuthenticated" color="standard">
<a :href="paypalAuthUrl" class="w-min" @click="handlePayPalAuth">
<PayPalColorIcon class="size-5" />
{{ formatMessage(messages.signInWithPaypal) }}
</a>
</ButtonStyled>
<ButtonStyled v-else>
<button class="w-min" @click="handleDisconnectPaypal">
<XIcon /> {{ formatMessage(messages.disconnectButton) }}
</button>
</ButtonStyled>
</div>
</div>
<div v-if="isPayPal && isPayPalAuthenticated" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.account)
}}</span>
</label>
<div class="flex flex-col gap-2 rounded-2xl bg-surface-2 px-4 py-2.5">
<span>{{ paypalEmail }}</span>
</div>
</div>
<div v-if="isVenmo" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(messages.venmoHandle) }} <span class="text-red">*</span></span
>
</label>
<div class="flex flex-row gap-2">
<input
v-model="venmoHandle"
type="text"
:placeholder="formatMessage(messages.venmoHandlePlaceholder)"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
<ButtonStyled color="brand">
<button
v-tooltip="!hasVenmoChanged ? 'Change the venmo username to save.' : undefined"
:disabled="venmoSaving || !hasVenmoChanged"
@click="saveVenmoHandle"
>
<CheckIcon v-if="venmoSaveSuccess" />
<SaveIcon v-else />
{{
venmoSaveSuccess
? formatMessage(messages.savedButton)
: formatMessage(messages.saveButton)
}}
</button>
</ButtonStyled>
</div>
<span v-if="venmoSaveError" class="text-sm font-bold text-red">
{{ venmoSaveError }}
</span>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.amount) }} <span class="text-red">*</span></span
>
</label>
<RevenueInputField
v-model="formData.amount"
:max-amount="effectiveMaxAmount"
:min-amount="selectedMethodDetails?.interval?.standard?.min || 0.01"
/>
<WithdrawFeeBreakdown
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
/>
<Checkbox v-model="agreedTerms">
<span>
<IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template>
</IntlFormatted>
</span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import { CheckIcon, PayPalColorIcon, SaveIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, Checkbox, financialMessages, formFieldLabels } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.js'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees, saveStateToStorage } =
useWithdrawContext()
const { formatMessage } = useVIntl()
const auth = await useAuth()
const isPayPal = computed(() => withdrawData.value.selection.provider === 'paypal')
const isVenmo = computed(() => withdrawData.value.selection.provider === 'venmo')
const selectedMethodDetails = computed(() => {
const methodId = withdrawData.value.selection.methodId
if (!methodId) return null
return availableMethods.value.find((m) => m.id === methodId) || null
})
const isPayPalAuthenticated = computed(() => {
return (auth.value.user as any)?.auth_providers?.includes('paypal') || false
})
const paypalEmail = computed(() => {
return (auth.value.user as any)?.payout_data?.paypal_address || ''
})
const paypalAuthUrl = computed(() => {
const route = useRoute()
const authToken = useCookie('auth-token')
const separator = route.fullPath.includes('?') ? '&' : '?'
const returnUrl = `${route.fullPath}${separator}paypal_auth_return=true`
return `${getAuthUrl('paypal', returnUrl)}&auth_token=${authToken.value}`
})
function handlePayPalAuth() {
saveStateToStorage()
}
async function handleDisconnectPaypal() {
try {
await removeAuthProvider('paypal')
} catch (error) {
console.error('Failed to disconnect PayPal:', error)
}
}
const venmoHandle = ref<string>((auth.value.user as any)?.venmo_handle || '')
const initialVenmoHandle = ref<string>((auth.value.user as any)?.venmo_handle || '')
const venmoSaving = ref(false)
const venmoSaveSuccess = ref(false)
const venmoSaveError = ref<string | null>(null)
const hasVenmoChanged = computed(() => {
return venmoHandle.value.trim() !== initialVenmoHandle.value.trim()
})
async function saveVenmoHandle() {
if (!venmoHandle.value.trim()) {
venmoSaveError.value = 'Please enter a Venmo handle'
return
}
venmoSaving.value = true
venmoSaveError.value = null
venmoSaveSuccess.value = false
try {
await useBaseFetch(`user/${(auth.value.user as any)?.id}`, {
method: 'PATCH',
body: {
venmo_handle: venmoHandle.value.trim(),
},
})
// @ts-expect-error auth.js is not typed
await useAuth(auth.value.token)
initialVenmoHandle.value = venmoHandle.value.trim()
venmoSaveSuccess.value = true
setTimeout(() => {
venmoSaveSuccess.value = false
}, 3000)
} catch (error) {
console.error('Failed to update Venmo handle:', error)
venmoSaveError.value = 'Failed to save Venmo handle. Please try again.'
} finally {
venmoSaving.value = false
}
}
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const effectiveMaxAmount = computed(() => {
const apiMax = selectedMethodDetails.value?.interval?.standard?.max
if (apiMax) {
return Math.min(roundedMaxAmount.value, apiMax)
}
return roundedMaxAmount.value
})
const formData = ref<Record<string, any>>({
amount: withdrawData.value.calculation.amount || undefined,
})
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const isComponentValid = computed(() => {
const hasAmount = (formData.value.amount || 0) > 0
const hasAgreed = agreedTerms.value
if (!hasAmount || !hasAgreed) return false
if (isPayPal.value) {
return isPayPalAuthenticated.value
} else if (isVenmo.value) {
return venmoHandle.value.trim().length > 0
}
return false
})
const calculatedFee = ref<number>(0)
const feeLoading = ref(false)
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
if (!amount || amount <= 0) {
calculatedFee.value = 0
return
}
const methodId = withdrawData.value.selection.methodId
if (!methodId) {
calculatedFee.value = 0
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee ?? 0
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
} finally {
feeLoading.value = false
}
}, 500)
watch(
() => formData.value.amount,
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (formData.value.amount) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = 0
feeLoading.value = false
}
},
{ deep: true },
)
watch(
[isComponentValid, venmoHandle, () => formData.value.amount, agreedTerms, isPayPalAuthenticated],
() => {
withdrawData.value.stageValidation.paypalDetails = isComponentValid.value
},
{ immediate: true },
)
onMounted(async () => {
if (formData.value.amount) {
feeLoading.value = true
calculateFeesDebounced()
}
})
const messages = defineMessages({
paymentMethod: {
id: 'dashboard.creator-withdraw-modal.paypal-details.payment-method',
defaultMessage: 'Payment method',
},
paypalAccount: {
id: 'dashboard.creator-withdraw-modal.paypal-details.paypal-account',
defaultMessage: 'PayPal account',
},
account: {
id: 'dashboard.creator-withdraw-modal.paypal-details.account',
defaultMessage: 'Account',
},
signInWithPaypal: {
id: 'dashboard.creator-withdraw-modal.paypal-details.sign-in-with-paypal',
defaultMessage: 'Sign in with PayPal',
},
paypalAuthDescription: {
id: 'dashboard.creator-withdraw-modal.paypal-details.paypal-auth-description',
defaultMessage: 'Connect your PayPal account to receive payments directly.',
},
venmoHandle: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-handle',
defaultMessage: 'Venmo handle',
},
venmoHandlePlaceholder: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-handle-placeholder',
defaultMessage: '@username',
},
venmoDescription: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-description',
defaultMessage: 'Enter your Venmo handle to receive payments.',
},
disconnectButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.disconnect-account',
defaultMessage: 'Disconnect account',
},
saveButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-button',
defaultMessage: 'Save',
},
savedButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.saved-button',
defaultMessage: 'Saved',
},
saveSuccess: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-success',
defaultMessage: 'Venmo handle saved successfully!',
},
})
</script>

View File

@@ -0,0 +1,323 @@
<template>
<div class="flex flex-col gap-4">
<Admonition v-if="shouldShowTaxLimitWarning" type="warning">
<IntlFormatted
:message-id="messages.taxLimitWarning"
:values="{
amount: formatMoney(maxWithdrawAmount),
}"
>
<template #b="{ children }">
<span class="font-semibold">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
<template #tax-link="{ children }">
<span class="cursor-pointer text-link" @click="onShowTaxForm">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</Admonition>
<div class="flex flex-col">
<div class="flex flex-col gap-2.5">
<div class="flex flex-row gap-1 align-middle">
<span class="align-middle font-semibold text-contrast">{{
formatMessage(messages.region)
}}</span>
<UnknownIcon
v-tooltip="formatMessage(messages.regionTooltip)"
class="mt-auto size-5 text-secondary"
/>
</div>
<Combobox
:model-value="selectedCountryCode"
:options="countries"
:placeholder="formatMessage(messages.countryPlaceholder)"
searchable
:search-placeholder="formatMessage(messages.countrySearchPlaceholder)"
:max-height="240"
class="h-12"
@update:model-value="handleCountryChange"
/>
</div>
<div class="flex flex-col gap-2.5">
<div class="flex flex-row gap-1 align-middle">
<span class="align-middle font-semibold text-contrast">{{
formatMessage(messages.selectMethod)
}}</span>
</div>
<div v-if="loading" class="flex min-h-[120px] items-center justify-center">
<SpinnerIcon class="size-8 animate-spin text-contrast" />
</div>
<template v-else>
<ButtonStyled
v-for="method in paymentOptions"
:key="method.value"
:color="withdrawData.selection.method === method.value ? 'green' : 'standard'"
:highlighted="withdrawData.selection.method === method.value"
type="chip"
>
<button
class="!justify-start !gap-2 !text-left sm:!h-10"
@click="handleMethodSelection(method)"
>
<component :is="method.icon" class="shrink-0" />
<span class="flex-1 truncate text-sm sm:text-[1rem]">
{{ typeof method.label === 'string' ? method.label : formatMessage(method.label) }}
</span>
<span class="ml-auto shrink-0 text-xs font-normal text-secondary sm:text-sm">{{
method.fee
}}</span>
</button>
</ButtonStyled>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { SpinnerIcon, UnknownIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
Combobox,
injectNotificationManager,
useDebugLogger,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useGeolocation } from '@vueuse/core'
import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts'
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const debug = useDebugLogger('MethodSelectionStage')
const {
withdrawData,
availableMethods,
paymentOptions,
balance,
maxWithdrawAmount,
paymentMethodsCache,
} = useWithdrawContext()
const userCountry = useUserCountry()
const allCountries = useCountries()
const { coords } = useGeolocation()
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const auth = await useAuth()
const messages = defineMessages({
taxLimitWarning: {
id: 'dashboard.creator-withdraw-modal.method-selection.tax-limit-warning',
defaultMessage:
'Your withdraw limit is <b>{amount}</b>, <tax-link>complete a tax form</tax-link> to withdraw more.',
},
region: {
id: 'dashboard.creator-withdraw-modal.method-selection.region',
defaultMessage: 'Region',
},
regionTooltip: {
id: 'dashboard.creator-withdraw-modal.method-selection.region-tooltip',
defaultMessage: 'Some payout methods are not available in certain regions.',
},
countryPlaceholder: {
id: 'dashboard.creator-withdraw-modal.method-selection.country-placeholder',
defaultMessage: 'Select your country',
},
countrySearchPlaceholder: {
id: 'dashboard.creator-withdraw-modal.method-selection.country-search-placeholder',
defaultMessage: 'Search countries...',
},
selectMethod: {
id: 'dashboard.creator-withdraw-modal.method-selection.select-method',
defaultMessage: 'Select withdraw method',
},
errorTitle: {
id: 'dashboard.creator-withdraw-modal.method-selection.error-title',
defaultMessage: 'Failed to load payment methods',
},
errorText: {
id: 'dashboard.creator-withdraw-modal.method-selection.error-text',
defaultMessage: 'Unable to fetch available payment methods. Please try again later.',
},
})
defineProps<{
onShowTaxForm: () => void
}>()
const emit = defineEmits<{
(e: 'close-modal'): void
}>()
const countries = useFormattedCountries()
const selectedCountryCode = computed(() => withdrawData.value.selection.country?.id)
const shouldShowTaxLimitWarning = computed(() => {
const balanceValue = balance.value
if (!balanceValue) return false
const formIncomplete = balanceValue.form_completion_status !== 'complete'
const wouldHitLimit = (balanceValue.withdrawn_ytd ?? 0) + (balanceValue.available ?? 0) >= 600
return formIncomplete && wouldHitLimit
})
const loading = ref(false)
watch(
() => withdrawData.value.selection.country,
async (country) => {
debug('Watch triggered, country:', country)
if (!country) {
availableMethods.value = []
return
}
if (paymentMethodsCache.value[country.id]) {
debug('Using cached methods for', country.id)
availableMethods.value = paymentMethodsCache.value[country.id]
return
}
loading.value = true
debug('Fetching payout methods for country:', country.id)
try {
const methods = (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: country.id },
})) as PayoutMethod[]
debug('Received payout methods:', methods)
paymentMethodsCache.value[country.id] = methods
availableMethods.value = methods
} catch (e) {
console.error('[MethodSelectionStage] Failed to fetch payout methods:', e)
addNotification({
title: formatMessage(messages.errorTitle),
text: formatMessage(messages.errorText),
type: 'error',
})
emit('close-modal')
} finally {
loading.value = false
}
},
{ immediate: true },
)
function handleMethodSelection(option: {
value: string
methodId: string | undefined
type: string
}) {
withdrawData.value.selection.method = option.value
withdrawData.value.selection.methodId = option.methodId ?? null
if (option.type === 'tremendous') {
withdrawData.value.selection.provider = 'tremendous'
} else if (option.type === 'fiat' || option.type === 'crypto') {
withdrawData.value.selection.provider = 'muralpay'
} else if (option.type === 'paypal') {
withdrawData.value.selection.provider = 'paypal'
} else if (option.type === 'venmo') {
withdrawData.value.selection.provider = 'venmo'
} else {
withdrawData.value.selection.provider = 'muralpay'
}
}
watch(paymentOptions, (newOptions) => {
withdrawData.value.selection.method = null
withdrawData.value.selection.methodId = null
withdrawData.value.selection.provider = null
if (newOptions.length === 1) {
const option = newOptions[0]
handleMethodSelection(option)
}
})
watch(
() => withdrawData.value.selection.provider,
(newProvider) => {
if (newProvider === 'tremendous') {
const userEmail = (auth.value.user as any)?.email || ''
withdrawData.value.providerData = {
type: 'tremendous',
deliveryEmail: userEmail,
giftCardDetails: null,
currency: undefined,
}
} else if (newProvider === 'muralpay') {
withdrawData.value.providerData = {
type: 'muralpay',
kycData: {} as any,
accountDetails: {},
}
} else if (newProvider === 'paypal' || newProvider === 'venmo') {
withdrawData.value.providerData = {
type: newProvider,
}
}
},
)
function handleCountryChange(countryCode: string | null) {
debug('handleCountryChange called with:', countryCode)
if (countryCode) {
const normalizedCode = countryCode.toUpperCase()
const country = allCountries.value.find((c) => c.alpha2 === normalizedCode)
debug('Found country:', country)
if (country) {
withdrawData.value.selection.country = {
id: country.alpha2,
name: country.alpha2 === 'TW' ? 'Taiwan' : country.nameShort,
}
debug('Set selectedCountry to:', withdrawData.value.selection.country)
}
} else {
withdrawData.value.selection.country = null
}
}
debug('Setup: userCountry.value =', userCountry.value)
debug('Setup: current selectedCountry =', withdrawData.value.selection.country)
if (!withdrawData.value.selection.country) {
const defaultCountryCode = userCountry.value || 'US'
debug('Setup: calling handleCountryChange with', defaultCountryCode)
handleCountryChange(defaultCountryCode)
debug('Setup: selectedCountryCode computed =', selectedCountryCode.value)
}
async function getCountryFromGeoIP(lat: number, lon: number): Promise<string | null> {
try {
const response = await fetch(
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`,
)
const data = await response.json()
return data.countryCode || null
} catch {
return null
}
}
onMounted(async () => {
if (withdrawData.value.selection.country?.id === 'US' && !userCountry.value) {
if (coords.value.latitude && coords.value.longitude) {
const geoCountry = await getCountryFromGeoIP(coords.value.latitude, coords.value.longitude)
if (geoCountry) {
handleCountryChange(geoCountry)
}
}
}
})
</script>

View File

@@ -0,0 +1,516 @@
<template>
<div class="flex flex-col gap-3 sm:gap-4">
<Admonition
v-if="selectedRail?.warningMessage"
type="warning"
:header="formatMessage(messages.cryptoWarningHeader)"
>
{{ formatMessage(selectedRail.warningMessage) }}
</Admonition>
<div v-if="selectedRail?.type === 'crypto'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.coin) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component
:is="getCurrencyIcon(selectedRail.currency)"
class="size-5 shrink-0"
:class="getCurrencyColor(selectedRail.currency)"
/>
<span class="text-sm font-semibold text-contrast sm:text-[1rem]">{{
selectedRail.currency
}}</span>
</div>
</div>
<div v-if="selectedRail?.type === 'fiat'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.accountOwner) }}
</span>
</label>
<div class="w-full rounded-[14px] bg-surface-2 p-3 sm:p-4">
<div class="flex flex-col gap-1">
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ accountOwnerName }}
</span>
<span class="break-words text-xs text-primary sm:text-sm">
{{ accountOwnerAddress }}
</span>
</div>
</div>
</div>
<div v-if="selectedRail?.requiresBankName" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.bankName) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-if="shouldShowBankNameDropdown"
v-model="formData.bankName"
:options="bankNameOptions"
:searchable="true"
:placeholder="formatMessage(formFieldPlaceholders.bankNamePlaceholderDropdown)"
class="h-10"
/>
<input
v-else
v-model="formData.bankName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.bankNamePlaceholder)"
autocomplete="off"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div v-for="field in selectedRail?.fields" :key="field.name" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(field.label) }}
<span v-if="field.required" class="text-red">*</span>
</span>
</label>
<input
v-if="['text', 'email', 'tel'].includes(field.type)"
v-model="formData[field.name]"
:type="field.type"
:placeholder="field.placeholder ? formatMessage(field.placeholder) : undefined"
:pattern="field.pattern"
:autocomplete="field.autocomplete || 'off'"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
<Combobox
v-else-if="field.type === 'select'"
v-model="formData[field.name]"
:options="
(field.options || []).map((opt) => ({
value: opt.value,
label: formatMessage(opt.label),
}))
"
:placeholder="field.placeholder ? formatMessage(field.placeholder) : undefined"
class="h-10"
/>
<input
v-else-if="field.type === 'date'"
v-model="formData[field.name]"
type="date"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
<span v-if="field.helpText" class="text-sm text-secondary">
{{ formatMessage(field.helpText) }}
</span>
</div>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="dynamicDocumentNumberField" class="overflow-hidden">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ dynamicDocumentNumberField.label }}
<span v-if="dynamicDocumentNumberField.required" class="text-red">*</span>
</span>
</label>
<input
v-model="formData.documentNumber"
:type="dynamicDocumentNumberField.type"
:placeholder="dynamicDocumentNumberField.placeholder"
autocomplete="off"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
</Transition>
<div v-if="selectedRail?.blockchain" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.network) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component
:is="getBlockchainIcon(selectedRail.blockchain)"
class="size-5 shrink-0"
:class="getBlockchainColor(selectedRail.blockchain)"
/>
<span class="text-sm font-semibold text-contrast sm:text-[1rem]">{{
selectedRail.blockchain
}}</span>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.amount) }}
<span class="text-red">*</span>
</span>
</label>
<RevenueInputField
v-model="formData.amount"
:min-amount="effectiveMinAmount"
:max-amount="effectiveMaxAmount"
/>
<WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled"
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
:exchange-rate="exchangeRate"
:local-currency="selectedRail?.currency"
/>
<Checkbox v-model="agreedTerms">
<span
><IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template> </IntlFormatted
></span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import {
Admonition,
Checkbox,
Combobox,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { useGeneratedState } from '@/composables/generated'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import {
getBlockchainColor,
getBlockchainIcon,
getCurrencyColor,
getCurrencyIcon,
} from '@/utils/finance-icons.ts'
import { getRailConfig } from '@/utils/muralpay-rails'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees } = useWithdrawContext()
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const selectedRail = computed(() => {
const railId = withdrawData.value.selection.method
return railId ? getRailConfig(railId) : null
})
const selectedMethodDetails = computed(() => {
const methodId = withdrawData.value.selection.methodId
if (!methodId) return null
return availableMethods.value.find((m) => m.id === methodId) || null
})
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const effectiveMinAmount = computed(
() => selectedMethodDetails.value?.interval?.standard?.min || 0.01,
)
const effectiveMaxAmount = computed(() => {
const apiMax = selectedMethodDetails.value?.interval?.standard?.max
if (apiMax) {
return Math.min(roundedMaxAmount.value, apiMax)
}
return roundedMaxAmount.value
})
const availableBankNames = computed(() => {
const rail = selectedRail.value
if (!rail || !rail.railCode) return []
const bankDetails = generatedState.value.muralBankDetails?.[rail.railCode]
return bankDetails?.bankNames || []
})
const shouldShowBankNameDropdown = computed(() => {
return availableBankNames.value.length > 0
})
const bankNameOptions = computed(() => {
return availableBankNames.value.map((name) => ({
value: name,
label: name,
}))
})
const providerData = withdrawData.value.providerData
const existingAccountDetails = providerData.type === 'muralpay' ? providerData.accountDetails : {}
const existingAmount = withdrawData.value.calculation.amount
const formData = ref<Record<string, any>>({
amount: existingAmount || undefined,
bankName: existingAccountDetails?.bankName ?? '',
...existingAccountDetails,
})
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const calculatedFee = ref<number | null>(null)
const exchangeRate = ref<number | null>(null)
const feeLoading = ref(false)
const hasDocumentTypeField = computed(() => {
const rail = selectedRail.value
if (!rail) return false
return rail.fields.some((field) => field.name === 'documentType')
})
const dynamicDocumentNumberField = computed(() => {
if (!hasDocumentTypeField.value) return null
const documentType = formData.value.documentType
if (!documentType) return null
const labelMap: Record<string, string> = {
NATIONAL_ID: formatMessage(messages.documentNumberNationalId),
PASSPORT: formatMessage(messages.documentNumberPassport),
RESIDENT_ID: formatMessage(messages.documentNumberResidentId),
RUC: formatMessage(messages.documentNumberRuc),
TAX_ID: formatMessage(messages.documentNumberTaxId),
}
const placeholderMap: Record<string, string> = {
NATIONAL_ID: formatMessage(messages.documentNumberNationalIdPlaceholder),
PASSPORT: formatMessage(messages.documentNumberPassportPlaceholder),
RESIDENT_ID: formatMessage(messages.documentNumberResidentIdPlaceholder),
RUC: formatMessage(messages.documentNumberRucPlaceholder),
TAX_ID: formatMessage(messages.documentNumberTaxIdPlaceholder),
}
return {
name: 'documentNumber',
type: 'text' as const,
label: labelMap[documentType] || 'Document number',
placeholder: placeholderMap[documentType] || 'Enter document number',
required: true,
}
})
const accountOwnerName = computed(() => {
const providerDataValue = withdrawData.value.providerData
if (providerDataValue.type !== 'muralpay') return ''
const kycData = providerDataValue.kycData
if (!kycData) return ''
if (kycData.type === 'individual') {
return `${kycData.firstName} ${kycData.lastName}`
} else if (kycData.type === 'business') {
return kycData.name
}
return ''
})
const accountOwnerAddress = computed(() => {
const providerDataValue = withdrawData.value.providerData
if (providerDataValue.type !== 'muralpay') return ''
const kycData = providerDataValue.kycData
if (!kycData || !kycData.physicalAddress) return ''
const addr = kycData.physicalAddress
const parts = [
addr.address1,
addr.address2,
addr.city,
addr.state,
addr.zip,
addr.country,
].filter(Boolean)
return parts.join(', ')
})
const allRequiredFieldsFilled = computed(() => {
const rail = selectedRail.value
if (!rail) return false
const amount = formData.value.amount
if (!amount || amount <= 0) return false
if (rail.requiresBankName && !formData.value.bankName) return false
const requiredFields = rail.fields.filter((f) => f.required)
const allRequiredPresent = requiredFields.every((f) => {
const value = formData.value[f.name]
return value !== undefined && value !== null && value !== ''
})
if (!allRequiredPresent) return false
if (dynamicDocumentNumberField.value?.required && !formData.value.documentNumber) return false
return true
})
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
const rail = selectedRail.value
const providerDataValue = withdrawData.value.providerData
const kycData = providerDataValue.type === 'muralpay' ? providerDataValue.kycData : null
if (!amount || amount <= 0 || !rail || !kycData) {
calculatedFee.value = null
exchangeRate.value = null
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee
exchangeRate.value = withdrawData.value.calculation.exchangeRate
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
exchangeRate.value = null
} finally {
feeLoading.value = false
}
}, 500)
watch(
formData,
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.accountDetails = { ...formData.value }
}
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = null
exchangeRate.value = null
feeLoading.value = false
}
},
{ deep: true },
)
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
}
watch(
() => withdrawData.value.selection.method,
(newMethod, oldMethod) => {
if (oldMethod && newMethod !== oldMethod) {
formData.value = {
amount: undefined,
bankName: '',
}
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.accountDetails = {}
}
withdrawData.value.calculation.amount = 0
calculatedFee.value = null
exchangeRate.value = null
}
},
)
const messages = defineMessages({
accountOwner: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.account-owner',
defaultMessage: 'Account owner',
},
cryptoWarningHeader: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.crypto-warning-header',
defaultMessage: 'Confirm your wallet address',
},
coin: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.coin',
defaultMessage: 'Coin',
},
network: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.network',
defaultMessage: 'Network',
},
documentNumberNationalId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-national-id',
defaultMessage: 'National ID number',
},
documentNumberPassport: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-passport',
defaultMessage: 'Passport number',
},
documentNumberResidentId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-resident-id',
defaultMessage: 'Resident ID number',
},
documentNumberRuc: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-ruc',
defaultMessage: 'RUC number',
},
documentNumberTaxId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-tax-id',
defaultMessage: 'Tax ID number',
},
documentNumberNationalIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-national-id-placeholder',
defaultMessage: 'Enter national ID number',
},
documentNumberPassportPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-passport-placeholder',
defaultMessage: 'Enter passport number',
},
documentNumberResidentIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-resident-id-placeholder',
defaultMessage: 'Enter resident ID number',
},
documentNumberRucPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-ruc-placeholder',
defaultMessage: 'Enter RUC number',
},
documentNumberTaxIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-tax-id-placeholder',
defaultMessage: 'Enter tax ID number',
},
})
</script>

View File

@@ -0,0 +1,362 @@
<template>
<div class="flex flex-col gap-3 sm:gap-4">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.entityQuestion) }}
<span class="text-red">*</span>
</span>
</label>
<Chips
v-model="entityType"
:items="['individual', 'business']"
:format-label="
(item: string) =>
item === 'individual'
? formatMessage(messages.privateIndividual)
: formatMessage(messages.businessEntity)
"
:never-empty="false"
:capitalize="false"
/>
<span class="leading-tight text-primary">
{{ formatMessage(messages.entityDescription) }}
</span>
</div>
<div v-if="entityType" class="flex flex-col gap-4">
<div v-if="entityType === 'business'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.businessName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.businessName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.businessNamePlaceholder)"
autocomplete="organization"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.email) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.email"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
autocomplete="email"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div v-if="entityType === 'individual'" class="flex flex-col gap-6">
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.firstName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.firstName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.firstNamePlaceholder)"
autocomplete="given-name"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.lastName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.lastName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.lastNamePlaceholder)"
autocomplete="family-name"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.dateOfBirth) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.dateOfBirth"
type="date"
:max="maxDate"
autocomplete="bday"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.addressLine) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.address1"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.addressPlaceholder)"
autocomplete="address-line1"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.addressLine2) }}
</span>
</label>
<input
v-model="formData.physicalAddress.address2"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.address2Placeholder)"
autocomplete="address-line2"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.city) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.city"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.cityPlaceholder)"
autocomplete="address-level2"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.stateProvince) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-if="subdivisionOptions.length > 0"
v-model="formData.physicalAddress.state"
:options="subdivisionOptions"
:placeholder="formatMessage(formFieldPlaceholders.statePlaceholder)"
searchable
search-placeholder="Search subdivisions..."
/>
<input
v-else
v-model="formData.physicalAddress.state"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.statePlaceholder)"
autocomplete="address-level1"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.postalCode) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.zip"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.postalCodePlaceholder)"
autocomplete="postal-code"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.country) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-model="formData.physicalAddress.country"
:options="countryOptions"
:placeholder="formatMessage(formFieldPlaceholders.countryPlaceholder)"
searchable
search-placeholder="Search countries..."
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Chips, Combobox, formFieldLabels, formFieldPlaceholders } from '@modrinth/ui'
import { useVIntl } from '@vintl/vintl'
import { useFormattedCountries } from '@/composables/country.ts'
import { useGeneratedState } from '@/composables/generated.ts'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
const { withdrawData } = useWithdrawContext()
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const providerData = withdrawData.value.providerData
const existingKycData = providerData.type === 'muralpay' ? providerData.kycData : null
const entityType = ref<'individual' | 'business' | null>(existingKycData?.type ?? null)
interface PayoutRecipientInfoMerged {
email: string
firstName?: string
lastName?: string
dateOfBirth?: string
businessName?: string
physicalAddress: {
address1: string
address2: string | null
country: string
state: string
city: string
zip: string
}
}
const auth = await useAuth()
const formData = ref<PayoutRecipientInfoMerged>({
email: existingKycData?.email ?? `${(auth.value.user as any)?.email}`,
firstName: existingKycData?.type === 'individual' ? existingKycData.firstName : '',
lastName: existingKycData?.type === 'individual' ? existingKycData.lastName : '',
dateOfBirth: existingKycData?.type === 'individual' ? existingKycData.dateOfBirth : '',
businessName: existingKycData?.type === 'business' ? existingKycData.name : '',
physicalAddress: {
address1: existingKycData?.physicalAddress?.address1 ?? '',
address2: existingKycData?.physicalAddress?.address2 ?? null,
country:
existingKycData?.physicalAddress?.country ?? withdrawData.value.selection.country?.id ?? '',
state: existingKycData?.physicalAddress?.state ?? '',
city: existingKycData?.physicalAddress?.city ?? '',
zip: existingKycData?.physicalAddress?.zip ?? '',
},
})
const maxDate = computed(() => {
const today = new Date()
const year = today.getFullYear() - 18
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
})
const countryOptions = useFormattedCountries()
const subdivisionOptions = computed(() => {
const selectedCountry = formData.value.physicalAddress.country
if (!selectedCountry) return []
const subdivisions = generatedState.value.subdivisions?.[selectedCountry] ?? []
return subdivisions.map((sub) => ({
value: sub.code.includes('-') ? sub.code.split('-')[1] : sub.code,
label: sub.localVariant || sub.name,
}))
})
watch(
[entityType, formData],
() => {
if (!entityType.value) {
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.kycData = null as any
}
return
}
if (withdrawData.value.providerData.type !== 'muralpay') return
if (entityType.value === 'individual') {
if (formData.value.dateOfBirth) {
withdrawData.value.providerData.kycData = {
type: 'individual',
firstName: formData.value.firstName || '',
lastName: formData.value.lastName || '',
email: formData.value.email,
dateOfBirth: formData.value.dateOfBirth,
physicalAddress: {
address1: formData.value.physicalAddress.address1,
address2: formData.value.physicalAddress.address2 || undefined,
country: formData.value.physicalAddress.country,
state: formData.value.physicalAddress.state,
city: formData.value.physicalAddress.city,
zip: formData.value.physicalAddress.zip,
},
}
}
} else {
withdrawData.value.providerData.kycData = {
type: 'business',
name: formData.value.businessName || '',
email: formData.value.email,
physicalAddress: {
address1: formData.value.physicalAddress.address1,
address2: formData.value.physicalAddress.address2 || undefined,
country: formData.value.physicalAddress.country,
state: formData.value.physicalAddress.state,
city: formData.value.physicalAddress.city,
zip: formData.value.physicalAddress.zip,
},
}
}
},
{ deep: true },
)
const messages = defineMessages({
entityQuestion: {
id: 'dashboard.creator-withdraw-modal.kyc.entity-question',
defaultMessage: 'Are you a withdrawing as an individual or business?',
},
entityDescription: {
id: 'dashboard.creator-withdraw-modal.kyc.entity-description',
defaultMessage:
'A business entity refers to a registered organization such as a corporation, partnership, or LLC.',
},
privateIndividual: {
id: 'dashboard.creator-withdraw-modal.kyc.private-individual',
defaultMessage: 'Private individual',
},
businessEntity: {
id: 'dashboard.creator-withdraw-modal.kyc.business-entity',
defaultMessage: 'Business entity',
},
})
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div class="flex flex-col gap-2.5 sm:gap-3">
<div class="flex flex-col gap-3">
<div class="flex w-full flex-col gap-1 sm:flex-row sm:justify-between sm:gap-0">
<span class="font-semibold text-contrast">{{ formatMessage(messages.withdrawLimit) }}</span>
<div>
<span class="text-orange">{{ formatMoney(usedLimit) }}</span> /
<span class="text-contrast">{{ formatMoney(600) }}</span>
</div>
</div>
<div class="flex h-2.5 w-full overflow-hidden rounded-full bg-surface-2">
<div
v-if="usedLimit > 0"
class="gradient-border bg-orange"
:style="{ width: `${(usedLimit / 600) * 100}%` }"
></div>
</div>
</div>
<template v-if="remainingLimit > 0">
<span>
<IntlFormatted
:message-id="messages.nearingThreshold"
:values="{
amountRemaining: formatMoney(remainingLimit),
}"
>
<template #b="{ children }">
<span class="font-medium">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</span>
<Admonition
type="warning"
show-actions-underneath
:header="formatMessage(messages.taxFormRequiredHeader)"
>
<span class="text-sm font-normal md:text-base">
{{
formatMessage(messages.taxFormRequiredBodyWithLimit, {
limit: formatMoney(remainingLimit),
})
}}
</span>
<template #icon="{ iconClass }">
<FileTextIcon :class="iconClass" />
</template>
<template #actions>
<ButtonStyled color="orange">
<button @click="showTaxFormModal">
{{ formatMessage(messages.completeTaxForm) }}
</button>
</ButtonStyled>
</template>
</Admonition>
</template>
<template v-else>
<span>
<IntlFormatted
:message-id="messages.withdrawLimitUsed"
:values="{ withdrawLimit: formatMoney(600) }"
>
<template #b="{ children }">
<b>
<component :is="() => normalizeChildren(children)" />
</b>
</template>
</IntlFormatted>
</span>
</template>
</div>
</template>
<script setup lang="ts">
import { FileTextIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { computed } from 'vue'
import { TAX_THRESHOLD_ACTUAL } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const props = defineProps<{
balance: any
onShowTaxForm: () => void
}>()
const { formatMessage } = useVIntl()
const usedLimit = computed(() => props.balance?.withdrawn_ytd ?? 0)
const remainingLimit = computed(() => {
const raw = TAX_THRESHOLD_ACTUAL - usedLimit.value
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100
})
function showTaxFormModal() {
props.onShowTaxForm()
}
const messages = defineMessages({
withdrawLimit: {
id: 'dashboard.creator-withdraw-modal.withdraw-limit',
defaultMessage: 'Withdraw limit',
},
nearingThreshold: {
id: 'dashboard.creator-withdraw-modal.nearing-threshold',
defaultMessage:
"You're nearing the withdraw threshold. You can withdraw <b>{amountRemaining}</b> now, but a tax form is required for more.",
},
taxFormRequiredHeader: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.header',
defaultMessage: 'Tax form required',
},
taxFormRequiredBody: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.body',
defaultMessage:
'To withdraw your full <b>{available}</b> available balance please complete the form below. It is required for tax reporting and only needs to be done once.',
},
taxFormRequiredBodyWithLimit: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.body-with-limit',
defaultMessage:
"You must complete a W-9 or W-8 form for Modrinth's tax records so we remain compliant with tax regulations.",
},
completeTaxForm: {
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
defaultMessage: 'Complete tax form',
},
withdrawLimitUsed: {
id: 'dashboard.creator-withdraw-modal.withdraw-limit-used',
defaultMessage:
"You've used up your <b>{withdrawLimit}</b> withdrawal limit. You must complete a tax form to withdraw more.",
},
})
</script>
<style lang="css" scoped>
.gradient-border {
position: relative;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.3), transparent);
border-radius: inherit;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: xor;
padding: 2px;
pointer-events: none;
}
}
</style>

View File

@@ -0,0 +1,653 @@
<template>
<div class="flex flex-col gap-4 sm:gap-5">
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="isUnverifiedEmail" class="overflow-hidden">
<Admonition type="warning" :header="formatMessage(messages.unverifiedEmailHeader)">
{{ formatMessage(messages.unverifiedEmailMessage) }}
</Admonition>
</div>
</Transition>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="shouldShowUsdWarning" class="overflow-hidden">
<Admonition type="warning" :header="formatMessage(messages.usdPaypalWarningHeader)">
<IntlFormatted :message-id="messages.usdPaypalWarningMessage">
<template #direct-paypal-link="{ children }">
<span class="cursor-pointer text-link" @click="switchToDirectPaypal">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</Admonition>
</div>
</Transition>
<div v-if="!showGiftCardSelector && selectedMethodDisplay" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.paymentMethod) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component :is="selectedMethodDisplay.icon" class="size-5 shrink-0" />
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">{{
typeof selectedMethodDisplay.label === 'string'
? selectedMethodDisplay.label
: formatMessage(selectedMethodDisplay.label)
}}</span>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.email) }} <span class="text-red">*</span></span
>
</label>
<input
v-model="deliveryEmail"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
autocomplete="email"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div v-if="showGiftCardSelector" class="flex flex-col gap-1">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ categoryLabel }} <span class="text-red">*</span></span
>
</label>
<Combobox
v-model="selectedGiftCardId"
:options="rewardOptions"
:placeholder="`Select ${categoryLabel.toLowerCase()}`"
searchable
:search-placeholder="`Search ${categoryLabelPlural.toLowerCase()}...`"
class="h-10"
>
<template #selected>
<div v-if="selectedRewardOption" class="flex items-center gap-2">
<img
v-if="selectedRewardOption.imageUrl"
:src="selectedRewardOption.imageUrl"
:alt="selectedRewardOption.label"
class="size-5 rounded-full object-cover"
loading="lazy"
/>
<span class="font-semibold leading-tight">{{ selectedRewardOption.label }}</span>
</div>
</template>
<template v-for="option in rewardOptions" :key="option.value" #[`option-${option.value}`]>
<div class="flex items-center gap-2">
<img
v-if="option.imageUrl"
:src="option.imageUrl"
:alt="option.label"
class="size-5 rounded-full object-cover"
loading="lazy"
/>
<span class="font-semibold leading-tight">{{ option.label }}</span>
</div>
</template>
</Combobox>
</div>
<span v-if="selectedMethodDetails" class="text-secondary">
{{ formatMoney(effectiveMinAmount) }} min,
{{ formatMoney(selectedMethodDetails.interval?.standard?.max ?? effectiveMaxAmount) }}
max withdrawal amount.
</span>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.amount) }} <span class="text-red">*</span></span
>
</label>
<div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5">
<Chips
v-model="selectedDenomination"
:items="denominationOptions"
:format-label="(amt: number) => formatMoney(amt)"
:never-empty="false"
:capitalize="false"
/>
<span v-if="denominationOptions.length === 0" class="text-error text-sm">
No denominations available for your current balance
</span>
</div>
<div v-else class="flex flex-col gap-2">
<RevenueInputField
v-model="formData.amount"
v-model:selected-currency="selectedCurrency"
:max-amount="effectiveMaxAmount"
:min-amount="effectiveMinAmount"
:show-currency-selector="showPayPalCurrencySelector"
:currency-options="currencyOptions"
/>
</div>
<WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled"
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
:exchange-rate="exchangeRate"
:local-currency="showPayPalCurrencySelector ? selectedCurrency : undefined"
/>
<Checkbox v-model="agreedTerms">
<span>
<IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template>
</IntlFormatted>
</span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import {
Admonition,
Checkbox,
Chips,
Combobox,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
paymentMethodMessages,
useDebugLogger,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { useAuth } from '@/composables/auth.js'
import { useBaseFetch } from '@/composables/fetch.js'
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const debug = useDebugLogger('TremendousDetailsStage')
const {
withdrawData,
maxWithdrawAmount,
availableMethods,
paymentOptions,
calculateFees,
setStage,
paymentMethodsCache,
} = useWithdrawContext()
const { formatMessage } = useVIntl()
const auth = await useAuth()
const userEmail = computed(() => {
return (auth.value.user as any)?.email || ''
})
const providerData = withdrawData.value.providerData
const initialDeliveryEmail =
providerData.type === 'tremendous'
? providerData.deliveryEmail || userEmail.value || ''
: userEmail.value || ''
const deliveryEmail = ref<string>(initialDeliveryEmail)
const showGiftCardSelector = computed(() => {
const method = withdrawData.value.selection.method
return method === 'merchant_card' || method === 'charity'
})
const showPayPalCurrencySelector = computed(() => {
const method = withdrawData.value.selection.method
return method === 'paypal'
})
const shouldShowUsdWarning = computed(() => {
const method = withdrawData.value.selection.method
const currency = selectedCurrency.value
return method === 'paypal' && currency === 'USD'
})
const selectedMethodDisplay = computed(() => {
const method = withdrawData.value.selection.method
if (!method) return null
return paymentOptions.value.find((m) => m.value === method) || null
})
const categoryLabel = computed(() => {
const method = withdrawData.value.selection.method
switch (method) {
case 'visa_card':
return formatMessage(paymentMethodMessages.virtualVisa)
case 'merchant_card':
return formatMessage(paymentMethodMessages.giftCard)
case 'charity':
return formatMessage(paymentMethodMessages.charity)
default:
return formatMessage(messages.reward)
}
})
const categoryLabelPlural = computed(() => {
const method = withdrawData.value.selection.method
switch (method) {
case 'visa_card':
return formatMessage(paymentMethodMessages.virtualVisaPlural)
case 'merchant_card':
return formatMessage(paymentMethodMessages.giftCardPlural)
case 'charity':
return formatMessage(paymentMethodMessages.charityPlural)
default:
return formatMessage(messages.rewardPlural)
}
})
const isUnverifiedEmail = computed(() => {
if (!deliveryEmail.value || !userEmail.value) return false
return deliveryEmail.value.toLowerCase() !== userEmail.value.toLowerCase()
})
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const formData = ref<Record<string, any>>({
amount: withdrawData.value.calculation.amount || undefined,
})
const selectedGiftCardId = ref<string | null>(withdrawData.value.selection.methodId || null)
const currencyOptions = [
{ value: 'USD', label: 'USD' },
{ value: 'AUD', label: 'AUD' },
{ value: 'CAD', label: 'CAD' },
{ value: 'CHF', label: 'CHF' },
{ value: 'CZK', label: 'CZK' },
{ value: 'DKK', label: 'DKK' },
{ value: 'EUR', label: 'EUR' },
{ value: 'GBP', label: 'GBP' },
{ value: 'MXN', label: 'MXN' },
{ value: 'NOK', label: 'NOK' },
{ value: 'NZD', label: 'NZD' },
{ value: 'PLN', label: 'PLN' },
{ value: 'SEK', label: 'SEK' },
{ value: 'SGD', label: 'SGD' },
]
function getCurrencyFromCountryCode(countryCode: string | undefined): string {
if (!countryCode) return 'USD'
const code = countryCode.toUpperCase()
const countryToCurrency: Record<string, string> = {
US: 'USD', // United States
GB: 'GBP', // UK
CA: 'CAD', // Canada
AU: 'AUD', // Australia
CH: 'CHF', // Switzerland
CZ: 'CZK', // Czech Republic
DK: 'DKK', // Denmark
MX: 'MXN', // Mexico
NO: 'NOK', // Norway
NZ: 'NZD', // New Zealand
PL: 'PLN', // Poland
SE: 'SEK', // Sweden
SG: 'SGD', // Singapore
// Eurozone countries
AT: 'EUR', // Austria
BE: 'EUR', // Belgium
CY: 'EUR', // Cyprus
EE: 'EUR', // Estonia
FI: 'EUR', // Finland
FR: 'EUR', // France
DE: 'EUR', // Germany
GR: 'EUR', // Greece
IE: 'EUR', // Ireland
IT: 'EUR', // Italy
LV: 'EUR', // Latvia
LT: 'EUR', // Lithuania
LU: 'EUR', // Luxembourg
MT: 'EUR', // Malta
NL: 'EUR', // Netherlands
PT: 'EUR', // Portugal
SK: 'EUR', // Slovakia
SI: 'EUR', // Slovenia
ES: 'EUR', // Spain
}
return countryToCurrency[code] || 'USD'
}
const initialCurrency = getCurrencyFromCountryCode(withdrawData.value.selection.country?.id)
const selectedCurrency = ref<string>(initialCurrency)
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const calculatedFee = ref<number>(0)
const exchangeRate = ref<number | null>(null)
const feeLoading = ref(false)
const rewardOptions = ref<
Array<{
value: string
label: string
imageUrl?: string
methodDetails?: {
id: string
name: string
interval?: {
fixed?: { values: number[] }
standard?: { min: number; max: number }
}
}
}>
>([])
const selectedRewardOption = computed(() => {
if (!selectedGiftCardId.value) return null
return rewardOptions.value.find((opt) => opt.value === selectedGiftCardId.value) || null
})
const selectedMethodDetails = computed(() => {
console.log(rewardOptions.value, selectedGiftCardId.value)
if (!selectedGiftCardId.value) return null
const option = rewardOptions.value.find((opt) => opt.value === selectedGiftCardId.value)
debug('Selected method details:', option?.methodDetails)
return option?.methodDetails || null
})
const useFixedDenominations = computed(() => {
const hasFixed = !!selectedMethodDetails.value?.interval?.fixed?.values
debug('Use fixed denominations:', hasFixed, selectedMethodDetails.value?.interval)
return hasFixed
})
const denominationOptions = computed(() => {
const fixedValues = selectedMethodDetails.value?.interval?.fixed?.values
if (!fixedValues) return []
const filtered = fixedValues
.filter((amount) => amount <= roundedMaxAmount.value)
.sort((a, b) => a - b)
debug(
'Denomination options (filtered by max):',
filtered,
'from',
fixedValues,
'max:',
roundedMaxAmount.value,
)
return filtered
})
const effectiveMinAmount = computed(() => {
return selectedMethodDetails.value?.interval?.standard?.min || 0.01
})
const effectiveMaxAmount = computed(() => {
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
if (methodMax !== undefined && methodMax !== null) {
return Math.min(roundedMaxAmount.value, methodMax)
}
return roundedMaxAmount.value
})
const selectedDenomination = computed({
get: () => formData.value.amount,
set: (value) => {
formData.value.amount = value
},
})
const allRequiredFieldsFilled = computed(() => {
const amount = formData.value.amount
if (!amount || amount <= 0) return false
if (!deliveryEmail.value) return false
if (showGiftCardSelector.value && !selectedGiftCardId.value) return false
return true
})
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
if (!amount || amount <= 0) {
calculatedFee.value = 0
exchangeRate.value = null
return
}
const methodId = showGiftCardSelector.value
? selectedGiftCardId.value
: withdrawData.value.selection.methodId
if (!methodId) {
calculatedFee.value = 0
exchangeRate.value = null
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee ?? 0
exchangeRate.value = withdrawData.value.calculation.exchangeRate
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
exchangeRate.value = null
} finally {
feeLoading.value = false
}
}, 500)
watch(deliveryEmail, (newEmail) => {
if (withdrawData.value.providerData.type === 'tremendous') {
withdrawData.value.providerData.deliveryEmail = newEmail
}
})
watch(
selectedCurrency,
(newCurrency) => {
if (withdrawData.value.providerData.type === 'tremendous') {
;(withdrawData.value.providerData as any).currency = newCurrency
}
},
{ immediate: true },
)
watch(
() => withdrawData.value.selection.country?.id,
(newCountryId) => {
if (showPayPalCurrencySelector.value && newCountryId) {
const detectedCurrency = getCurrencyFromCountryCode(newCountryId)
selectedCurrency.value = detectedCurrency
}
},
)
watch(
[() => formData.value.amount, selectedGiftCardId, deliveryEmail, selectedCurrency],
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (showGiftCardSelector.value && selectedGiftCardId.value) {
withdrawData.value.selection.methodId = selectedGiftCardId.value
}
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = 0
exchangeRate.value = null
feeLoading.value = false
}
},
{ deep: true },
)
onMounted(async () => {
const methods = availableMethods.value
const selectedMethod = withdrawData.value.selection.method
rewardOptions.value = methods
.filter((m) => m.type === 'tremendous')
.filter(
(m) =>
m.category === selectedMethod ||
(selectedMethod === 'merchant_card' && m.category === 'visa_card'),
)
.map((m) => ({
value: m.id,
label: m.name,
imageUrl: m.image_url || m.image_logo_url || undefined,
methodDetails: {
id: m.id,
name: m.name,
interval: m.interval,
},
}))
debug('Loaded reward options:', rewardOptions.value.length, 'methods')
debug('Sample method with interval:', rewardOptions.value[0]?.methodDetails)
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
}
})
watch(
() => withdrawData.value.selection.method,
(newMethod, oldMethod) => {
if (oldMethod && newMethod !== oldMethod) {
formData.value = {
amount: undefined,
}
selectedGiftCardId.value = null
calculatedFee.value = 0
exchangeRate.value = null
// Clear currency when switching away from PayPal International
if (newMethod !== 'paypal' && withdrawData.value.providerData.type === 'tremendous') {
;(withdrawData.value.providerData as any).currency = undefined
}
}
},
)
async function switchToDirectPaypal() {
withdrawData.value.selection.country = {
id: 'US',
name: 'United States',
}
let usMethods = paymentMethodsCache.value['US']
if (!usMethods) {
try {
usMethods = (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: 'US' },
})) as PayoutMethod[]
paymentMethodsCache.value['US'] = usMethods
} catch (error) {
console.error('Failed to fetch US payment methods:', error)
return
}
}
availableMethods.value = usMethods
const directPaypal = usMethods.find((m) => m.type === 'paypal')
if (directPaypal) {
withdrawData.value.selection.provider = 'paypal'
withdrawData.value.selection.method = directPaypal.id
withdrawData.value.selection.methodId = directPaypal.id
withdrawData.value.providerData = {
type: 'paypal',
}
await setStage('paypal-details', true)
} else {
console.error('An error occured - no paypal in US region??')
}
}
const messages = defineMessages({
unverifiedEmailHeader: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header',
defaultMessage: 'Unverified email',
},
unverifiedEmailMessage: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-message',
defaultMessage:
'The delivery email you have entered is not associated with your Modrinth account. Modrinth cannot recover rewards sent to an incorrect email address.',
},
paymentMethod: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.payment-method',
defaultMessage: 'Payment method',
},
reward: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward',
defaultMessage: 'Reward',
},
rewardPlaceholder: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward-placeholder',
defaultMessage: 'Select reward',
},
rewardPlural: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward-plural',
defaultMessage: 'Rewards',
},
usdPaypalWarningHeader: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.usd-paypal-warning-header',
defaultMessage: 'Lower fees available',
},
usdPaypalWarningMessage: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.usd-paypal-warning-message',
defaultMessage:
'You selected USD for PayPal International. <direct-paypal-link>Switch to direct PayPal</direct-paypal-link> for better fees (≈2% instead of ≈6%).',
},
})
</script>

View File

@@ -139,7 +139,7 @@ const messages = defineMessages({
resubmitForReviewDesc: {
id: 'project-moderation-nags.resubmit-for-review-desc',
defaultMessage:
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
"Your project has been {status, select, rejected {rejected} withheld {withheld} other {{status}}} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
},
visitModerationPage: {
id: 'project-moderation-nags.visit-moderation-page',

View File

@@ -85,6 +85,7 @@
<ModpackPermissionsFlow
v-model="modpackJudgements"
:project-id="project.id"
:project-updated="project.updated"
@complete="handleModpackPermissionsComplete"
/>
</div>
@@ -446,6 +447,11 @@ function resetProgress() {
localStorage.removeItem(`modpack-permissions-${props.project.id}`)
localStorage.removeItem(`modpack-permissions-index-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-data-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-permanent-no-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-updated-${props.project.id}`)
modpackPermissionsComplete.value = false
modpackJudgements.value = {}
@@ -1331,6 +1337,11 @@ function clearProjectLocalStorage() {
localStorage.removeItem(`moderation-actions-${props.project.slug}`)
localStorage.removeItem(`moderation-inputs-${props.project.slug}`)
localStorage.removeItem(`moderation-stage-${props.project.slug}`)
sessionStorage.removeItem(`modpack-permissions-data-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-permanent-no-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-updated-${props.project.id}`)
actionStates.value = {}
}

View File

@@ -161,6 +161,7 @@ import { computed, onMounted, ref, watch } from 'vue'
const props = defineProps<{
projectId: string
projectUpdated: string
modelValue?: ModerationJudgements
}>()
@@ -202,6 +203,10 @@ const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
},
},
)
const cachedProjectUpdated = useSessionStorage<string | null>(
`modpack-permissions-updated-${props.projectId}`,
null,
)
const currentIndex = ref(0)
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
@@ -363,11 +368,13 @@ async function fetchModPackData(): Promise<void> {
}
modPackData.value = sortedData
cachedProjectUpdated.value = props.projectUpdated
persistAll()
} catch (error) {
console.error('Failed to fetch modpack data:', error)
modPackData.value = []
permanentNoFiles.value = []
cachedProjectUpdated.value = props.projectUpdated
persistAll()
}
}
@@ -457,7 +464,10 @@ function getJudgements(): ModerationJudgements {
onMounted(() => {
loadPersistedData()
if (!modPackData.value) {
const isStale = cachedProjectUpdated.value !== props.projectUpdated
if (!modPackData.value || isStale) {
fetchModPackData()
}
})
@@ -477,6 +487,7 @@ watch(
() => props.projectId,
() => {
clearPersistedData()
cachedProjectUpdated.value = null
loadPersistedData()
if (!modPackData.value) {
fetchModPackData()

View File

@@ -105,10 +105,9 @@
<script setup>
import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets'
import { Avatar, Badge, CopyCode, useRelativeTime } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { formatProjectType, renderHighlightedString } from '@modrinth/utils'
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
import { renderHighlightedString } from '~/helpers/highlight.js'
import { getProjectTypeForUrl } from '~/helpers/projects.js'
const formatRelativeTime = useRelativeTime()

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